Skip to main content

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​

#CheckWhyWhere
1Read discovery (/.well-known/openid-configuration) at startup; never hard-code endpoints or the issuerEndpoints and the issuer differ between the organization path and a custom domainOAuth 2.0 API
2Validate iss on the authorization response and on every token, byte-for-byte against the discovery issuer; never disable issuer checks in your libraryMix-up attacks (the issuer identifier in the response), token substitution (OIDC Core 3.1.3.7)Validate the callback, Validate the issuer
3Register 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 siteRedirect URI rules
4Use PKCE with S256 on every authorization requestStops code interception; plain is refusedPKCE
5Send and verify state (128 bits of randomness, bound to the browser session)CSRF on the callbackAuthorization endpoint
6Send nonce for OIDC and check it in the ID tokenID token replayAuthorization endpoint
7Verify token signatures against jwks_uri, check aud, exp, and typ: at+jwt for access tokensForged or misdirected tokensToken formats
8Use DPoP for public clients and for anything handling sensitive data; implement the use_dpop_nonce retryA stolen bearer token is usable from anywhere; a DPoP token is notToken endpoint
9Prefer private_key_jwt or tls_client_auth over shared secrets for confidential clientsSecrets leak through logs and config files; keys do notFAPI 2.0
10Store 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 lostOAuth clients
11Handle invalid_client as "unknown or disabled"Disabled clients are refused on every OAuth endpointInactive clients
12Register a back-channel logout URI and validate the logout token like an ID tokenSingle sign-out that does not depend on the browserLogout
13Use end_session_endpoint for sign-out, not the hosted /logout route/logout needs a CSRF token only LumoAuth pages haveSigning out of the hosted session
14Keep tokens out of localStorage and out of cookies without HttpOnly + SameSiteXSS turns stored tokens into a full account takeoverToken storage
SettingPublic clients (SPA / mobile)Confidential clients (server)
Grant typeauthorization_codeauthorization_code, client_credentials
PKCE✅ Required (S256)✅ Recommended
state✅ Required✅ Required
nonce✅ Required for OIDC✅ Required for OIDC
Client authnoneprivate_key_jwt or tls_client_auth; client_secret_basic at minimum
Token bindingDPoP recommendedDPoP recommended
PARRecommendedRequired 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.

Issuer validation​

The one string to compare against is the discovery issuer of the entry point your application uses:

Entry pointissuer
Organization pathhttps://app.lumoauth.dev/orgs/acme-corp/api/v1
Verified custom domainhttps://auth.acme.com

That value appears as the iss parameter on authorization responses (success and error), the iss claim of access, ID and logout tokens, and the iss field of introspection responses. The pre-September-2026 form without /api/v1 is still accepted by LumoAuth's own verifiers while old tokens expire, but is no longer minted.

Standard

The issuer identifier in the response and OpenID Connect Core 3.1.3.7: compare iss with the discovery issuer before trusting a code or a token. Most libraries do this when you pass issuer; the mistake is passing the wrong string or turning the check off to "make it work".

Redirect URI Validation​

LumoAuth performs exact-match validation on redirect_uri:

  • Wildcards, prefix matches and variable loopback ports are not permitted; register every URI you use.
  • Web clients must use https. Native clients (application type Native) may use loopback http (127.0.0.1, localhost, [::1]) or a private-use scheme such as com.example.app:/callback (as the native-apps best practice recommends).
  • javascript:, data:, vbscript:, blob:, file: and about: URIs are refused at registration time in the portal, the admin API and dynamic client registration. Presentation URIs (client_uri, logo_uri, policy_uri, tos_uri) must be http(s).
  • response_mode=form_post is only delivered to http(s) redirect URIs.

Token Storage​

TokenRecommended storage
Access token (SPA)In-memory only
Refresh token (SPA)Avoid; use silent auth, or a BFF that holds the refresh token server-side
Access token (mobile)Secure enclave / Keychain
Refresh token (mobile)Secure enclave / Keychain
Access token (server)In-memory / short-lived cache
Refresh token (server)Encrypted database
danger

Never store tokens in localStorage or in cookies without HttpOnly + SameSite=Strict flags.

Clients that don't need refresh tokens should leave refresh_token out of their grant types: LumoAuth then issues none. A refresh-token chain has an absolute lifetime (90 days, or the client's refresh-token lifetime if longer), so plan for a periodic interactive sign-in. See Refresh tokens.

Revocation and offline validation​

Blocking, deactivating or deleting a user, signing them out everywhere, and a password change or reset invalidate their existing tokens immediately at LumoAuth (introspection, UserInfo, LumoAuth APIs, refresh). A resource server that only verifies JWT signatures offline keeps accepting such a token until exp. Keep access-token lifetimes short, or introspect for sensitive operations.

Claims​

Request only the scopes you need: user claims in access tokens follow the granted OIDC scopes (email, profile, phone, address), and roles must be enabled per client. Don't rely on email or roles being present in an access token unless you requested the scope or enabled the claim. See Claims and roles.

CSRF Protection​

Always include and verify the state parameter. The state value must:

  1. Be cryptographically random (≥ 128 bits of entropy).
  2. Be bound to the user's session on your side.
  3. Be verified before processing the authorization response.

Standards used on this page​

Plain nameStandard
Discovery documentRFC 8414 and OpenID Connect Discovery
Issuer identifier in the responseRFC 9207
OpenID Connect (OIDC)OpenID Connect Core 1.0
Proof Key for Code Exchange (PKCE)RFC 7636
JWT access token profileRFC 9068
Sender-constrained tokens (DPoP)RFC 9449
JWT client authentication and the JWT bearer grantRFC 7523
Mutual TLS client authenticationRFC 8705
LogoutOpenID Connect Back-Channel Logout and RP-Initiated Logout
Pushed authorization requests (PAR)RFC 9126
OAuth for native appsRFC 8252
Token introspectionRFC 7662

See the standards reference for what each one covers and how LumoAuth uses it.