Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions docs/libraries/api-clients/go.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <oauth_access_token>` |
| `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"},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3/5 (strong opinion: non-blocking)

This should include offline_access to showcase getting a refresh token

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)
}
Comment on lines +349 to +367
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4/5 (serious issue: blocking)

Have we validated this code works? We're OAuth 2.1 so state is optional and not even recommended but PKCE is required.

That we're generating a state suggests we might be not generating a PKCE challenge.


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
Expand Down
146 changes: 146 additions & 0 deletions docs/libraries/api-clients/java.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <oauth_access_token>` |
| `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<String, Object> 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<java.io.InputStream> 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
Expand Down
79 changes: 79 additions & 0 deletions docs/libraries/api-clients/python.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <oauth_access_token>` |
| `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
Expand Down
Loading