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 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; 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:

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:

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. Do not reuse a client that already handles an unrelated application login.

Use this server-side federation profile:

{
  "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.

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:

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:

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:

{
  "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:

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. 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:

?code=AUTHORIZATION_CODE&state=STATE

Error:

?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 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:

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 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":

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. 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:

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