Skip to content

Commit 2dee73a

Browse files
refactor: add debug logging to device flow, improve auth UX and README
- Add auth:device debug namespace for tracing device flow login - Improve fallback message: "Your SSO provider requires device-based login" instead of confusing OAuth jargon - Rewrite README auth section with scannable table showing three login methods and when each is used - Add "Credential resolution order" subsection - Note that API tokens are scoped to individual user accounts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f0937a6 commit 2dee73a

4 files changed

Lines changed: 27 additions & 9 deletions

File tree

README.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ Search across your company's knowledge, chat with Glean Assistant, manage the fu
1919
- [Quick Start](#quick-start)
2020
- [Why Glean CLI?](#why-glean-cli)
2121
- [Authentication](#authentication)
22-
- [OAuth (recommended)](#oauth-recommended)
2322
- [API Token (CI/CD)](#api-token-cicd)
23+
- [Credential resolution order](#credential-resolution-order)
2424
- [Interactive TUI](#interactive-tui)
2525
- [Keyboard Shortcuts](#keyboard-shortcuts)
2626
- [Slash Commands](#slash-commands)
@@ -95,28 +95,36 @@ glean search "engineering docs" --output ndjson | jq .title
9595

9696
## Authentication
9797

98-
### OAuth (recommended)
99-
10098
```bash snippet=readme/snippet-04.sh
101-
glean auth login # browser PKCE flow, or device flow for SSO/Okta
99+
glean auth login # interactive login (detects the best method automatically)
102100
glean auth status # verify credentials, host, and token expiry
103101
glean auth logout # remove all stored credentials
104102
```
105103

106-
OAuth uses PKCE with Dynamic Client Registration when available. For SSO configurations where DCR is unavailable (e.g. Okta), `auth login` falls back to the Device Authorization Grant (RFC 8628) — you'll approve the login on a verification page instead. Tokens are stored securely in the system keyring and refreshed automatically.
104+
`glean auth login` detects the right authentication method for your environment automatically:
105+
106+
| Method | When it's used | What happens |
107+
| --- | --- | --- |
108+
| **Browser login** | Default for most Glean instances | Opens your browser, you approve, done |
109+
| **Device code login** | Organizations using an external IdP (e.g. Okta) | Prints a URL and code — open the URL, enter the code |
110+
| **API token** | Instances without OAuth support | Prompts you to paste a token from Glean Admin |
107111

108-
For instances that don't support OAuth at all, `auth login` falls back to prompting for an API token.
112+
You don't need to choose — `auth login` tries each method in order and uses the first one that works. Tokens are stored securely in the system keyring and refreshed automatically.
109113

110114
### API Token (CI/CD)
111115

112-
Set credentials via environment variables — no interactive login needed:
116+
For non-interactive environments, set credentials via environment variables:
113117

114118
```bash snippet=readme/snippet-05.sh
115119
export GLEAN_API_TOKEN=your-token
116120
export GLEAN_HOST=your-company-be.glean.com
117121
glean search "test"
118122
```
119123

124+
API tokens are scoped to an individual user account. Generate one from **Glean Admin → Settings → API Tokens**.
125+
126+
### Credential resolution order
127+
120128
Credentials are resolved in this order: environment variables → system keyring → `~/.glean/config.json`.
121129

122130
## Interactive TUI

internal/auth/auth.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var (
3131
dcrLog = debug.New("auth:dcr")
3232
tokenLog = debug.New("auth:token")
3333
emailLog = debug.New("auth:email")
34+
deviceLog = debug.New("auth:device")
3435
)
3536

3637
//go:embed success.html
@@ -66,18 +67,21 @@ func Login(ctx context.Context) error {
6667
loginLog.Log("OAuth discovery succeeded: auth=%s token=%s registration=%s", disc.Endpoint.AuthURL, disc.Endpoint.TokenURL, disc.RegistrationEndpoint)
6768

6869
// Try DCR / static client first (standard authorization code flow).
70+
loginLog.Log("attempting authorization code + PKCE flow")
6971
authCodeErr := tryAuthCodeLogin(ctx, host, disc)
7072
if authCodeErr == nil {
7173
return nil
7274
}
75+
loginLog.Log("auth code flow failed: %v", authCodeErr)
7376

7477
// Only fall back to device flow when the auth code flow failed because no
7578
// OAuth client could be obtained (DCR unsupported + no static client).
7679
// Transient failures (network, user closing browser, port conflicts) should
7780
// not silently switch to a different grant type.
7881
canDeviceFlow := disc.DeviceFlowClientID != "" && disc.DeviceAuthEndpoint != ""
7982
if errors.Is(authCodeErr, errNoOAuthClient) && canDeviceFlow {
80-
fmt.Fprintf(os.Stderr, "Note: no OAuth client available, trying device flow…\n")
83+
loginLog.Log("falling back to device authorization grant (client_id=%s)", disc.DeviceFlowClientID)
84+
fmt.Fprintf(os.Stderr, "\nYour SSO provider requires device-based login.\n")
8185
return deviceFlowLogin(ctx, host, disc)
8286
}
8387

internal/auth/device.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ type deviceTokenError struct {
4040
// deviceFlowLogin performs the OAuth 2.0 Device Authorization Grant (RFC 8628).
4141
func deviceFlowLogin(ctx context.Context, host string, disc *discoveryResult) error {
4242
scopes := resolveScopes(disc.Provider)
43+
deviceLog.Log("requesting device code from %s (client_id=%s)", disc.DeviceAuthEndpoint, disc.DeviceFlowClientID)
4344

4445
authResp, err := requestDeviceCode(ctx, disc.DeviceAuthEndpoint, disc.DeviceFlowClientID, scopes)
4546
if err != nil {
4647
return fmt.Errorf("device authorization request failed: %w", err)
4748
}
49+
deviceLog.Log("device code received: user_code=%s verification_uri=%s expires_in=%d", authResp.UserCode, authResp.VerificationURI, authResp.ExpiresIn)
4850

4951
verificationURL := authResp.VerificationURIComplete
5052
if verificationURL == "" {
@@ -69,10 +71,13 @@ func deviceFlowLogin(ctx context.Context, host string, disc *discoveryResult) er
6971

7072
_ = browser.OpenURL(verificationURL)
7173

74+
deviceLog.Log("polling token endpoint %s (interval=%ds)", disc.Endpoint.TokenURL, authResp.Interval)
7275
token, err := pollForToken(ctx, disc.Endpoint.TokenURL, disc.DeviceFlowClientID, authResp)
7376
if err != nil {
77+
deviceLog.Log("device flow failed: %v", err)
7478
return fmt.Errorf("device flow login failed: %w", err)
7579
}
80+
deviceLog.Log("device flow token received")
7681

7782
return saveAndPrintToken(ctx, host, disc, disc.DeviceFlowClientID, token)
7883
}
@@ -105,6 +110,7 @@ func requestDeviceCode(ctx context.Context, endpoint, clientID string, scopes []
105110
desc = errResp.Error
106111
}
107112
if errResp.Error == "unauthorized_client" {
113+
deviceLog.Log("IdP rejected device code grant for client %s: %s", clientID, desc)
108114
return nil, fmt.Errorf("%s\n\nAsk your IdP administrator to add the device_code grant type\nto OAuth app %s", desc, clientID)
109115
}
110116
if desc != "" {

snippets/readme/snippet-04.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
glean auth login # browser PKCE flow, or device flow for SSO/Okta
1+
glean auth login # interactive login (detects the best method automatically)
22
glean auth status # verify credentials, host, and token expiry
33
glean auth logout # remove all stored credentials

0 commit comments

Comments
 (0)