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.
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:
- Authorization code, query response, PAR if you want the request off the front channel.
- Token exchange with the client’s registered method:
none, Basic, POST,client_secret_jwt, orprivate_key_jwt. - Validate the ID token against the published JWKS (
RS256orRS512). - Call UserInfo as JSON, or as a signed JWT if you registered
userinfo_signed_response_alg. - Refresh with the refresh token and client authentication. You may omit
binding_chainandbinding_link.
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.
Access tokens today still carry iss: "id.m7.org". Do not configure an access-token validator to require the discovery issuer. ID tokens use the SSO issuer, with a documented migration allowlist for legacy id.m7.org. 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. /token/ack promotes the exact pending generation after the client has stored it. ack_supersede_pending starts a new child from active state if an older pending attempt is stuck. Use this when a lost HTTP response must not kill the previous refresh token. web-php already holds pending packages in an encrypted cookie. token-php and the CLI expose acknowledgeToken() / ack and leave storage to you.
DPoP. Access tokens can carry cnf.jkt. /token policy is disabled, optional, or required. A DPoP-bound lineage needs a fresh proof per endpoint, with ath bound to that access token. Token-php and the CLI do not generate proofs; you supply one. The BFF coordinates browser proofs for the hops it owns.
Fingerprints are an M7 credential-binding value, not DPoP. Do not send a DPoP proof on an unbound Bearer token.
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. That is why CLI 0.1.1 exists.
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 advertise mTLS. Discovery does not. Do not claim OpenID certification. The stock profile is implemented; a generic-library conformance run is still queued.
SSO reference: M7 SSO API. SDK: Identity SDK. Development access, MTL-10, PHP this round.