M7 consumer identity federation protocol reference
This reference defines the protocol contract for using an M7 consumer account as an OpenID Connect identity provider in another identity system. It is for protocol-level federation, identity brokers, and custom integrations.
For ordinary application sign-in, use the standard M7 SDK. The SDK owns the normal application session and callback integration. Do not implement this federation profile merely to replace the standard SDK.
Profile summary
The M7 consumer-federation profile uses:
- OpenID Connect authorization code flow;
- top-level browser redirects and query response mode for the integration in this guide;
- mandatory PKCE using
S256; - a high-entropy, single-use
statevalue; - a high-entropy, single-use OpenID Connect
nonce; - the
openid profile emailscope set for the current federation adapter; - an ID token as the signed identity assertion;
- UserInfo as the current scoped profile projection; and
- the ID-token
subas the stable external identity key.
The relying system must complete ID-token and UserInfo validation before it creates, links, or signs in a local account. It must never use email, username, or another presentation claim as the federation key.
Discovery and endpoints
This reference uses the query integration throughout. Other clients may select the additional response modes, subject to their registration, callback-validation and browser requirements. The iframe password-login tests do not establish iframe federation or consent coverage.
Retrieve current metadata before configuring an integration:
GET https://sso.user.m7.org/.well-known/openid-configuration
The current production endpoints are:
| Purpose | Endpoint |
|---|---|
| Authorization-server issuer | https://sso.user.m7.org |
| Authorization | GET https://sso.user.m7.org/authorize |
| Pushed authorization request (optional) | POST https://sso.user.m7.org/par |
| Token | POST https://sso.user.m7.org/token |
| UserInfo | GET or POST https://sso.user.m7.org/userinfo |
| Dynamic client registration | POST https://sso.user.m7.org/register |
| Signing-key set | GET https://sso.user.m7.org/jwks.json |
Do not apply one issuer rule to every token class. Discovery returns the
canonical authorization-server issuer https://sso.user.m7.org. During the
current ID-token migration, validate an exact issuer allowlist containing
https://sso.user.m7.org and legacy id.m7.org; current access tokens retain
iss: "id.m7.org".
Client registration
Use a dedicated OAuth client for each federation integration. Do not reuse a client that already serves an unrelated application sign-in path. Independent registration permits safe redirect, secret, policy, and lifecycle changes without breaking the other application.
Create and manage the client in the
M7 account application, or use
POST /register when an authorized provisioning tool
needs dynamic registration. Dynamic registration requires an active M7 user
bearer token; it is not an anonymous client-enrollment endpoint.
The server-side federation profile uses this registration contract:
| Metadata | Value or rule |
|---|---|
application_type |
web |
client_name |
A user-facing name that identifies the relying system |
redirect_uris |
One or more exact HTTPS callback URIs; no wildcards |
response_types |
code |
grant_types |
authorization_code |
scope |
Permit at least openid profile email for the current profile |
token_endpoint_auth_method |
client_secret_basic for the recommended confidential-server profile |
initiate_login_uri |
Optional trusted HTTPS login-start URI used for PAR re-entry |
M7 returns the client_id and, for a secret-based method, the plaintext
client_secret once. The identifier is public. The secret is a credential:
store it in a server-side secret manager and never send it to a browser,
mobile client, front-channel authorization URL, log, or source repository.
Public clients can register token_endpoint_auth_method=none and still must
use PKCE. A public client has no secret and sends its client_id in the token
request. A confidential identity broker should use a server-side method,
normally client_secret_basic or private_key_jwt.
At present, M7's every-request consumer-federation consent policy is enabled for clients specifically designated for that profile. Registering a generic OAuth client does not by itself designate it as the consumer-federation client. Confirm that profile assignment as part of federation onboarding.
Client authentication
The token and PAR endpoints authenticate the client with its registered
token_endpoint_auth_method. A request must not silently fall back to a
different method.
| Method | Request contract |
|---|---|
none |
Send client_id in the form body. For public clients only. |
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. |
For JWT client assertions, iss and sub must equal the client_id; aud
must equal the canonical authorization-server issuer or the exact endpoint
receiving the assertion; and iat, exp, and a unique jti are required.
An assertion sent to /par may therefore use https://sso.user.m7.org or the
exact PAR endpoint as aud; the same rule applies at /token.
Authorization-code flow
Transaction material
Before redirecting the browser, the relying system must generate and retain one transaction record containing:
| Value | Requirement |
|---|---|
state |
High-entropy, unpredictable, bound to the initiating browser session, expiring, and single-use |
nonce |
High-entropy, unpredictable, expiring, single-use, and later matched exactly against the ID token |
code_verifier |
43–128 RFC 3986 unreserved characters, retained only by the relying system |
code_challenge |
Base64url-encoded SHA-256 of the exact verifier, without padding |
redirect_uri |
Exact registered callback URI used for both authorization and token exchange |
| requested scopes | The exact scope set expected by the callback and claim validator |
Do not store the client secret, authorization code, or token package in the browser transaction. Do not place the verifier or nonce in logs or analytics.
Authorization request
Send a top-level browser GET to /authorize with:
| Parameter | Requirement |
|---|---|
client_id |
Registered federation client identifier |
redirect_uri |
Exact registered HTTPS callback URI |
response_type |
code |
scope |
openid profile email for the current federation profile |
state |
The transaction's exact opaque state |
nonce |
The transaction's exact OpenID Connect nonce |
code_challenge |
Challenge derived from the retained verifier |
code_challenge_method |
Exact, case-sensitive value S256 |
Do not put a client secret in this request. Do not embed the M7 authorization page in an iframe. M7 owns sign-in, account selection, and consent interaction.
An integration may instead authenticate to /par, push the same request
parameters, and, after the HTTP 201 Created response, immediately navigate
the browser to /authorize with the returned request_uri. A PAR URI is
opaque, short-lived, and single-use. Do not add or override authorization
fields in the browser request. Start a new PAR transaction after expiry.
Callback parameters
M7 returns to the exact registered callback using a top-level browser GET
with query parameters. Require exactly one terminal shape.
Success:
| Parameter | Meaning |
|---|---|
code |
Confidential, short-lived, single-use authorization code |
state |
Original opaque state, returned unchanged |
Error:
| Parameter | Meaning |
|---|---|
error |
Stable OAuth error category |
error_reason |
Stable M7 machine-readable reason when supplied |
error_description |
Optional safe, non-normative prose; never branch on it |
trace_id |
Optional opaque support-correlation identifier |
state |
Original state when M7 has a trusted callback transaction |
Before interpreting either shape, compare state with the stored transaction
using an exact, timing-safe comparison and atomically consume the transaction.
Reject missing, mismatched, array-shaped, duplicate, or conflicting protected
fields. Never exchange a code after a callback error or a failed state check.
An expired PAR request is not a callback result: the expired record can no
longer supply trusted callback state. Begin a new authorization request from
the registered initiate_login_uri or the M7 re-entry page.
Token exchange
Exchange the code with a server-to-server form request:
POST https://sso.user.m7.org/token
Content-Type: application/x-www-form-urlencoded
| Form field | Requirement |
|---|---|
grant_type |
authorization_code |
code |
Exact code received at the callback |
redirect_uri |
Exact URI used in the authorization request |
code_verifier |
Original, unmodified PKCE verifier |
client_id |
Required in the body for none; may accompany other methods when permitted |
Authenticate using the client's registered method. The code, redirect URI, client, and verifier are one binding; any mismatch must fail closed.
A successful response for openid contains an access_token, id_token,
token_type, granted scope, and expiry data. token_type=Bearer identifies
an unbound access token; token_type=DPoP identifies a token bound through
cnf.jkt. It can also contain refresh or
binding material under client and service policy. A federation proof normally
does not request offline access: validate the ID token, retrieve the current
UserInfo projection, establish the local session, and discard provider tokens
that the integration does not need.
State and nonce requirements
state and nonce have different security purposes and neither replaces the
other:
statebinds the browser callback to the relying system's transaction and prevents login CSRF and response injection.noncebinds the ID token to that same authorization attempt and prevents reuse of an identity assertion in another browser transaction.
Both values must be generated with a cryptographically secure random source, stored under an expiry, accepted once, and compared exactly. Do not derive one from the other. Delete the transaction after success, terminal error, or expiry.
Scopes and returned identity data
The current M7 consumer-federation adapter requests exactly:
openid profile email
| Scope | Federation use |
|---|---|
openid |
Enables ID-token issuance. Required for this profile. |
profile |
Permits standard profile and M7 consumer profile projection through UserInfo. |
email |
Permits the active primary email and email_verified when available. |
groups |
Tenant-member group data; it does not expose consumer-account groups and is not part of this profile. |
offline_access |
Requests durable refresh behavior; normally unnecessary for a one-time federation proof. |
Request only data the relying system needs. An allowed scope is not proof that
every corresponding claim exists. Email and profile fields remain optional;
sub is the only stable federation identity key.
Consent behavior
For a client designated for M7 consumer federation, every new interactive authorization request shows the M7 consent screen, including when the user already has an eligible M7 session. M7 does not currently persist a Connected Apps grant, so a prior approval never suppresses the next screen.
The approval is short-lived, single-use, and bound to the exact client,
redirect URI, scopes, state, PKCE challenge, and nonce. It cannot authorize a
different request. prompt=none cannot complete this interaction and returns
consent_required; begin a fresh interactive request if the user can approve.
This is a temporary federation policy until Connected Apps grant storage is available. Integrations must not infer permanent approval or attempt to cache, replay, or bypass M7's consent decision.
ID-token claims and validation
An M7 ID token is a signed identity assertion for the relying client. It is not an API access token. Decode it only as part of a complete verification procedure.
The current consumer identity profile requires:
| Claim | Validation |
|---|---|
iss |
Exact current allowlist: https://sso.user.m7.org or legacy id.m7.org; reject all other values |
aud |
Contains the relying OAuth client_id |
azp |
When present, or when aud has multiple values, exactly equals the client_id |
sub |
Stable dashed UUID for the M7 consumer account |
nonce |
Exact value stored for the authorization transaction |
typ |
Exact logical token type id |
rlm |
Exact realm consumer |
pty |
Exact principal type user |
iat |
Valid issued-at time; reject an implausible future value |
nbf |
Not later than the current time plus the validator's small allowed skew |
exp |
Later than the current time |
preferred_username or name |
Optional presentation value; never an identity key |
Validation order:
- Parse the protected header and require the client's configured ID-token
algorithm, also present in live discovery and the verifier's allowlist,
plus a usable
kid. Do not accept an algorithm supplied only by the token. - Read the conventional
keysarray from/jwks.jsonand select the one exact member whosekidandalgmatch the protected header. - Require the algorithm-specific key type and parameters, then validate the JWS signature before trusting any payload claim. See signing profiles for the available algorithms, advertisement status and SDK compatibility.
- Validate issuer, audience, authorized party, logical token type, realm, principal type, time claims, and the exact transaction nonce.
- Use the validated
subas the external account identifier.
Cache public keys by kid according to their HTTP caching metadata and refresh
the JWKS once for an unknown kid before failing. Do not pin one signing key indefinitely. Do
not accept an access token, tenant token, refresh token, or decoded but
unverified JWT as an M7 consumer identity assertion.
UserInfo request and validation
After validating the ID token, call UserInfo with the access token. M7 derives the owning OAuth client from the validated token:
GET https://sso.user.m7.org/userinfo
Authorization: Bearer ACCESS_TOKEN
An optional client_id is accepted only as a legacy exact-match assertion.
POST is also supported; use it when an M7 fingerprint binding is required so
the fingerprint does not enter a URL. The example is for token_type=Bearer.
For token_type=DPoP, use Authorization: DPoP and one fresh ath-bound
proof for UserInfo. M7 creates any required internal downstream attestations;
clients do not send them. See the UserInfo contract.
The relying system must:
- require a successful JSON response;
- require a nonempty
sub; - compare UserInfo
subexactly with the already validated ID-tokensub; - reject a mismatch rather than linking either identity;
- consume only claims authorized by the granted scopes; and
- ignore unknown
m7extension fields unless its own contract defines them.
Use preferred_username, name, picture, locale, email, and
email_verified only as scoped profile attributes. Email can be absent and can
change. A relying system that needs a verified email must require
email_verified=true and still key the account by sub.
An invalid, expired, revoked, incorrectly bound, or unsupported token returns
HTTP 401 with invalid_token. A valid token belonging to a different
client_id returns HTTP 403 with insufficient_scope.
Identity linking boundary
Store the federation link as at least the provider identity plus the validated
M7 consumer sub. Keep the relying tenant or realm in the local uniqueness
boundary when one consumer is allowed to join more than one independent
tenant. Do not automatically merge accounts based on matching email,
username, display name, or avatar.
Require an authenticated, explicit linking ceremony before attaching a new M7 identity to an existing local account. Protect link, unlink, and recovery operations against account takeover independently of the ordinary login flow.
Errors
Authorization callback errors and token endpoint errors are different transactions and must remain separate.
| Surface | Representative errors | Required handling |
|---|---|---|
| Authorization callback | access_denied, login_required, consent_required, interaction_required, temporarily_unavailable, server_error |
Validate state, do not call /token, and follow the stable error and error_reason contract. |
| Local M7 authorization page | Invalid client, redirect, PAR, or untrusted request state | Do not invent a callback from untrusted input; start a new request after correcting configuration or expiry. |
| Token endpoint | invalid_request, invalid_client, invalid_grant, unauthorized_client, invalid_scope, unsupported_grant_type, invalid_dpop_proof, server_error |
Do not establish a session; classify by error, not prose, and restart authorization when the code or transaction is no longer usable. |
| UserInfo | invalid_token, insufficient_scope, server_error |
Reject the identity proof; do not fall back to unvalidated ID-token presentation claims. |
Treat unknown error and reason values as observable terminal failure, never as
success. error_description is safe human-readable context and is not a stable
programming interface. Retain an opaque trace_id for support correlation,
but never include credentials, raw callback URLs, token contents, or internal
provider diagnostics in a support record.
See Authorization callback outcomes for the complete callback error/reason matrix, retryability, and PAR re-entry contract.
Security requirements
- Use HTTPS for every registered URI and protocol endpoint.
- Use a dedicated client and exact redirect-URI matching; do not use wildcards.
- Keep confidential-client authentication and token exchange on a trusted server.
- Generate fresh state, nonce, PKCE verifier, and PKCE challenge for every authorization attempt.
- Bind and expire the complete transaction, then consume it atomically.
- Validate callback state before success or error handling.
- Validate the ID-token signature and all required consumer-profile claims.
- Require ID-token and UserInfo subject equality before account mapping.
- Use
sub, never email or username, as the durable external identity. - Apply least-privilege scopes and do not request
offline_accesswithout a durable refresh-token use case. - Never log authorization codes, state, nonce, PKCE material, client secrets, access tokens, ID tokens, refresh material, DPoP proofs, or raw callback URLs.
- Rate-limit login starts and callbacks, reject replay, and make local session creation idempotent.
- Rotate a disclosed client secret and invalidate affected local transactions.
- Keep provider authentication separate from local authorization: a valid M7 identity does not by itself grant tenant membership, roles, or resources.