Quickstart: authorization code with PKCE
Use authorization code flow with PKCE for every user-facing application:
server-rendered web apps, browser-based backends, native apps, and SPAs.
M7 requires PKCE for this flow. Use S256; do not use plain in new clients.
Before starting, register an OAuth client with an exact HTTPS redirect URI. You can use the registration endpoint or manage the client through the M7 User API.
1. Create state and PKCE values
For each sign-in attempt, generate:
state: a high-entropy, single-use value stored with the user's browser session. M7 requires it on authorization requests and returns it unchanged.code_verifier: a high-entropy secret retained only by your application.code_challenge: base64url-encoded SHA-256 of the verifier, without padding.nonce: recommended when requestingopenid; validate the returned value in the ID token.
Example shell commands for illustration only:
CODE_VERIFIER="$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')"
CODE_CHALLENGE="$(printf %s "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')"
STATE="$(openssl rand -hex 32)"
NONCE="$(openssl rand -hex 32)"
Keep the verifier and state server-side or in a platform-protected native-app store. They are not values to expose in URLs, logs, analytics, or referrer headers.
2. Redirect the browser to M7
Build a GET request to https://sso.user.m7.org/authorize:
https://sso.user.m7.org/authorize?
client_id=YOUR_CLIENT_ID&
redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback&
response_type=code&
scope=openid%20profile%20email&
state=STATE&
nonce=NONCE&
code_challenge=CODE_CHALLENGE&
code_challenge_method=S256
Use a top-level browser navigation. M7 handles sign-in, account selection, and any required user interaction. Do not render M7 pages in an iframe.
See Authorization requests for the complete parameter reference and PAR if you do not want authorization parameters in the browser URL.
3. Validate the callback
After approval, M7 redirects to the exact registered redirect_uri with query
parameters like:
https://app.example.com/oauth/callback?code=AUTHORIZATION_CODE&state=STATE
Before using the code:
- Verify the returned
stateis exactly the one associated with this browser session. - Require exactly one terminal shape:
codeorerror, never both or neither. - If the callback contains
error, do not exchange a code. Useerroranderror_reasonfor program behavior; do not parseerror_description. - Treat unknown error or reason values as a terminal authorization failure, never as success.
- Do not expose the code, state, or raw callback URL to application logs, analytics, screenshots, or telemetry.
Authorization codes are short-lived and single-use.
Read Authorization callback outcomes for the complete success and error contract, including cancellation, interaction-required, policy, configuration, protocol, and retryable server outcomes.
A user cancellation can return:
https://app.example.com/oauth/callback?error=access_denied&error_reason=login_cancelled&error_description=login_cancelled&state=STATE
A non-interactive request that needs sign-in can return:
https://app.example.com/oauth/callback?error=login_required&error_reason=session_required&state=STATE
Validate state before classifying either result. Start a fresh interactive
authorization request after login_required; do not call /token for either
error.
4. Exchange the code at /token
Make a server-to-server POST request. A public client sends its client_id;
a confidential client authenticates using its registered method.
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--user "CLIENT_ID:CLIENT_SECRET" \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=AUTHORIZATION_CODE' \
--data-urlencode 'redirect_uri=https://app.example.com/oauth/callback' \
--data-urlencode 'code_verifier=CODE_VERIFIER'
redirect_uri must exactly match both the registered URI and the URI used in
the authorization request. For the full client-authentication and response
contract, see Token endpoint.
A successful package includes an access token, expiry, granted scope, and normally the M7 refresh material:
{
"token_type": "bearer",
"access_token": "ACCESS_TOKEN",
"expires_in": 900,
"refresh_token": "REFRESH_TOKEN",
"refresh_expires_in": 2592000,
"binding_chain": "BINDING_CHAIN",
"binding_link": "BINDING_LINK",
"scope": "openid profile email",
"id_token": "ID_TOKEN"
}
id_token is returned when the granted scope includes openid. Actual
lifetimes and optional fields are controlled by the client and service policy.
Persist the complete refresh package atomically and protect every credential
field, not only refresh_token.
5. Retrieve claims and use the access token
Call /userinfo with the access token and the same OAuth client_id:
curl --fail-with-body --silent --show-error \
--header 'Authorization: Bearer ACCESS_TOKEN' \
'https://sso.user.m7.org/userinfo?client_id=YOUR_CLIENT_ID'
See UserInfo and scopes for returned claims. Verify an ID token before trusting it; see Discovery and signing keys.
Production checklist
- Use an exact HTTPS redirect URI; no wildcards or partial matching.
- Generate and validate
statefor every browser authorization. - Use PKCE
S256and retain the verifier until the exchange succeeds or expires. - Keep code exchange and client secrets off the browser for confidential clients.
- Persist a replacement refresh package before discarding the prior one.
- Send access tokens with
Authorization: Bearer; review the complete authorization and credential lifecycle before storing or refreshing credentials.