# Tutorial: integrate M7 consumer identity as a federated provider

This tutorial walks a third-party identity broker or tenant identity system
from client registration through production verification. The result accepts
an M7 consumer account as an external OpenID Connect identity, validates it,
and maps it to a local account.

**This is not the normal way to add M7 sign-in to an application.** For
ordinary application login, use the standard M7 SDK. Implement the direct OIDC
flow below only when your system acts as an identity broker, federation layer,
or custom relying party that must validate and map M7 consumer identity itself.

Keep the [consumer-federation protocol reference](https://m7.org/docs/api/sso.user.m7.org/consumer-federation-reference.md)
open while implementing. It is the normative field and validation reference;
this page concentrates on integration order and verification.

The tutorial uses top-level navigation and query callbacks throughout. For a
different integration, follow the [response-mode guide](https://m7.org/docs/api/sso.user.m7.org/response-modes.md);
do not change delivery without also implementing its validation and browser
requirements. Iframe federation and consent were not part of the live iframe
password-sign-in checks.

## 1. Confirm that direct federation is the right integration

Choose the integration before creating a client:

| Requirement | Integration |
| --- | --- |
| Sign a user into an ordinary M7-enabled application | Standard M7 SDK |
| Accept an M7 consumer as an external identity, then create or locate a separate local account | Direct OIDC consumer federation in this tutorial |
| Authenticate a machine without a user | OAuth `client_credentials`, not consumer federation |

Direct federation creates two security boundaries: M7 authenticates the
consumer, and your system independently decides whether that verified consumer
may create, link, or use a local account. Successful M7 authentication must not
automatically grant local tenant membership, roles, or resource access.

## 2. Choose the callback and login-start URLs

Choose two stable HTTPS URLs owned by your integration:

```text
Callback:    https://login.example.com/oidc/m7/callback
Login start: https://login.example.com/oidc/m7/start
```

The callback receives M7's browser response. The optional login-start URL can
create a new authorization transaction when a pushed authorization request
expires.

Before registration:

- deploy the callback on HTTPS;
- make it a top-level browser endpoint, not an iframe or background API call;
- ensure it accepts only the intended `GET` query response;
- remove redirects that rewrite its scheme, host, port, path, query, or
  trailing slash; and
- decide whether production, staging, and development will use separate OAuth
  clients. Separate clients are recommended.

Redirect matching is exact. Treat these as different callbacks:

```text
https://login.example.com/oidc/m7/callback
https://login.example.com/oidc/m7/callback/
```

Do not register wildcards. Do not use a request header or user input to build
the callback dynamically.

## 3. Register a dedicated OAuth client

Create a new OAuth client in the
[M7 account application](https://user.m7.org/). Do not reuse a client that
already handles an unrelated application login.

Use this server-side federation profile:

```json
{
  "application_type": "web",
  "client_name": "Example identity federation",
  "redirect_uris": ["https://login.example.com/oidc/m7/callback"],
  "response_types": ["code"],
  "grant_types": ["authorization_code"],
  "scope": "openid profile email",
  "token_endpoint_auth_method": "client_secret_basic",
  "initiate_login_uri": "https://login.example.com/oidc/m7/start"
}
```

The web interface collects the same effective metadata even when its fields
are presented differently. An authorized provisioning tool may instead call
[`POST /register`](https://m7.org/docs/api/sso.user.m7.org/registration.md).

Save the returned `client_id`. For a secret-based registration, save the
one-time `client_secret` immediately in a secret manager. Do not paste it into
source code, a browser bundle, an issue, or a customer-facing document.

For the every-request consent behavior described below, M7 must designate the
new client for the consumer-federation profile during onboarding. A generic
OAuth registration is a valid OIDC client, but registration alone does not
enable the federation-specific consent gate.

## 4. Configure the relying system

Use deployment secrets or configuration with names appropriate to your
system. A typical mapping is:

```text
M7_OIDC_ISSUER=https://sso.user.m7.org
M7_OIDC_CLIENT_ID=CLIENT_ID
M7_OIDC_CLIENT_SECRET=CLIENT_SECRET
M7_OIDC_REDIRECT_URI=https://login.example.com/oidc/m7/callback
```

Keep the secret server-side. The browser needs the public `client_id`, but it
must never receive the secret.

Retrieve discovery at startup or through a controlled metadata cache:

```text
GET https://sso.user.m7.org/.well-known/openid-configuration
```

Verify that discovery supplies the authorization, token, UserInfo,
registration, PAR, and JWKS URLs expected for the production origin. Keep
issuer policy token-class-specific: during the current ID-token migration,
accept exactly `https://sso.user.m7.org` and legacy `id.m7.org`; current access
tokens retain `iss: "id.m7.org"`.

## 5. Create a server-side authorization transaction

When a user selects **Continue with M7**, generate fresh values using a
cryptographically secure random source:

1. `state` — at least 256 bits of unpredictable data;
2. `nonce` — an independent unpredictable value;
3. `code_verifier` — 43–128 RFC 3986 unreserved characters; and
4. `code_challenge` — base64url without padding of
   `SHA-256(code_verifier)`.

Store a short-lived server-side transaction similar to:

```json
{
  "provider": "m7",
  "state": "STATE",
  "nonce": "NONCE",
  "code_verifier": "CODE_VERIFIER",
  "redirect_uri": "https://login.example.com/oidc/m7/callback",
  "scope": "openid profile email",
  "created_at": "CURRENT_TIME",
  "expires_at": "CURRENT_TIME_PLUS_SHORT_TTL",
  "used": false
}
```

Give the browser only an opaque transaction cookie or identifier. Mark a
cookie `Secure`, `HttpOnly`, and `SameSite=Lax` where the application design
permits. Never put the verifier, client secret, or token material in the
cookie.

Expire transactions promptly. Ten minutes is an appropriate upper bound for
the current M7 authorization and PAR handoff windows; a shorter local timeout
is acceptable.

## 6. Redirect the browser to M7

Construct this authorization request using a URL builder that applies RFC 3986
query encoding:

```text
https://sso.user.m7.org/authorize?
  client_id=CLIENT_ID&
  redirect_uri=https%3A%2F%2Flogin.example.com%2Foidc%2Fm7%2Fcallback&
  response_type=code&
  scope=openid%20profile%20email&
  state=STATE&
  nonce=NONCE&
  code_challenge=CODE_CHALLENGE&
  code_challenge_method=S256
```

Navigate the top-level browser to the resulting URL. Do not send the client
secret, code verifier, or local account identifiers.

For a client designated for consumer federation, M7 presents consent on every
new interactive request, even if an eligible M7 session already exists. This
is intentional until Connected Apps grant storage is available. The user can
authorize the displayed scopes or cancel.

Do not use `prompt=none` for this profile; consent requires interaction. Use
`prompt=login` only when your policy requires fresh M7 authentication. Use
`prompt=select_account` when the user must explicitly use the M7 account
chooser, selecting an eligible remembered account or signing in with another;
it may be combined with `login` or `consent`.

If front-channel parameters should be minimized, use
[PAR](https://m7.org/docs/api/sso.user.m7.org/authorization-requests.md#pushed-authorization-requests-par). Push the
same parameters with authenticated `POST /par`, then redirect immediately with
the returned `request_uri`. Never mix stored PAR fields with replacements in
the browser URL.

## 7. Handle the callback before exchanging anything

M7 returns to the exact callback with one of two shapes.

Success:

```text
?code=AUTHORIZATION_CODE&state=STATE
```

Error:

```text
?error=ERROR&error_reason=REASON&state=STATE
```

The error shape can also contain safe `error_description` prose and an opaque
`trace_id`.

At the callback:

1. Load the server-side transaction associated with this browser.
2. Require a scalar `state` and compare it exactly with the stored value.
3. Reject duplicate protected fields and any response containing both `code`
   and `error`, or neither.
4. Atomically mark the transaction used so a second callback cannot proceed.
5. Reject an expired or previously used transaction.
6. If `error` is present, stop. Do not call `/token`.
7. If `code` is present, continue to the server-side exchange.

Do not log the raw query string, code, state, or full callback URL. Treat an
unknown `error` or `error_reason` as a terminal authorization failure, never as
success. Use [Authorization callback outcomes](https://m7.org/docs/api/sso.user.m7.org/authorization-callback-outcomes.md)
for the complete stable error and retry contract.

## 8. Exchange the authorization code

From the trusted server, send a form-encoded request using the registered
client-authentication method. For `client_secret_basic`:

```bash
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://login.example.com/oidc/m7/callback' \
  --data-urlencode 'code_verifier=CODE_VERIFIER'
```

Use the HTTP library's credential facility in production so the secret does
not enter a command history or process listing.

The `redirect_uri` and verifier must be the original, unmodified transaction
values. Never retry with a different verifier or callback. An authorization
code is short-lived and single-use.

Require a successful JSON response containing both `id_token` and
`access_token`. Treat every returned token, refresh field, binding value, or
activation value as a secret. A one-time federation proof normally needs the
ID token and a single UserInfo request; do not retain provider credentials that
your integration does not need.

## 9. Validate the ID token

Do not establish a local session after merely decoding the JWT. Perform these
checks in order:

1. Parse the protected header, require the client's configured ID-token
   algorithm in both live discovery and your verifier's allowlist, and require
   a nonempty `kid`.
2. Read the conventional `keys` array from
   `https://sso.user.m7.org/jwks.json` and select the exact member whose `kid`
   matches the protected header.
3. Validate the JWS signature with that key.
4. Require `iss` to equal `https://sso.user.m7.org` or legacy `id.m7.org` and
   reject every other value.
5. Require `aud` to contain your exact `client_id`.
6. If `azp` is present, or if `aud` contains multiple values, require `azp` to
   equal your `client_id`.
7. Require `typ=id`, `rlm=consumer`, and `pty=user`.
8. Validate `iat`, `nbf`, and `exp` with a small explicit clock-skew policy.
9. Require the token's `nonce` to equal the unused transaction nonce exactly.
10. Require `sub` to be a valid dashed UUID and retain it as the provider
    identity key.

M7's `/jwks.json` is a conventional JWKS with `RSA`, `EC`, `OKP`, and `AKP`
provider keys. Your OIDC library must support the exact selected algorithm and
key format; the [signing profiles](https://m7.org/docs/api/sso.user.m7.org/discovery-and-keys.md#signing-profiles)
explain the 15 asymmetric profiles, verified discovery advertisement and
M7 SDK boundary. Require the selected key's `alg` to match the token header
and enforce its algorithm-specific key parameters. Cache resolved keys by `kid` using the response's HTTP caching
metadata, and refresh the JWKS once for an unknown `kid` before failing so key
rotation does not break valid sign-ins. A published `x5u` points to the
matching PEM on the same SSO origin for validators that need PEM input.

Reject the proof if any check fails. Do not fall back to a decoded claim,
access token, email address, or username.

## 10. Retrieve and validate UserInfo

Request the current scoped identity projection with the access token. M7
derives the same OAuth client from the validated token. This example assumes
`/token` returned `token_type: "Bearer"`:

```bash
curl --fail-with-body --silent --show-error \
  --header 'Authorization: Bearer ACCESS_TOKEN' \
  'https://sso.user.m7.org/userinfo'
```

If `/token` returned `token_type: "DPoP"`, do not use the example unchanged.
Use `Authorization: DPoP` and supply one fresh UserInfo proof as required by
the [UserInfo request contract](https://m7.org/docs/api/sso.user.m7.org/userinfo.md#request). M7 creates any required
internal downstream attestations; clients do not send them.

Then:

1. require a successful JSON response;
2. require a scalar, nonempty `sub`;
3. compare UserInfo `sub` exactly with the validated ID-token `sub`;
4. reject a mismatch as a security failure;
5. use only fields permitted by the granted scopes; and
6. ignore unknown `m7` extension fields.

Use `preferred_username`, `name`, `picture`, and `locale` as presentation data.
Use `email` only when returned, and require `email_verified=true` before
treating it as verified contact data. Neither email nor username is a stable
identity key.

## 11. Match or create the local account

Look up the local federation record by a stable key such as:

```text
provider = m7
provider_subject = VALIDATED_ID_TOKEN_SUB
tenant_or_realm = CURRENT_LOCAL_SECURITY_BOUNDARY
```

Include the local tenant or realm in the uniqueness boundary when the same M7
consumer may independently join more than one tenant.

If a verified link exists:

- confirm the local account and membership are active;
- apply current tenant and role policy; and
- sign in that local account.

If no link exists:

- follow the tenant's explicit signup, invitation, or account-claim policy;
- let the user choose or confirm the local account where required;
- store the validated M7 `sub` only after the local operation succeeds; and
- make the write idempotent so callback retries cannot create duplicates.

Never auto-link an existing local account solely because email, username,
display name, or avatar happens to match. Linking to an existing account needs
an authenticated, explicit linking ceremony.

## 12. Establish the local session

After identity validation and local authorization both succeed:

1. rotate the local session identifier;
2. record the local account, tenant, authentication method, and authentication
   time;
3. apply the local session lifetime and reauthentication policy;
4. clear the one-time OIDC transaction; and
5. discard or securely handle all M7 credentials according to the integration's
   documented lifecycle.

Do not use the M7 ID token as your application's session cookie and do not send
it to a resource API as an access token.

## 13. Implement repeat sign-in

A returning user must complete a new authorization transaction. Generate new
state, nonce, verifier, and challenge every time; never reuse the first-login
values.

The expected returning path is:

1. the user starts M7 federation from the tenant or broker;
2. M7 may reuse its own eligible login session, but still shows the federation
   consent screen;
3. M7 returns a new code and the new state;
4. the relying system validates a new ID token and UserInfo response;
5. the validated `sub` finds the existing federation link; and
6. the relying system creates a fresh local session without recreating or
   relinking the account.

To test a different M7 account, start a new transaction and use the M7 account
chooser or fresh-login path presented by M7. Do not alter the callback's local
account mapping before the new ID token and UserInfo subject are validated.

An expired local transaction, M7 state, or PAR record must restart from the
login-start endpoint. Do not replay the prior code or callback URL.

## 14. Handle failures safely

| Failure | Handling |
| --- | --- |
| User cancels consent | Validate state, handle `access_denied`, and return to the relying sign-in page without creating a session. |
| Interaction is required | Start a new interactive request; do not retry with `prompt=none`. |
| PAR or local transaction expires | Create fresh state, nonce, PKCE material, and PAR data. |
| State is missing or mismatched | Abort before token exchange and record a credential-free security event. |
| Token endpoint returns `invalid_client` | Check the registered authentication method, client identifier, and deployed secret. Do not expose those values in the error page. |
| Token endpoint returns `invalid_grant` | Treat the code as unusable; verify expiry, reuse, redirect URI, and PKCE configuration, then start over. |
| ID-token signature, issuer, audience, type, time, or nonce fails | Reject the identity proof and do not query or update a local account. |
| Signing `kid` is unknown | Refresh that individual key once according to the key-lookup instructions; fail closed if it remains unavailable or invalid. |
| UserInfo returns `invalid_token` | Reject the proof and start a new authorization if the user retries. |
| UserInfo returns `insufficient_scope` | Verify that the access token belongs to the configured `client_id`; do not accept profile data from another client. |
| ID-token and UserInfo subjects differ | Treat as a security failure; never choose one subject or merge accounts. |
| M7 returns `server_error` or a transient network failure | Show a safe retry path and retain only an opaque `trace_id`; do not reuse a consumed code. |

Never display or log provider response bodies that can contain credentials.
Use stable `error` and `error_reason` values for behavior, not
`error_description` prose.

## 15. Verify the integration before production

Run these end-to-end tests against a non-production client first:

### Registration and routing

- The registered callback matches the deployed callback exactly.
- A wrong path, trailing slash, host, or scheme is rejected.
- The production secret is absent from browser requests, HTML, JavaScript,
  logs, and error pages.
- Discovery resolves the expected endpoints.

### First and returning sign-in

- A new M7 consumer can authorize, validate, and complete the permitted local
  signup or claim flow.
- The consent screen lists the expected scopes.
- The same consumer can sign in again and resolves the existing federation
  link without creating a duplicate account.
- A different consumer does not inherit the first consumer's link or session.
- A disabled local account or tenant membership remains denied even after
  successful M7 authentication.

### Transaction and callback defenses

- Missing, changed, duplicated, expired, and replayed state are rejected before
  code exchange.
- A callback with both `code` and `error`, or neither, is rejected.
- A code cannot be exchanged twice.
- A wrong verifier or redirect URI produces a closed failure.
- Consent cancellation returns safely without a local session.

### Token and profile validation

- A wrong issuer, audience, authorized party, nonce, logical type, realm,
  principal type, expiry, or signature is rejected.
- An unknown signing key is fetched by `kid`, and an invalid replacement key
  still fails closed.
- A UserInfo subject mismatch never creates, finds, or links an account.
- Missing optional email or profile claims do not change the identity key.

### Recovery and operations

- Expired PAR and local transactions restart with entirely new proof values.
- Token, UserInfo, key-service, and network failures produce a safe retry or
  terminal result without leaking diagnostics.
- Concurrent callbacks create at most one local link and one accepted session.
- Logs contain correlation identifiers but no codes, state, nonce, verifier,
  secrets, tokens, raw callback URLs, or personal profile bodies.

## Production-readiness checklist

- [ ] A dedicated production OAuth client is registered.
- [ ] M7 has designated the client for consumer-federation consent behavior.
- [ ] Every redirect and login-start URI is exact, stable, and HTTPS.
- [ ] The client secret is stored in a secret manager with documented rotation.
- [ ] Discovery metadata and per-`kid` key lookup are implemented and cached.
- [ ] Authorization uses top-level navigation, `response_type=code`, and only
      approved scopes.
- [ ] State, nonce, verifier, and challenge are fresh, expiring, and single-use.
- [ ] PKCE accepts only `S256` and preserves the verifier exactly.
- [ ] Callback parsing rejects missing, duplicate, conflicting, expired, and
      replayed values before token exchange.
- [ ] Token exchange uses the registered client-authentication method on a
      trusted server.
- [ ] ID-token signature and every required consumer claim are validated.
- [ ] UserInfo is requested for the owning client and its `sub` must match the
      ID token.
- [ ] Local account matching uses the validated provider `sub` and the correct
      tenant or realm boundary.
- [ ] Email and username matching cannot silently link accounts.
- [ ] Returning sign-in creates fresh proof values and reuses only the verified
      local federation link.
- [ ] Provider authentication is followed by independent local membership,
      role, and resource authorization.
- [ ] Cancellation, expiry, replay, invalid client, invalid grant, signature,
      nonce, subject-mismatch, and transient-failure paths are tested.
- [ ] Logs and telemetry redact every credential and raw callback URL.
- [ ] Operational monitoring, rate limits, incident response, and secret/key
      rotation have named owners.

## Next references

- [Consumer federation protocol reference](https://m7.org/docs/api/sso.user.m7.org/consumer-federation-reference.md)
- [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)
