docs: rewrite developer docs for integrator audience#169
Conversation
- Split operator content out; focus on applications integrating with AuthGate - Add OIDC Discovery entry point, supported scopes, and confidential-client paths - Add new pages for OpenID Connect, Tokens and Revocation, and Errors - Document nonce, PKCE rules, rotation reuse-detection, and rate-limit handling - Register new doc pages in the sidebar - Add *.pem and *.key to .gitignore to prevent signing-key leaks - Fix mermaid v11 rendering by removing JSON bodies, aliases, and em dashes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR rewrites the embedded developer documentation (internal/templates/docs/*.md) to target integrators (client developers) rather than operators, making OIDC Discovery the primary entry point and expanding guidance on flows, token handling, and errors.
Changes:
- Reframes “Getting Started” and per-flow docs around integration concerns (Discovery-first, PKCE/state/nonce guidance, confidential vs public client paths).
- Adds three new integrator-focused pages: OpenID Connect, Tokens & Revocation, and Errors, and wires them into the docs sidebar.
- Updates ancillary repo hygiene/docs rendering support (Mermaid adjustments) and adds
*.pem/*.keyto.gitignoreto reduce accidental key commits.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/templates/docs/tokens.md | New page documenting refresh, rotation-mode behavior, revoke/introspect/tokeninfo, and rate limits for integrators |
| internal/templates/docs/oidc.md | New page covering ID tokens vs access tokens, claim validation, and UserInfo usage |
| internal/templates/docs/errors.md | New error catalog with scenario-based handling guidance |
| internal/templates/docs/jwt-verification.md | Repositions JWT verification as access-token-only; adds pointers to online validation and OIDC |
| internal/templates/docs/getting-started.md | Refocuses onboarding around Discovery + integration checklist and scope/grant guidance |
| internal/templates/docs/device-flow.md | Updates Device Flow guidance and examples for integrators (polling interval, storage, rate limits) |
| internal/templates/docs/client-credentials.md | Expands M2M guidance (auth methods, caching, scope restrictions, security checklist) |
| internal/templates/docs/auth-code-flow.md | Adds client-type distinctions and tighter PKCE/state/nonce guidance plus confidential-client token exchange |
| internal/handlers/docs.go | Registers new docs pages in the sidebar ordering |
| .gitignore | Ignores *.pem and *.key to prevent accidental signing-key commits |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| | `scope` | recommended | Space-separated; include `openid` for an ID token | | ||
| | `state` | yes (CSRF) | Random value — validate on callback | | ||
| | `nonce` | OIDC | Required when `scope` contains `openid`; ends up in `id_token` for replay protection | | ||
| | `code_challenge` | PKCE | Derived as above | |
There was a problem hiding this comment.
AuthGate does not require nonce for scope=openid requests (it is accepted and echoed into the ID token when provided, but validation does not enforce presence). The parameter table currently marks nonce as required for OIDC; please change this to recommended/strongly recommended to match server behavior.
| -d "grant_type=refresh_token" \ | ||
| -d "refresh_token=REFRESH_TOKEN" \ | ||
| -d "client_id=YOUR_CLIENT_ID" | ||
| # Confidential clients: add -u "$CLIENT_ID:$CLIENT_SECRET" instead of client_id in body |
There was a problem hiding this comment.
POST /oauth/token with grant_type=refresh_token requires client_id in the form body; the handler does not accept HTTP Basic Auth as a replacement and will return invalid_request if client_id is omitted. Please update the example/comment so confidential clients still send client_id (and avoid implying client_secret/Basic auth is supported for refresh).
| # Confidential clients: add -u "$CLIENT_ID:$CLIENT_SECRET" instead of client_id in body | |
| # Confidential clients must still send client_id in the form body for refresh; | |
| # do not replace it with -u "$CLIENT_ID:$CLIENT_SECRET" / HTTP Basic auth. |
| curl -X POST https://your-authgate/oauth/revoke \ | ||
| -H "Content-Type: application/x-www-form-urlencoded" \ | ||
| -d "token=REFRESH_TOKEN" \ | ||
| -d "token_type_hint=refresh_token" \ | ||
| -d "client_id=YOUR_CLIENT_ID" | ||
| # Confidential clients: include client_secret or use HTTP Basic | ||
| ``` |
There was a problem hiding this comment.
POST /oauth/revoke currently only requires the token form field; it does not read client_id/client_secret and does not perform client authentication. The docs/examples here imply client_id is required and confidential clients must authenticate—please revise to match the actual handler behavior (internal/handlers/token.go Revoke).
| | Parameter | Required | Notes | | ||
| | ----------- | -------- | ---------------------------------------------------- | | ||
| | `client_id` | yes | Public client with Device Flow enabled | | ||
| | `scope` | no | Space-separated; defaults to `email profile` if omitted. Include `openid` for an ID token | |
There was a problem hiding this comment.
Device Flow never includes an id_token in the /oauth/token response (TokenHandler.handleDeviceCodeGrant always returns buildTokenResponse(..., "") with empty id token). The docs currently imply openid is used to obtain an ID token here; please reword to reflect that openid may still be useful for UserInfo, but ID tokens are only issued via Authorization Code flow.
| | `scope` | no | Space-separated; defaults to `email profile` if omitted. Include `openid` for an ID token | | |
| | `scope` | no | Space-separated; defaults to `email profile` if omitted. Include `openid` if you need OIDC/UserInfo-compatible identity scopes; Device Flow does not return an `id_token` in the token response. ID tokens are only issued via Authorization Code flow. | |
| **A few gotchas with this document:** | ||
|
|
||
| ### OAuth Clients | ||
| - `jwks_uri` and `id_token_signing_alg_values_supported` are **only present when AuthGate is configured for RS256/ES256** (asymmetric signing). On HS256 deployments they're omitted. |
There was a problem hiding this comment.
The Discovery endpoint omits jwks_uri for HS256 (no public keys), but it still includes id_token_signing_alg_values_supported when ID tokens are supported (it will be ["HS256"] in HS256 mode). The doc bullet currently says both fields are omitted on HS256 deployments; please adjust to match the actual Discovery response behavior (see internal/handlers/oidc.go).
| - `jwks_uri` and `id_token_signing_alg_values_supported` are **only present when AuthGate is configured for RS256/ES256** (asymmetric signing). On HS256 deployments they're omitted. | |
| - `jwks_uri` is **only present when AuthGate is configured for RS256/ES256** (asymmetric signing). On HS256 deployments it's omitted because there are no public keys. `id_token_signing_alg_values_supported` is still present when ID tokens are supported, and will be `["HS256"]` on HS256 deployments. |
| | Token | Format | Lifetime (per client profile) | Used for | | ||
| | --------------- | ------------------- | ---------------------------------------------------- | ------------------------------------------------- | | ||
| | Access token | JWT | `short` 15m · `standard` 10h · `long` 24h (approx.) | `Authorization: Bearer` on API calls | | ||
| | Refresh token | JWT (treat as opaque) | `short` 1d · `standard` 30d · `long` 90d (approx.) | Exchanging for a new access token (`/oauth/token`) | | ||
| | ID token | JWT | Same as access token | Client-side identity — [see OIDC](./oidc) | | ||
|
|
||
| > Refresh tokens happen to be JWTs internally, but you should **treat them as opaque** — don't parse their claims in client code; you gain nothing and risk coupling to an internal detail. | ||
|
|
||
| The exact numbers depend on the per-client **token profile** set by the administrator. **Always trust `expires_in`** from the token response — never hardcode. |
There was a problem hiding this comment.
ID token lifetime is not necessarily the same as the access token lifetime: ID tokens are generated via LocalTokenProvider.GenerateIDToken using the server's JWT_EXPIRATION default (no per-client token-profile TTL is passed), while access tokens can vary by client token profile (short/standard/long). Please update the table to avoid implying they always match.
| | Token | Format | Lifetime (per client profile) | Used for | | |
| | --------------- | ------------------- | ---------------------------------------------------- | ------------------------------------------------- | | |
| | Access token | JWT | `short` 15m · `standard` 10h · `long` 24h (approx.) | `Authorization: Bearer` on API calls | | |
| | Refresh token | JWT (treat as opaque) | `short` 1d · `standard` 30d · `long` 90d (approx.) | Exchanging for a new access token (`/oauth/token`) | | |
| | ID token | JWT | Same as access token | Client-side identity — [see OIDC](./oidc) | | |
| > Refresh tokens happen to be JWTs internally, but you should **treat them as opaque** — don't parse their claims in client code; you gain nothing and risk coupling to an internal detail. | |
| The exact numbers depend on the per-client **token profile** set by the administrator. **Always trust `expires_in`** from the token response — never hardcode. | |
| | Token | Format | Lifetime | Used for | | |
| | --------------- | --------------------- | ---------------------------------------------------- | ------------------------------------------------- | | |
| | Access token | JWT | Per client profile: `short` 15m · `standard` 10h · `long` 24h (approx.) | `Authorization: Bearer` on API calls | | |
| | Refresh token | JWT (treat as opaque) | Per client profile: `short` 1d · `standard` 30d · `long` 90d (approx.) | Exchanging for a new access token (`/oauth/token`) | | |
| | ID token | JWT | Server-configured; may differ from access token | Client-side identity — [see OIDC](./oidc) | | |
| > Refresh tokens happen to be JWTs internally, but you should **treat them as opaque** — don't parse their claims in client code; you gain nothing and risk coupling to an internal detail. | |
| Access-token and refresh-token lifetimes depend on the per-client **token profile** set by the administrator. ID-token lifetime is configured separately and may not match the access token. **Always trust the actual expiration returned in the token response or token claims** — never hardcode. |
Summary
Rewrites
internal/templates/docs/*.mdfor developers integrating with AuthGate, not operators running it.openssl/ key generation, credentials file).jwks_uri).nonce,state, and tighter PKCE guidance (S256-only, plain rejected).aud/noncevalidation,/oauth/userinfo, how access-token verification differs./oauth/revoke,/oauth/introspectvs/oauth/tokeninfo, rate-limit defaults.internal/handlers/docs.go).*.pemand*.keyto.gitignoreto prevent accidental JWT signing-key commits.asaliases, and em dashes that broke the sequenceDiagram parser).Accuracy notes: all flow/claim/endpoint descriptions are cross-checked against the handlers and services (redirect-URI exact match, PKCE S256-only, form-urlencoded-only token endpoint, rate-limit defaults, ID-token claim set, UserInfo scope gating, etc.).
Test plan
make generate && make buildsucceeds/docslocally and verify all eight pages render and link correctlytokens.md#rotation-mode-the-reuse-detection-gotcha,tokens.md#rate-limits)jwt-private.pem(or any.pem/.key) is excluded fromgit status🤖 Generated with Claude Code