Authorization code flow
Start, sign-in, callback and refresh: the flow step by step, with its rules.
1. Start
Your backend creates a random state and a PKCE code_verifier (43 to 128 characters, RFC 7636), stores both on the server for this browser, and redirects the browser to:
<auth>/oauth/authorize ?response_type=code &client_id=<your client ID> &redirect_uri=<one of your redirect URIs, exactly> &scope=openid &state=<state> &code_challenge=<BASE64URL(SHA256(code_verifier))> &code_challenge_method=S256
nonce is optional and comes back in the ID token. prompt and max_age are not supported.
2. Sign-in
<auth> sends the browser on to the sign-in surface, in the organisation's sign-in branding with your frontend's name. The person enters their email first, then their password, or signs up with a verification code sent to that email. Where the frontend asks for it, they enter their member code and facts to become a member. Then the sign-in surface runs the membership check and approves the request.
A user still signed in on the sign-in surface from another frontend of the organisation, in the same browser, passes without a password (up to 7 days without use, 30 days at most), but the membership check runs again.
3. Callback
The browser comes back to your redirect URI with code and state. Check that state is the one you stored for this browser, then exchange the code from your backend:
POST <auth>/oauth/token Authorization: Basic base64(<client ID>:<client secret>) apikey: <publishable key> Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=<code>&redirect_uri=<the same redirect URI>&code_verifier=<code_verifier>
The answer holds access_token, token_type (bearer), expires_in (3600), refresh_token and id_token.
4. Refresh
Refresh before the access token expires, from your backend:
POST <auth>/oauth/token Authorization: Basic base64(<client ID>:<client secret>) apikey: <publishable key> Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=<refresh token>
Every refresh returns a new refresh token. Store it before you use the new access token, and run one refresh per session at a time: a refresh token already used is accepted again for 10 seconds only, after which using it counts as theft and ends the whole session.
The rules, and the traps behind them
- PKCE
S256only. Discovery also listsplain; never use it. - Scope exactly
openid. The sign-in surface refuses any other scope with an error page, and the token is refused too. A request without a scope is notopenid: the Auth server records it asemail, and it is refused. Do not addoffline_access: you get a refresh token without it. stateis required, although the protocol leaves it optional. Besides protecting your callback, it lets the sign-in surface restart an expired request: an authorization lives 10 minutes, a sign-up with its email code can take longer, and the surface then starts the same request again, so you still get your code with your ownstatefor your owncode_verifier. Keepstateand verifier for at least 30 minutes.- HTTP Basic for the client (
client_secret_basic). The secret in the form body is refused. - Redirect URIs are exact. A request the sign-in surface cannot serve (an unknown or disconnected frontend, an unregistered redirect URI, a wrong scope) ends on a neutral error page there; nothing comes back to your redirect URI.
Lifetimes
| What | Lifetime |
|---|---|
| Authorization and code | 10 minutes |
| Access token | 1 hour (expires_in 3600) |
| Refresh token | Rotates on every refresh; a used one is accepted again for 10 seconds |
| Session | No fixed end; it lasts until it is revoked. Set your own session lifetime if you need one |