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; and
  • nonce: a high-entropy value when requesting openid.

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:

  • iss and sub must equal client_id;
  • aud must equal the exact endpoint URL being called;
  • iat must be current and no more than 120 seconds old;
  • exp must be in the future;
  • jti must 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:

  1. the signature using the key selected by the token header's kid;
  2. the token type, expected audience, and authorized client_id;
  3. exp, nbf, and other applicable time claims;
  4. the granted scopes and principal type; and
  5. 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:

  1. durably store the complete new package;
  2. atomically mark it as the active package; then
  3. 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 state and PKCE verifier for each browser flow, plus a fresh nonce whenever requesting openid.
  • Validate callback state before 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.