Stock OIDC and the M7 path

You do not have to take every M7 extension on day one. The same authorization server offers a stock OpenID Connect profile and an opt-in M7 lifecycle. Mix them up and you will fight the protocol. Keep them straight and a generic client can sign in.

People, machines, and tenants is why the principal is explicit. This post is which fields you actually send.

A two-lane pier: one open lane and one with extra locks, same light at the end.

Stock is the default

Discovery lives at https://sso.user.m7.org/.well-known/openid-configuration. The issuer in that document is https://sso.user.m7.org. PKCE is S256 only. plain is rejected.

A stock integration:

  1. Authorization code, query response, PAR if you want the request off the front channel.
  2. Token exchange with the client’s registered method: none, client_secret_basic, client_secret_post, client_secret_jwt, private_key_jwt, tls_client_auth, or self_signed_tls_client_auth.
  3. Validate the ID token with the registered signing algorithm (RS256 by default), the appropriate verification key, and the required issuer, audience, time, and nonce checks. Asymmetric profiles use the published JWKS; confidential-client HMAC profiles use the client secret.
  4. Call UserInfo as JSON, or as a signed JWT if you registered userinfo_signed_response_alg.
  5. Refresh with the refresh token and client authentication. Under the default advisory binding policy, you may omit binding_chain and binding_link. Omit refresh_mode for ordinary renewal: it already selects strict_rotation. Persist the complete replacement package and serialize concurrent refreshes.

Default binding policy is advisory. Identity still returns usable binding material. A generic client may ignore those members. That is how stock OIDC is supposed to work: unknown token-response fields are ignored, not fatal.

Newly issued access tokens now carry iss: "https://sso.user.m7.org". Configure new access-token validation for that issuer. Accept legacy id.m7.org only when explicitly included in trusted migration policy. ID tokens have their own documented migration allowlist. Read issuer values by token class before you copy a single iss check.

If Sign in with M7 plus ID-token validation is all you need, stop here. The PHP website install is that path.

When to turn M7 on

Turn the extensions on when the threat is loss, theft, or replay of a bearer package — not when you want more JSON.

Bindings. binding_chain and binding_link are possession secrets for the refresh lineage. Policy is advisory (default), optional (if you send them they must be valid), or required (refresh fails without a complete package). The client record is the authority, not the token request. Personal-lineage certificates inherently require bindings; the chain participates in unwrapping the key.

ACK. A refresh can return a pending successor instead of swapping the live package immediately. The old package stays active; the pending access and refresh tokens are unusable until /token/ack activates the exact stored generation. Persist the entire envelope, confirm activation, then promote it. ack rejects another refresh while a successor is pending. ack_supersede_pending replaces a never-active pending child and makes only the latest eligible for activation. Lost ACK responses permit idempotent retries; lost refresh responses still need expiry or resolution handling. The documented Web/PHP BFF profile holds pending packages in an encrypted cookie. Choose that lifecycle deliberately; storage and concurrency remain part of the integration.

DPoP. Access and refresh tokens can carry cnf.jkt. M7 supports ES256 proofs; registration and grant policy determine when they are required. Code pickup and refresh keep the same bound key. Protected resources need a fresh method/URI/time proof, the access-token ath, and replay enforcement. The documented Web/PHP BFF profile coordinates browser proofs for the hops it owns; that does not install proof validation on your API.

mTLS. tls_client_auth authenticates the client with an M7-trusted certificate and its registered subject or SAN selector. self_signed_tls_client_auth uses the exact self-signed certificate registered through the client’s keys. Use the discovered mtls_endpoint_aliases; the issuer stays on the canonical SSO origin.

Certificate-bound tokens are a separate registration choice: tls_client_certificate_bound_access_tokens=true. M7 records the leaf certificate’s SHA-256 thumbprint in cnf.x5t#S256. Refresh must present the same certificate. Protected resources must require that exact bound certificate over mutual TLS; accepting the token as an ordinary bearer defeats the binding. If DPoP and certificate confirmation are both present, both constraints apply. Plan certificate transport, proxy trust, and resource checks for your deployment; a browser sign-in presentation or SDK installation does not establish them.

Fingerprints are an M7 credential-binding value, not DPoP. Do not send a DPoP proof on an unbound Bearer token.

For the current choices across authorization requests, sender constraints, and renewal, read Choose the security profile your app can use and the provider profile.

What not to do

Do not send stored profile scope on every refresh “because it was on the device grant.” Omit scope to keep the original grant; send it only to request an equal or narrower subset. Keep this rule even when upgrading the CLI.

Do not treat local JWT validation as “this caller may hit every route.” Signature and claims are one check. Resource servers still enforce audience, scope, tenant, and object policy.

Do not confuse certificate authentication with certificate-bound tokens. Do not claim OpenID certification: conformance preparation is not an official suite result or certification.

SSO reference: M7 SSO API. SDK: Identity SDK. Development access, MTL-10, PHP this round.