Integrator checklist
Work through this list once per application. Every item is something LumoAuth enforces or emits; skipping one is the difference between a spec-compliant client and one that can be phished, replayed, or pointed at the wrong server.
The checklist
| # | Check | Why | Where |
|---|---|---|---|
| 1 | Read discovery (/.well-known/openid-configuration) at startup; never hard-code endpoints or the issuer | Endpoints and the issuer differ between the organization path and a custom domain | OAuth 2.0 API |
| 2 | Validate iss on the authorization response and on every token, byte-for-byte against the discovery issuer; never disable issuer checks in your library | Mix-up attacks (the issuer identifier in the response), token substitution (OIDC Core 3.1.3.7) | Validate the callback, Validate the issuer |
| 3 | Register exact redirect URIs; https for web apps, loopback http or a private-use scheme for native apps, never javascript:/data:/file: | Exact matching is what makes the code unusable by an attacker's site | Redirect URI rules |
| 4 | Use PKCE with S256 on every authorization request | Stops code interception; plain is refused | PKCE |
| 5 | Send and verify state (128 bits of randomness, bound to the browser session) | CSRF on the callback | Authorization endpoint |
| 6 | Send nonce for OIDC and check it in the ID token | ID token replay | Authorization endpoint |
| 7 | Verify token signatures against jwks_uri, check aud, exp, and typ: at+jwt for access tokens | Forged or misdirected tokens | Token formats |
| 8 | Use DPoP for public clients and for anything handling sensitive data; implement the use_dpop_nonce retry | A stolen bearer token is usable from anywhere; a DPoP token is not | Token endpoint |
| 9 | Prefer private_key_jwt or tls_client_auth over shared secrets for confidential clients | Secrets leak through logs and config files; keys do not | FAPI 2.0 |
| 10 | Store the client secret once; it is hashed server-side and never shown again. Rotate instead of "looking it up" | Nothing to recover if the copy is lost | OAuth clients |
| 11 | Handle invalid_client as "unknown or disabled" | Disabled clients are refused on every OAuth endpoint | Inactive clients |
| 12 | Register a back-channel logout URI and validate the logout token like an ID token | Single sign-out that does not depend on the browser | Logout |
| 13 | Use end_session_endpoint for sign-out, not the hosted /logout route | /logout needs a CSRF token only LumoAuth pages have | Signing out of the hosted session |
| 14 | Keep tokens out of localStorage and out of cookies without HttpOnly + SameSite | XSS turns stored tokens into a full account takeover | Token storage |
Recommended Configuration
| Setting | Public clients (SPA / mobile) | Confidential clients (server) |
|---|---|---|
| Grant type | authorization_code | authorization_code, client_credentials |
| PKCE | ✅ Required (S256) | ✅ Recommended |
state | ✅ Required | ✅ Required |
nonce | ✅ Required for OIDC | ✅ Required for OIDC |
| Client auth | none | private_key_jwt or tls_client_auth; client_secret_basic at minimum |
| Token binding | DPoP recommended | DPoP recommended |
| PAR | Recommended | Required for FAPI |
PKCE (Proof Key for Code Exchange): the client generates a random secret (the code verifier) and sends a hash of it when starting the authorization flow, then the real secret when exchanging the code. DPoP (sender-constrained tokens): binds an access token to a client-held key so that a stolen token alone is useless from another host. PAR (pushed authorization requests): the client POSTs the authorization parameters to a back-channel endpoint first, receiving a request_uri that it then uses in the browser redirect.