# 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 `state` value;
- a high-entropy, single-use OpenID Connect `nonce`;
- the `openid profile email` scope 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 `sub` as 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](https://m7.org/docs/api/sso.user.m7.org/response-modes.md), 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:

```text
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](https://user.m7.org/), or use
[`POST /register`](https://m7.org/docs/api/sso.user.m7.org/registration.md) 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:

```text
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:

- `state` binds the browser callback to the relying system's transaction and
  prevents login CSRF and response injection.
- `nonce` binds 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:

```text
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:

1. 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.
2. Read the conventional `keys` array from `/jwks.json` and select the one
   exact member whose `kid` and `alg` match the protected header.
3. Require the algorithm-specific key type and parameters, then validate the
   JWS signature before trusting any payload claim. See [signing profiles](https://m7.org/docs/api/sso.user.m7.org/discovery-and-keys.md#signing-profiles)
   for the available algorithms, advertisement status and SDK compatibility.
4. Validate issuer, audience, authorized party, logical token type, realm,
   principal type, time claims, and the exact transaction nonce.
5. Use the validated `sub` as 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:

```text
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](https://m7.org/docs/api/sso.user.m7.org/userinfo.md#request).

The relying system must:

1. require a successful JSON response;
2. require a nonempty `sub`;
3. compare UserInfo `sub` exactly with the already validated ID-token `sub`;
4. reject a mismatch rather than linking either identity;
5. consume only claims authorized by the granted scopes; and
6. ignore unknown `m7` extension 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](https://m7.org/docs/api/sso.user.m7.org/authorization-callback-outcomes.md)
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_access` without 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.

## Related references

- [Dynamic client registration](https://m7.org/docs/api/sso.user.m7.org/registration.md)
- [Authorization requests and PAR](https://m7.org/docs/api/sso.user.m7.org/authorization-requests.md)
- [Authorization callback outcomes](https://m7.org/docs/api/sso.user.m7.org/authorization-callback-outcomes.md)
- [Token endpoint](https://m7.org/docs/api/sso.user.m7.org/token.md)
- [UserInfo and scopes](https://m7.org/docs/api/sso.user.m7.org/userinfo.md)
- [Discovery and signing keys](https://m7.org/docs/api/sso.user.m7.org/discovery-and-keys.md)
- [Access-token formats](https://m7.org/docs/api/sso.user.m7.org/token-formats.md)
- [Authorization and credential lifecycle](https://m7.org/docs/api/sso.user.m7.org/authorization.md)
