diff --git a/docs/libraries/api-clients/go.mdx b/docs/libraries/api-clients/go.mdx index 05227060..918b62b7 100644 --- a/docs/libraries/api-clients/go.mdx +++ b/docs/libraries/api-clients/go.mdx @@ -265,6 +265,158 @@ response, err := client.Client.Chat.Create(ctx, &glean.ChatRequest{ }) ``` +### OAuth Authentication + +OAuth allows you to use access tokens from your identity provider (Google, Azure, Okta, etc.) instead of Glean-issued tokens. + +:::info Prerequisites +- OAuth enabled in [Glean Admin > Third-Party OAuth](https://app.glean.com/admin/setup/third-party-oauth) +- Your OAuth Client ID registered with Glean +- See [OAuth Setup Guide](https://docs.glean.com/administration/oauth/oauth-idp) for admin configuration +::: + +OAuth requests require these headers: + +| Header | Value | +|--------|-------| +| `Authorization` | `Bearer ` | +| `X-Glean-Auth-Type` | `OAUTH` | + +#### Example: Authorization Code Flow + +This example uses [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2): + +```go +package main + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "net/http" + "os" + "sync" + + "golang.org/x/oauth2" + glean "github.com/gleanwork/api-client-go" + "github.com/gleanwork/api-client-go/models/components" +) + +var oauthConfig = &oauth2.Config{ + ClientID: os.Getenv("OAUTH_CLIENT_ID"), + ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"), + RedirectURL: "http://localhost:8080/callback", + Scopes: []string{"openid", "email"}, + Endpoint: oauth2.Endpoint{ + AuthURL: os.Getenv("OAUTH_AUTH_URL"), + TokenURL: os.Getenv("OAUTH_TOKEN_URL"), + }, +} + +// In-memory store for state and PKCE verifier (use Redis/database in production) +var ( + authStore = make(map[string]string) // state -> verifier + authMu sync.Mutex +) + +func generateState() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(b), nil +} + +// oauthTransport adds OAuth headers to all requests +type oauthTransport struct { + token string + transport http.RoundTripper +} + +func (t *oauthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("Authorization", "Bearer "+t.token) + req.Header.Set("X-Glean-Auth-Type", "OAUTH") + return t.transport.RoundTrip(req) +} + +func main() { + http.HandleFunc("/login", handleLogin) + http.HandleFunc("/callback", handleCallback) + http.ListenAndServe(":8080", nil) +} + +func handleLogin(w http.ResponseWriter, r *http.Request) { + state, err := generateState() + if err != nil { + http.Error(w, "Failed to generate state", http.StatusInternalServerError) + return + } + + // Generate PKCE code verifier + verifier := oauth2.GenerateVerifier() + + // Store state and verifier for callback validation + authMu.Lock() + authStore[state] = verifier + authMu.Unlock() + + // Include PKCE challenge in authorization URL + url := oauthConfig.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier)) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func handleCallback(w http.ResponseWriter, r *http.Request) { + // Validate state and retrieve PKCE verifier + state := r.URL.Query().Get("state") + authMu.Lock() + verifier, valid := authStore[state] + delete(authStore, state) + authMu.Unlock() + + if !valid { + http.Error(w, "Invalid state parameter", http.StatusBadRequest) + return + } + + // Exchange code for token with PKCE verifier + code := r.URL.Query().Get("code") + token, err := oauthConfig.Exchange(context.Background(), code, oauth2.VerifierOption(verifier)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Create HTTP client with OAuth headers + httpClient := &http.Client{ + Transport: &oauthTransport{ + token: token.AccessToken, + transport: http.DefaultTransport, + }, + } + + // Create Glean client with custom HTTP client + client := glean.New( + glean.WithInstance(os.Getenv("GLEAN_INSTANCE")), + glean.WithClient(httpClient), + ) + + results, err := client.Client.Search.Query(r.Context(), components.SearchRequest{ + Query: "quarterly reports", + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(results) +} +``` + +:::tip +Access tokens typically expire after ~1 hour. For production use, use `oauth2.Config.TokenSource` for automatic refresh. +::: + ## Error Handling ```go diff --git a/docs/libraries/api-clients/java.mdx b/docs/libraries/api-clients/java.mdx index c376b23c..4f8691d3 100644 --- a/docs/libraries/api-clients/java.mdx +++ b/docs/libraries/api-clients/java.mdx @@ -230,6 +230,152 @@ glean: instance: ${GLEAN_INSTANCE} ``` +### OAuth Authentication + +OAuth allows you to use access tokens from your identity provider (Google, Azure, Okta, etc.) instead of Glean-issued tokens. + +:::info Prerequisites +- OAuth enabled in [Glean Admin > Third-Party OAuth](https://app.glean.com/admin/setup/third-party-oauth) +- Your OAuth Client ID registered with Glean +- See [OAuth Setup Guide](https://docs.glean.com/administration/oauth/oauth-idp) for admin configuration +::: + +OAuth requests require these headers: + +| Header | Value | +|--------|-------| +| `Authorization` | `Bearer ` | +| `X-Glean-Auth-Type` | `OAUTH` | + +#### Example: Authorization Code Flow with Spring Security + +This example uses Spring Security OAuth2 Client: + +```yaml +# application.yml +spring: + security: + oauth2: + client: + registration: + glean: + provider: glean + client-id: ${OAUTH_CLIENT_ID} + client-secret: ${OAUTH_CLIENT_SECRET} + scope: openid,email + authorization-grant-type: authorization_code + provider: + glean: + issuer-uri: ${OAUTH_ISSUER} + +glean: + instance: ${GLEAN_INSTANCE} +``` + +```java +// SecurityConfig.java - Enable PKCE for confidential clients +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, + ClientRegistrationRepository clientRegistrationRepository) throws Exception { + + // Create resolver with PKCE enabled + OAuth2AuthorizationRequestResolver pkceResolver = + pkceAuthorizationRequestResolver(clientRegistrationRepository); + + http.oauth2Login(oauth2 -> oauth2 + .authorizationEndpoint(auth -> auth + .authorizationRequestResolver(pkceResolver))); + + return http.build(); + } + + private OAuth2AuthorizationRequestResolver pkceAuthorizationRequestResolver( + ClientRegistrationRepository repo) { + var resolver = new DefaultOAuth2AuthorizationRequestResolver(repo, "/oauth2/authorization"); + resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce()); + return resolver; + } +} +``` + +```java +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.web.bind.annotation.*; +import com.glean.api_client.glean_api_client.Glean; +import com.glean.api_client.glean_api_client.models.components.*; +import com.glean.api_client.glean_api_client.utils.HTTPClient; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Map; + +@RestController +public class GleanOAuthController { + + @Value("${glean.instance}") + private String gleanInstance; + + @GetMapping("/search") + public Map search( + @RegisteredOAuth2AuthorizedClient("glean") OAuth2AuthorizedClient oauthClient, + @RequestParam String query) throws Exception { + + String accessToken = oauthClient.getAccessToken().getTokenValue(); + + // Create custom HTTP client that adds OAuth headers + HTTPClient oauthHttpClient = new HTTPClient() { + private final HttpClient client = HttpClient.newHttpClient(); + + @Override + public HttpResponse send(HttpRequest request) + throws java.io.IOException, InterruptedException { + HttpRequest oauthRequest = HttpRequest.newBuilder(request, (n, v) -> true) + .header("Authorization", "Bearer " + accessToken) + .header("X-Glean-Auth-Type", "OAUTH") + .build(); + return client.send(oauthRequest, HttpResponse.BodyHandlers.ofInputStream()); + } + }; + + // Create Glean client with custom HTTP client + Glean glean = Glean.builder() + .instance(gleanInstance) + .client(oauthHttpClient) + .build(); + + var response = glean.client().search().query() + .searchRequest(SearchRequest.builder() + .query(query) + .pageSize(10) + .build()) + .call(); + + return Map.of("results", response.searchResponse() + .map(SearchResponse::results) + .orElse(List.of())); + } +} +``` + +:::tip +Access tokens typically expire after ~1 hour. Spring Security OAuth2 Client handles token refresh automatically when configured with a refresh token. +::: + ## Error Handling ```java diff --git a/docs/libraries/api-clients/python.mdx b/docs/libraries/api-clients/python.mdx index 8bb21c43..ca9afe74 100644 --- a/docs/libraries/api-clients/python.mdx +++ b/docs/libraries/api-clients/python.mdx @@ -208,6 +208,85 @@ response = client.client.chat.create( ) ``` +### OAuth Authentication + +OAuth allows you to use access tokens from your identity provider (Google, Azure, Okta, etc.) instead of Glean-issued tokens. + +:::info Prerequisites +- OAuth enabled in [Glean Admin > Third-Party OAuth](https://app.glean.com/admin/setup/third-party-oauth) +- Your OAuth Client ID registered with Glean +- See [OAuth Setup Guide](https://docs.glean.com/administration/oauth/oauth-idp) for admin configuration +::: + +OAuth requests require these headers: + +| Header | Value | +|--------|-------| +| `Authorization` | `Bearer ` | +| `X-Glean-Auth-Type` | `OAUTH` | + +#### Example: Authorization Code Flow + +This example uses [Authlib](https://pypi.org/project/Authlib/) with Flask: + +```python +import os +import httpx +from flask import Flask, redirect, request, jsonify +from authlib.integrations.flask_client import OAuth +from glean.api_client import Glean +from glean.api_client import models + +app = Flask(__name__) +app.secret_key = os.urandom(24) + +oauth = OAuth(app) +oauth.register( + name='provider', + client_id=os.getenv('OAUTH_CLIENT_ID'), + client_secret=os.getenv('OAUTH_CLIENT_SECRET'), + server_metadata_url=os.getenv('OAUTH_ISSUER') + '/.well-known/openid-configuration', + client_kwargs={'scope': 'openid email'}, + code_challenge_method='S256', # Enable PKCE +) + +@app.route('/login') +def login(): + redirect_uri = 'http://localhost:5000/callback' + return oauth.provider.authorize_redirect(redirect_uri) + +@app.route('/callback') +def callback(): + token = oauth.provider.authorize_access_token() + + # Create HTTP client with OAuth headers + http_client = httpx.Client(headers={ + 'Authorization': f"Bearer {token['access_token']}", + 'X-Glean-Auth-Type': 'OAUTH', + }) + + # Use OAuth token with Glean + with Glean( + instance=os.getenv('GLEAN_INSTANCE'), + client=http_client, + ) as glean: + results = glean.client.search.query( + search_request=models.SearchRequest( + query='quarterly reports', + page_size=10, + ) + ) + + return jsonify(results.to_dict()) + +if __name__ == '__main__': + app.run(port=5000) +``` + +:::tip +Access tokens typically expire after ~1 hour. For production use, implement token refresh using `token['refresh_token']`. +::: + ## Error Handling ```python diff --git a/docs/libraries/api-clients/typescript.mdx b/docs/libraries/api-clients/typescript.mdx index 334704b7..dfabe83c 100644 --- a/docs/libraries/api-clients/typescript.mdx +++ b/docs/libraries/api-clients/typescript.mdx @@ -196,6 +196,107 @@ const response = await client.client.chat.create({ }); ``` +### OAuth Authentication + +OAuth allows you to use access tokens from your identity provider (Google, Azure, Okta, etc.) instead of Glean-issued tokens. + +:::info Prerequisites +- OAuth enabled in [Glean Admin > Third-Party OAuth](https://app.glean.com/admin/setup/third-party-oauth) +- Your OAuth Client ID registered with Glean +- See [OAuth Setup Guide](https://docs.glean.com/administration/oauth/oauth-idp) for admin configuration +::: + +OAuth requests require these headers: + +| Header | Value | +|--------|-------| +| `Authorization` | `Bearer ` | +| `X-Glean-Auth-Type` | `OAUTH` | + +#### Example: Authorization Code Flow + +This example uses [openid-client](https://github.com/panva/openid-client) (v6+) with Express: + +```typescript +import express from 'express'; +import session from 'express-session'; +import * as client from 'openid-client'; +import { Glean } from '@gleanwork/api-client'; + +const config = { + clientId: process.env.OAUTH_CLIENT_ID!, + clientSecret: process.env.OAUTH_CLIENT_SECRET!, + redirectUri: 'http://localhost:3000/callback', + issuer: process.env.OAUTH_ISSUER!, // e.g., https://accounts.google.com +}; + +const app = express(); +app.use(session({ secret: 'session-secret', resave: false, saveUninitialized: false })); + +let oidcConfig: client.Configuration; + +// Initialize OIDC client on startup +async function init() { + oidcConfig = await client.discovery( + new URL(config.issuer), + config.clientId, + config.clientSecret + ); +} + +app.get('/login', async (req, res) => { + // Generate PKCE code verifier and state for security + const codeVerifier = client.randomPKCECodeVerifier(); + const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier); + const state = client.randomState(); + + // Store in session for verification on callback + req.session.codeVerifier = codeVerifier; + req.session.state = state; + + const authUrl = client.buildAuthorizationUrl(oidcConfig, { + redirect_uri: config.redirectUri, + scope: 'openid email', + code_challenge: codeChallenge, + code_challenge_method: 'S256', + state, + }); + res.redirect(authUrl.href); +}); + +app.get('/callback', async (req, res) => { + const { codeVerifier, state } = req.session; + const callbackUrl = new URL(req.url, `http://${req.headers.host}`); + + // Exchange code for tokens with PKCE and state verification + const tokens = await client.authorizationCodeGrant(oidcConfig, callbackUrl, { + pkceCodeVerifier: codeVerifier, + expectedState: state, + }); + + // Use OAuth token with Glean + const glean = new Glean({ instance: process.env.GLEAN_INSTANCE! }); + + const results = await glean.client.search.query({ + query: 'quarterly reports', + pageSize: 10, + }, { + headers: { + 'Authorization': `Bearer ${tokens.access_token}`, + 'X-Glean-Auth-Type': 'OAUTH', + }, + }); + + res.json(results); +}); + +init().then(() => app.listen(3000)); +``` + +:::tip +Access tokens typically expire after ~1 hour. For production use, implement token refresh using `tokens.refresh_token`. +::: + ## Error Handling ```typescript