Authorization and credential lifecycle
M7 SSO is the OAuth 2.0 and OpenID Connect authorization server for M7 applications. This guide describes the complete project-specific authorization contract: how clients obtain credentials, how they transmit them, which principal each credential represents, and how credentials expire, rotate, and become invalid.
The production authorization-server issuer and base URL are:
https://sso.user.m7.org
Start with discovery instead of hard-coding endpoint URLs:
GET https://sso.user.m7.org/.well-known/openid-configuration
Use HTTPS for every request. For user sign-in, send the browser to
/authorize; never collect an M7 password in your application or post directly
to an M7 sign-in form-processing route.
Choose an authorization flow
| Integration | Flow | Resulting principal |
|---|---|---|
| Web, browser, SPA, or native application signing in a user | Authorization code with PKCE | Consumer user or tenant member |
| CLI, TV, or input-constrained device | Device authorization | Consumer user or tenant member |
| Server-to-server application acting as itself | Client credentials | OAuth application |
Authorization code with PKCE is the default for user-facing applications. Use
client_credentials only for a confidential application acting as itself; it
does not impersonate a user and does not issue a refresh token.
Principals and authorization boundaries
An access token represents exactly one principal. The token's audience, client, scopes, and current server-side state further limit what it can do.
| Principal | Subject and boundary |
|---|---|
| Consumer user | sub is the M7 user ID. The token acts only as that user and within its granted audience and scopes. |
| Tenant member | sub is the tenant member ID. The token is additionally bound to the OAuth client's tenant. Current application group-admission policy is checked at sign-in and refresh. |
| OAuth application | sub is the public client_id. The token acts only as that application for its allowed audience and scopes. |
The authenticated OAuth client must match the client_id carried by a token.
A tenant browser session cannot be reused for a different tenant. Tenant group
membership and client policy are live authorization inputs, so applications
must not assume that an earlier admission decision lasts indefinitely.
Scopes restrict disclosure and API permissions; they do not replace audience,
tenant, ownership, group, or resource checks. Request only the scopes the
application needs. Standard M7 OpenID Connect scopes are openid, profile,
email, groups, and offline_access; client policy may allow additional
application-specific scopes.
Credential inventory
| Credential | Format and inspection | Acquisition | Transmission and accepted use | Lifetime and terminal behavior |
|---|---|---|---|---|
client_id |
Public identifier; not a secret | Client registration or management | Form/query field where required; Basic username for client_secret_basic |
Valid while the client is active |
| Client secret | High-entropy secret; do not parse | Returned once when a secret-based client is registered or configured | Basic password, form client_secret, or HMAC key for client_secret_jwt, according to registered method |
Replace if disclosed; plaintext is not retrievable later |
| Client assertion | Compact signed JWT constructed by the client | Created for one endpoint request | Form fields client_assertion_type and client_assertion |
Short-lived, unique jti, rejected after expiry, staleness, or replay |
| Authorization code | Opaque, confidential, single-use value | Browser callback from /authorize |
Form field code at /token; never use as an API bearer token |
Approximately 10 minutes; a redemption attempt consumes it |
| PKCE verifier | High-entropy application secret | Created before /authorize |
Form field code_verifier at /token |
Retain only until the code succeeds, expires, or is abandoned |
| Access token | Signed JWT; decoding alone does not validate it | /token |
Authorization: Bearer ACCESS_TOKEN at protected APIs |
Short-lived; expiry, revocation, audience, client, scope, and server-side active state apply |
| Refresh token | Signed JWT but opaque to the client | User-authorized /token result |
Form field refresh_token at /token, with the returned M7 binding values |
Longer-lived; belongs to one client and rotates according to refresh mode |
binding_chain and binding_link |
Opaque M7 refresh-package secrets | Returned beside a refresh token | Form fields with the matching refresh token | Rotate with the refresh package; loss makes the package unusable |
| Fingerprint | 64-character hexadecimal client binding | Generated and retained by an integration that uses fingerprint binding | Request field on issuance and later protected or refresh requests that require it | Must remain available for the bound credential or refresh lineage |
| ID token | Signed OpenID Connect JWT | Returned when openid is granted |
Consumed and validated by the relying party; never use as an API access token | Validate signature, audience, time claims, and nonce before use |
| Device code | Opaque secret | /device_authorization |
Form field device_code while polling /token |
10 minutes; consumed after successful pickup |
| User code | Short human-readable lookup code; not proof by itself | /device_authorization |
Entered only on M7's verification_uri |
Expires with the device authorization |
| ACK activation secret | Opaque one-time bearer credential | Pending ACK-mode refresh response | JSON field activation_secret at /token/ack |
Retain until activation succeeds or reaches a terminal state |
| M7 browser session | M7-managed secure cookie state | M7-hosted sign-in | Sent by the browser only to M7 | Cleared or invalidated by M7 logout/session policy |
Store a refresh token, binding_chain, binding_link, fingerprint when used,
and any pending ACK data as one atomic credential package. Never place access
tokens, refresh tokens, ID tokens, client secrets, client assertions, binding
values, fingerprints, or activation secrets in a URL or query string.
Authorization code with PKCE
1. Create transaction values
For each sign-in attempt, create and retain:
state: a high-entropy, single-use CSRF correlation value;code_verifier: a high-entropy PKCE secret;code_challenge: base64url-encoded SHA-256 of the verifier, without padding; andnonce: a high-entropy value when requestingopenid.
Use S256. Discovery currently also lists plain, but the token exchange
validates the SHA-256 transformation; plain is not a working integration
profile.
2. Navigate the browser to /authorize
GET https://sso.user.m7.org/authorize
| Parameter | Required | Contract |
|---|---|---|
client_id |
Yes | Registered OAuth client identifier |
redirect_uri |
Yes | Exact registered HTTPS callback URI |
response_type |
Yes | code |
state |
Yes | Returned unchanged; validate before exchanging the code |
code_challenge |
Yes for a usable flow | PKCE challenge derived from the retained verifier |
code_challenge_method |
No | Defaults to S256 when a challenge is present; send S256 explicitly |
scope |
No | Space-delimited requested scopes, limited by client policy |
nonce |
Recommended with openid |
Returned in the ID token |
prompt |
No | none, login, or create |
max_age |
No | Non-negative maximum authentication age in seconds |
login_hint |
No | Email address or username hint |
Example browser navigation, shown over several lines for readability:
https://sso.user.m7.org/authorize?
client_id=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
M7 owns the sign-in, account-selection, signup, recovery, and other browser interaction. Use a top-level navigation and do not embed these pages or call their form-processing routes as an API.
On success, M7 redirects to the exact registered URI:
https://app.example.com/oauth/callback?code=AUTHORIZATION_CODE&state=STATE
Validate state before using code. If the callback contains error, handle
that error and do not attempt a code exchange.
M7 uses top-level browser GET callbacks with query parameters. Error
callbacks include a standard OAuth error, normally a stable M7
error_reason, optional safe prose and trace information, and the original
state when it was available. See
Authorization callback outcomes for
the complete outcome matrix, safe-return boundary, and client handling rules.
3. Exchange the code
POST https://sso.user.m7.org/token
Content-Type: application/x-www-form-urlencoded
Authenticate the client using its registered method. A public client using
none sends client_id in the body. Example for client_secret_basic:
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 equal both the registered URI and the URI sealed into the
authorization code. The code is consumed, the PKCE challenge is verified, and
the authenticated client must match the client that received the code.
A typical user-authorized result is:
{
"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"
}
Fields and lifetimes depend on granted scopes and client policy. Treat the actual response as authoritative and persist the complete refresh package before continuing.
Pushed authorization requests
PAR sends authorization parameters directly to M7 before browser navigation:
POST https://sso.user.m7.org/par
Content-Type: application/x-www-form-urlencoded
Authenticate with the client's registered token-endpoint authentication
method. Send the same authorization fields described above. A successful
response contains a short-lived opaque request_uri:
{
"request_uri": "urn:ietf:params:oauth:request_uri:OPAQUE_VALUE",
"expires_in": 600
}
Navigate the browser to:
https://sso.user.m7.org/authorize?client_id=CLIENT_ID&request_uri=REQUEST_URI
Do not add or override stored authorization fields in that browser request. A
supplied client_id must match the pushed request. Start a new PAR transaction
after expiry.
OAuth client authentication
The configured token_endpoint_auth_method controls client authentication at
/token, /par, /device_authorization, and /introspect. The client must
also be enabled for the requested grant type.
| Method | Exact request contract |
|---|---|
none |
Send client_id in the form body. Intended for public clients; rejected for client_credentials and introspection. |
client_secret_basic |
Send Authorization: Basic BASE64(client_id:client_secret). |
client_secret_post |
Send client_id and client_secret in the form body. |
client_secret_jwt |
Send client_id, the JWT bearer assertion type, and an HMAC-signed client_assertion. |
private_key_jwt |
Send the same assertion fields with an assertion signed by a registered private key. |
The assertion type is:
urn:ietf:params:oauth:client-assertion-type:jwt-bearer
For both JWT methods:
issandsubmust equalclient_id;audmust equal the exact endpoint URL being called;iatmust be current and no more than 120 seconds old;expmust be in the future;jtimust be present and unique; and- the registered signing algorithm must match the assertion header.
client_secret_jwt supports HS256, HS384, and HS512 according to client
configuration. private_key_jwt supports RS256, RS384, RS512, and
ES256; its public key comes from one registered inline jwks value or one
registered HTTPS jwks_uri.
For example, an assertion sent to /par uses
https://sso.user.m7.org/par as aud, while an assertion sent to /token
uses https://sso.user.m7.org/token.
Client-authentication transport examples
The authorization-code example above shows client_secret_basic. A public
client configured with none uses the same exchange without --user and adds
its identifier to the body:
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=CLIENT_ID' \
--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'
For client_secret_post, send the identifier and secret in the body:
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=CLIENT_ID' \
--data-urlencode 'client_secret=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'
client_secret_jwt and private_key_jwt use the same HTTP transport; only the
configured assertion signing key and algorithm differ:
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=CLIENT_ID' \
--data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
--data-urlencode 'client_assertion=CLIENT_ASSERTION' \
--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'
Client secrets and private keys belong only in server-side secret storage. Do not embed a confidential credential in browser, SPA, native, CLI, or device application code that an end user can extract.
Access tokens
M7 access tokens are signed JWTs with current server-side session state.
M7 defines separate consumer-user, tenant-member, and OAuth-application claim
profiles. See Access-token formats for their decoded
payload shapes and the current human session to access transition.
Send access tokens to protected APIs using the bearer scheme exactly as shown:
Authorization: Bearer ACCESS_TOKEN
The response currently spells token_type as bearer; HTTP authorization
scheme matching is case-insensitive, but use Bearer in requests.
Resource servers may decode the protected header to select the key and may inspect claims only as part of a complete validation procedure. Decoding a JWT does not validate its signature or active state. At minimum, validate:
- the signature using the key selected by the token header's
kid; - the token type, expected audience, and authorized
client_id; exp,nbf, and other applicable time claims;- the granted scopes and principal type; and
- revocation or active state when the resource's risk model requires an online decision.
M7's /jwks.json response is a per-key lookup instruction document, not an
enumerable RFC 7517 keys array. Use its template with the JWT header's
kid. The authorization-server issuer is https://sso.user.m7.org, while
current OAuth and OpenID Connect tokens carry iss: "id.m7.org" and the key
instruction document identifies https://id.m7.org. Validate the exact token
profile rather than treating these identifiers as interchangeable.
UserInfo bearer example
/userinfo accepts GET or POST and additionally requires the OAuth
client_id that owns the access token:
curl --fail-with-body --silent --show-error \
--header 'Authorization: Bearer ACCESS_TOKEN' \
'https://sso.user.m7.org/userinfo?client_id=CLIENT_ID'
The token must be active, have access-token type, represent a supported user,
tenant-member, or application principal, and belong to CLIENT_ID. A client
mismatch returns insufficient_scope; an invalid, expired, revoked, or
unsupported token returns invalid_token.
If a provisioned token is fingerprint-bound, send its matching fingerprint
request field. Fingerprint binding is an M7 credential control; it does not
replace bearer-token validation or any authorization boundary.
For fingerprint-bound UserInfo requests, use POST so the fingerprint stays
out of the URL:
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/userinfo' \
--header 'Authorization: Bearer ACCESS_TOKEN' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=CLIENT_ID' \
--data-urlencode 'fingerprint=FINGERPRINT'
ID tokens
When openid is granted, /token returns an id_token signed as a JWT. It
asserts the authenticated principal to the relying party identified by aud
and azp; it is not an access token and must not be sent to resource APIs.
Before establishing an application session, validate the signature, key,
audience, authorized party, expiry, and other time claims. If the authorization
request supplied nonce, require an exact match. Use sub as the stable
principal identifier. Do not use email, display name, or preferred username as
a database key.
Refresh packages
Refresh by sending the whole stored package to /token, not only the refresh
token:
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=refresh_token' \
--data-urlencode 'refresh_token=REFRESH_TOKEN' \
--data-urlencode 'binding_chain=BINDING_CHAIN' \
--data-urlencode 'binding_link=BINDING_LINK'
The refresh token must have been issued to the authenticated client_id.
When the lineage is fingerprint-bound, also send its 64-character hexadecimal
fingerprint. new_fingerprint requests a replacement binding; if
fingerprint is present and new_fingerprint is omitted, M7 keeps the current
binding.
M7 normally uses strict_rotation when refresh_mode is omitted. The accepted
M7 lifecycle modes are static, strict_rotation, grace_rotation, ack,
and ack_supersede_pending. Most integrations should omit refresh_mode.
Use an ACK mode only with durable, atomic storage and a tested crash-recovery
procedure.
For a normal rotated response:
- durably store the complete new package;
- atomically mark it as the active package; then
- discard the predecessor only after the commit succeeds.
Do not mix a new refresh token with old binding values. If the response is ambiguous or the request fails before a complete replacement is received, retain the predecessor until the selected lifecycle's documented terminal behavior proves it unusable.
ACK-mode activation
An ACK-mode refresh can return a pending replacement with an activation envelope and predecessor bundle. Persist the entire response before calling:
POST https://sso.user.m7.org/token/ack
Content-Type: application/json
The JSON object must contain only:
{
"activation_id": "ACTIVATION_UUID",
"activation_secret": "ACTIVATION_SECRET"
}
Do not add a bearer access token. The activation secret authorizes an unbound activation. On success M7 returns:
{
"token_state": "active",
"activation_id": "ACTIVATION_UUID",
"idempotent": false
}
Only then promote the pending package and remove the predecessor. Retain all
pending state after a timeout or transport failure because the outcome is
ambiguous. activation_expired, activation_not_current,
activation_parent_not_current, activation_parent_invalid, and
activation_not_pending are terminal lifecycle results.
Device authorization
Start a device flow with:
POST https://sso.user.m7.org/device_authorization
Content-Type: application/x-www-form-urlencoded
Authenticate using the client's registered method. The client must be active
and enabled for the device grant. A public client may use none and send
client_id in the body.
Common request fields are scope, aud, nonce, fingerprint,
access_expires, refresh_expires, claims, access_claims, and
refresh_claims. Requested audiences, scopes, claims, and lifetimes remain
subject to client policy.
Example:
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/device_authorization' \
--user 'CLIENT_ID:CLIENT_SECRET' \
--data-urlencode 'scope=openid profile email offline_access' \
--data-urlencode 'aud=https://api.example.com'
A successful result is valid for 10 minutes:
{
"device_code": "DEVICE_CODE",
"user_code": "ABCD-EFGH",
"verification_uri": "https://sso.user.m7.org/device_login",
"verification_uri_complete": "https://sso.user.m7.org/device_login?user_code=ABCD-EFGH",
"expires_in": 600,
"interval": 5
}
Keep device_code secret. Show the user verification_uri_complete and the
human user_code; never ask the user to type or view device_code. The user
signs in and approves only on M7's browser page.
Poll /token no more frequently than interval:
curl --silent --show-error \
--request POST 'https://sso.user.m7.org/token' \
--user 'CLIENT_ID:CLIENT_SECRET' \
--data-urlencode 'grant_type=device_code' \
--data-urlencode 'device_code=DEVICE_CODE'
The standard grant identifier
urn:ietf:params:oauth:grant-type:device_code is also accepted. Before
approval, /token returns authorization_pending. Stop polling after success,
expiry, invalid_grant, or another terminal error. Successful pickup consumes
the device code and returns the approved token package.
Client credentials
The client must be confidential and enabled for client_credentials. The
required aud selects one allowed resource. scope, access_expires,
claims, fingerprint, and a management label are optional and remain
subject to policy.
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=client_credentials' \
--data-urlencode 'aud=https://api.example.com' \
--data-urlencode 'scope=resource.read' \
--data-urlencode 'access_expires=900'
The result contains an application-principal access token and no refresh token. Obtain another token by repeating the authenticated grant when the access token expires.
Dynamic client registration
/register creates an active OAuth client owned by the user represented by an
access token:
POST https://sso.user.m7.org/register
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
The access token must be active and authorized for the registration operation. A minimal public-client request is:
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/register' \
--header 'Authorization: Bearer ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"client_name": "Example App",
"redirect_uris": ["https://app.example.com/oauth/callback"],
"token_endpoint_auth_method": "none"
}'
Redirect URIs are exact HTTPS values. A secret-based registration returns the
plaintext client_secret in the successful registration response; store it
immediately because it is not later retrievable in plaintext. A
private_key_jwt registration supplies either inline jwks or one HTTPS
jwks_uri, never both.
Introspection, revocation, and logout
Introspection
POST https://sso.user.m7.org/introspect
Content-Type: application/x-www-form-urlencoded
Introspection requires a confidential client authenticated with its configured
method. Send token and optional token_type_hint. The authenticated client
may inspect only a token issued to that client.
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/introspect' \
--user 'CLIENT_ID:CLIENT_SECRET' \
--data-urlencode 'token=ACCESS_TOKEN' \
--data-urlencode 'token_type_hint=access_token'
Treat active: false as unusable even when the token still decodes or its
cryptographic signature remains valid.
Revocation
POST https://sso.user.m7.org/revoke
Content-Type: application/x-www-form-urlencoded
Revocation supports confidential client authentication with
client_secret_basic or client_secret_post. Send token and optional
token_type_hint; refresh_token is accepted as a token-field alias. The
authenticated client may revoke only its own token.
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/revoke' \
--user 'CLIENT_ID:CLIENT_SECRET' \
--data-urlencode 'token=REFRESH_TOKEN' \
--data-urlencode 'token_type_hint=refresh_token'
After successful revocation, discard the affected local credential package.
Browser end session
The browser endpoint is https://sso.user.m7.org/end-session and accepts
GET or POST. It recognizes id_token_hint,
post_logout_redirect_uri, state, client_id, logout_hint, and
ui_locales.
Use id_token_hint to establish client context, but keep the ID token out of a
URL by submitting a top-level form with method="post". A
post_logout_redirect_uri must exactly match a registered HTTPS logout URI;
without a valid ID-token hint, client_id is also required. Use a simple GET
navigation only for generic logout or a client-scoped request that does not
carry an ID token.
M7 validates the client and return URI before exposing a client-scoped redirect. Clear the application's own session as well; M7 cannot clear cookies on the application's domain.
DPoP status
Bearer authorization is the supported public contract. M7 currently accepts and stores DPoP-style proofs on parts of the user-token and refresh path, and ACK can require a matching proof for an already bound lineage. However, the ordinary public path does not yet provide the complete method binding, mandatory proof identifier, and replay enforcement needed to promise an end-to-end RFC 9449 profile.
Do not opt a new integration into DPoP based only on the presence of a DPoP
header, token confirmation data, or browser proof code. Discovery does not
advertise a supported DPoP signing-algorithm profile. Continue to send access
tokens with Authorization: Bearer and rely on TLS, short lifetimes, audience,
client binding, scopes, revocation, and the M7 refresh-package controls.
An existing deployment with a DPoP-bound lineage must preserve its private key and follow its provisioned compatibility profile until it can be migrated or reauthorized. Losing that key is terminal for the bound lineage. This compatibility behavior is not a general integration contract.
Error handling
JSON failures use OAuth-style error and error_description fields. Branch
on error, not on prose in error_description.
| Error | Integration action |
|---|---|
invalid_request |
Correct missing, malformed, conflicting, or unsupported request data. |
invalid_client |
Check the configured client authentication method and credential; do not retry an unchanged secret or assertion. |
unauthorized_client |
Use only a grant or endpoint enabled for this client. |
unsupported_grant_type |
Correct grant_type. |
invalid_grant |
Treat the code, device code, refresh token, or related package as invalid for this transaction; restart or reauthorize as appropriate. |
invalid_token |
Reject the access token and obtain a valid credential. |
insufficient_scope |
Do not retry as-is; correct the client, audience, scopes, tenant, ownership, or policy boundary. |
authorization_pending |
Device flow only: continue polling at or below the supplied interval. |
access_denied |
The principal is not admitted to the requested application or declined the request. |
server_error |
Retry only when safe for the flow; preserve refresh or ACK recovery state across ambiguous failures. |
HTTP 401 is used for invalid bearer credentials, HTTP 403 for authenticated credentials that cross a client or authorization boundary, and HTTP 400 for most malformed OAuth requests. Some transient service failures use HTTP 5xx.
Security checklist
- Register exact HTTPS redirect and post-logout URIs.
- Generate a fresh
stateand PKCE verifier for each browser flow, plus a freshnoncewhenever requestingopenid. - Validate callback
statebefore exchanging the authorization code. - Keep confidential-client authentication and code exchange off the browser.
- Validate signed tokens before trusting claims; decoding is not validation.
- Enforce audience, client, principal, scope, tenant, ownership, group, and resource boundaries at every protected service.
- Store refresh packages atomically and retain predecessor data until the selected lifecycle has committed.
- Revoke tokens and rotate client secrets after suspected disclosure.
- Never log tokens, codes, client secrets, assertions, binding values, fingerprints, device codes, activation secrets, or M7 browser cookies.
- Use safe placeholders in diagnostics and support material.