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 requesting openid; 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:

  1. Verify the returned state is exactly the one associated with this browser session.
  2. Require exactly one terminal shape: code or error, never both or neither.
  3. If the callback contains error, do not exchange a code. Use error and error_reason for program behavior; do not parse error_description.
  4. Treat unknown error or reason values as a terminal authorization failure, never as success.
  5. 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 state for every browser authorization.
  • Use PKCE S256 and 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.