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
GETquery 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:
state— at least 256 bits of unpredictable data;nonce— an independent unpredictable value;code_verifier— 43–128 RFC 3986 unreserved characters; andcode_challenge— base64url without padding ofSHA-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:
- Load the server-side transaction associated with this browser.
- Require a scalar
stateand compare it exactly with the stored value. - Reject duplicate protected fields and any response containing both
codeanderror, or neither. - Atomically mark the transaction used so a second callback cannot proceed.
- Reject an expired or previously used transaction.
- If
erroris present, stop. Do not call/token. - If
codeis 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:
- 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. - Read the conventional
keysarray fromhttps://sso.user.m7.org/jwks.jsonand select the exact member whosekidmatches the protected header. - Validate the JWS signature with that key.
- Require
issto equalhttps://sso.user.m7.orgor legacyid.m7.organd reject every other value. - Require
audto contain your exactclient_id. - If
azpis present, or ifaudcontains multiple values, requireazpto equal yourclient_id. - Require
typ=id,rlm=consumer, andpty=user. - Validate
iat,nbf, andexpwith a small explicit clock-skew policy. - Require the token's
nonceto equal the unused transaction nonce exactly. - Require
subto 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:
- require a successful JSON response;
- require a scalar, nonempty
sub; - compare UserInfo
subexactly with the validated ID-tokensub; - reject a mismatch as a security failure;
- use only fields permitted by the granted scopes; and
- ignore unknown
m7extension 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
subonly 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:
- rotate the local session identifier;
- record the local account, tenant, authentication method, and authentication time;
- apply the local session lifetime and reauthentication policy;
- clear the one-time OIDC transaction; and
- 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:
- the user starts M7 federation from the tenant or broker;
- M7 may reuse its own eligible login session, but still shows the federation consent screen;
- M7 returns a new code and the new state;
- the relying system validates a new ID token and UserInfo response;
- the validated
subfinds the existing federation link; and - 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
codeanderror, 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-
kidkey 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
S256and 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
submust match the ID token. - Local account matching uses the validated provider
suband 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.