Token endpoint
This page describes the ordinary OAuth endpoint. Opted-in tenant browsers use browser-direct token pickup and refresh, which sends JSON and mandatory endpoint proofs, holds refresh credentials at SSO, and exposes only its public token package. Ordinary client authentication below is unchanged.
POST https://sso.user.m7.org/token
Content-Type: application/x-www-form-urlencoded
Use the token endpoint to exchange authorization codes and device codes,
refresh an existing package, obtain a service token with client_credentials,
or delegate user access with RFC 8693 Token Exchange. It accepts only POST.
Client authentication is always tied to the OAuth client's configured
token_endpoint_auth_method. A client must also be enabled for the requested
grant type.
Client authentication
| Method | Request contract |
|---|---|
none |
Send client_id in the form body. Suitable for public clients. It cannot use client_credentials or Token Exchange. |
client_secret_basic |
Send HTTP Authorization: Basic base64(client_id:client_secret). |
client_secret_post |
Send client_id and client_secret in the form body. |
client_secret_jwt |
Send client_id, client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer, and a signed client_assertion. |
private_key_jwt |
Send the same assertion fields. The client must have an inline jwks or HTTPS jwks_uri configured. |
tls_client_auth |
Present the registered CA-issued client certificate and send client_id in the form body to the discovered mTLS token alias. |
self_signed_tls_client_auth |
Present the exact registered self-signed client certificate and send client_id in the form body to the discovered mTLS token alias. |
For JWT client assertions, iss and sub must equal the client_id. The
aud claim may identify the canonical authorization-server issuer
https://sso.user.m7.org or the exact invoked endpoint URL. The assertion must
contain a current iat, unexpired exp, and unique jti. M7 rejects replayed
and stale assertions. Supported private_key_jwt signing algorithms are RSA
(RS256, RS384, RS512) and ES256; configure the intended algorithm on
the client.
client_secret_jwt uses a configured HMAC algorithm: HS256, HS384, or
HS512. Dynamic Registration selects HS256 when that method's algorithm is
omitted and RS256 when a private_key_jwt algorithm is omitted; the
effective algorithm is returned and stored authoritatively.
For either mTLS method, use
https://mtls-sso.user.m7.org/token, as published in
mtls_endpoint_aliases. The authorization-server issuer remains
https://sso.user.m7.org. When
tls_client_certificate_bound_access_tokens=true, successful issuance places
the presented leaf certificate's SHA-256 thumbprint in
cnf."x5t#S256". Refresh requests and protected-resource requests must then
present the same certificate; there is no bearer fallback.
client_secret_jwt and HS256/384/512 token signing need recoverable HMAC key
material. API User stores it in a versioned AES-256-GCM envelope. Secret-JWT
uses it for assertion verification; HS token issuance uses the same original
secret through the protected signing handoff. API User and SSO, plus ID.M7 for
token signing, must have the same
M7_OAUTH_CLIENT_SECRET_JWT_KEYS key ring (64-character hexadecimal keys,
newest first); M7_OAUTH_CLIENT_SECRET_JWT_KEY remains a single-key fallback.
One-way legacy hashes must be replaced by setting a recoverable secret before
secret-JWT authentication or HMAC issuance. See HMAC setup.
The current-source eligibility guard rejects a saved none + HS256/384/512
configuration with HTTP 400 unauthorized_client, before code/device pickup
or refresh exchange. Posted signing/authentication settings cannot override
the saved client. Public clients must use an asymmetric signing algorithm.
Local acceptance is complete; production verification of this restriction
remains pending. Existing credentials are not revoked by this check.
DPoP policy
After authenticating the client, /token applies its registered
dpop_policy consistently to authorization-code, device-code, refresh-token,
and client-credentials grants:
| Policy | Required behavior |
|---|---|
disabled |
/token ignores DPoP and prevents new DPoP or fingerprint binding. An existing DPoP-bound refresh lineage cannot refresh under this setting; start a new authorization. |
optional |
Omit DPoP for an unbound credential, or send a valid proof to bind the issued lineage. Supplied fingerprint bindings are retained. |
required |
Send a valid DPoP header on every token request. Fingerprint binding remains optional. |
A supplied proof must be an ES256 dpop+jwt for the exact token endpoint and
HTTP POST, with a current iat and nonempty, per-request jti. Missing or
invalid required material returns invalid_dpop_proof before an authorization
code or device code is consumed and before token issuance begins. Generate a
new proof for every request and retry. M7 atomically retains an opaque hash of
the verified key thumbprint and jti for the proof window; concurrent or later
reuse of the same proof returns invalid_dpop_proof.
The proof sent to /token binds the token request itself and therefore does
not contain the protected-resource ath claim. Resource proofs for a returned
DPoP access token are separate and must bind the exact access token.
This client setting governs customer application tokens only. It does not change M7's internal SSO root-session binding. See DPoP status. Changing it does not migrate live refresh lineages. Existing packages that no longer fit the configured policy may fail and require authorization again.
Discovery advertises ES256 through dpop_signing_alg_values_supported.
Dynamic Registration uses the standard dpop_bound_access_tokens boolean;
true maps to required DPoP, while omission or false retains opportunistic
DPoP. M7 does not currently issue DPoP-Nonce; clients still need a new proof
and unique jti for each request and retry.
Authorization-code exchange
Send these form fields after receiving a code at the registered callback:
| Field | Required | Notes |
|---|---|---|
grant_type |
Yes | authorization_code |
code |
Yes | The authorization code from /authorize. It is single-use. |
redirect_uri |
Yes | Exactly the same registered URI used in the authorization request. |
code_verifier |
Yes | The original, exact 43-to-128-character unreserved PKCE verifier. M7 verifies its SHA-256 challenge and does not trim or normalize it. |
fingerprint |
No | A 64-character hexadecimal M7 credential binding, when the client integration uses one. |
If the original authorization request used dpop_jkt, the /token request
must carry a valid DPoP proof from that exact key. The proof both satisfies the
authorization-code binding and determines the cnf.jkt sender constraint on
the issued token lineage.
Authenticate the client as described above. See the complete browser flow in Quickstart.
Token Exchange (RFC 8693)
The local-user, single-target profile is deployed and advertised. Consumer
RS256 exchange, access-only/offline selection, delegated strict refresh,
permission withdrawal and cleanup passed live acceptance on 2026-09-20.
This evidence does not establish every tenant, signing or sender-binding
combination. Check discovery's grant_types_supported before integration.
An authenticated confidential application can exchange its original M7 user access token for access to one connected provider. Register the canonical exchange grant on the requesting client and configure the provider's connection policy. The provider must permit exchange, and the connection must be active and approved under that policy. Owner-proxy admin applications cannot use this grant.
| Field | Required | Contract |
|---|---|---|
grant_type |
Yes | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token |
Yes | Active local M7 user access token issued to the authenticated client. |
subject_token_type |
Yes | urn:ietf:params:oauth:token-type:access_token |
requested_token_type |
No | Omit or use urn:ietf:params:oauth:token-type:access_token. |
audience or resource |
Yes | Exactly one target. It must match the connection audience and both apps' registered audiences. resource must be an absolute URI without a fragment. |
scope |
No | Space-delimited subset of the source token, requesting client's allowed scopes, connection scopes and provider's exchange scope ceiling. |
fingerprint |
Conditional | Existing M7 fingerprint binding when required by the subject credential. |
Parameters cannot repeat. If both audience and resource are supplied, they
must identify the same target. This profile does not accept actor_token,
external JWT/SAML subjects, client-credentials or refresh-token subjects,
ID-token subjects, redelegation, custom claim
overrides, or HMAC token signing. Both apps must use the same identity realm:
the consumer realm, or the same organization. Tenant subjects require a
current delegated M7 identity or consumer-user link; native federated tenant
shadow credentials are not supported by this initial exchange profile.
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/token' \
--user 'CLIENT_ID:CLIENT_SECRET' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
--data-urlencode 'subject_token=USER_ACCESS_TOKEN' \
--data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \
--data-urlencode 'audience=https://files.example.com' \
--data-urlencode 'scope=files:read'
With no offline_access in the requested scope, the response contains only
the access credential, even when the source authorization permits offline
access. Omitted scope uses the allowed intersection excluding offline_access.
Explicitly requesting authorized offline_access returns access and refresh
credentials together, including the normal M7 binding metadata. The requesting
client must also register refresh_token. Requesting
requested_token_type=...:refresh_token is not a bundle-selection mechanism
and is rejected. These are M7 issuance-policy choices under
RFC 8693's optional refresh-token response.
A refresh token is usable without presenting the previous access token.
{
"access_token": "ACCESS_TOKEN",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 300,
"scope": "files:read"
}
The JWT preserves the user sub, identifies the requesting app in client_id
and act.sub, and stamps the receiving app's registered OAuth UUID in
m7.target_client_id. Its aud contains the selected resource audience and
the issuer-added receiving-app UUID. Introspection includes m7 and act.
See requester and target claims
before applying a client restriction; client_id is not the receiving app.
No ID token is issued by this grant. Existing DPoP or certificate binding must
be proved and is retained; ordinary client authentication and DPoP replay
checks still apply. Initial access lifetime cannot exceed the remaining subject
token lifetime. Registered client lifetimes, the provider's exchange TTL cap,
and app/connection expiration further limit issued credentials.
Refresh an exchanged bundle using the ordinary refresh_token grant below.
Every refresh rechecks both registrations, current subject identity and tenant
group admission, connection approval, audiences, and the complete original
scope ceiling. It preserves the actor and target and cannot extend the
delegation's absolute offline expiration. Only scope subsets, the existing
binding/fingerprint package and permitted refresh lifecycle selection apply;
custom claims, requested lifetimes and target overrides do not change this
profile's issuance policy. Obtain a new authorization when the original scope
ceiling is no longer permitted.
Withdrawal of connection or exchange approval blocks new exchange and delegated refresh. It does not, by itself, revoke the original source login. An offline JWT check alone cannot observe a policy change; use the applicable live token and connection verification checks where current permission is required.
External token acceptance is out of scope. Impersonation, multiple targets and chained exchanges are deferred and unsupported in this profile. The SDK exchange guide covers the current-source PHP operation and CLI command, including safe storage and the distinction from ordinary refresh.
Refresh-token exchange
The standard advisory binding policy permits a refresh request with the refresh token and client authentication. M7 binding-required profiles additionally need the stored binding package:
| Field | Required | Notes |
|---|---|---|
grant_type |
Yes | refresh_token |
refresh_token |
Yes | The refresh credential. M7 also accepts the legacy alias token. |
binding_chain |
Conditional | Required by a binding-required profile or legacy personal-lineage signing. Save the value returned with the refresh token. |
binding_link |
Conditional | Send with binding_chain when the binding package is required or supplied. |
fingerprint |
Conditional | Required when the existing lineage is fingerprint-bound. Exactly 64 hexadecimal characters. |
new_fingerprint |
No | A replacement binding fingerprint. When fingerprint is supplied and new_fingerprint is omitted, M7 keeps the same value. |
scope |
No | A space-delimited subset of the authenticated refresh lineage. Omission uses the lineage's full granted scope. |
access_expires |
No | Requests the next access-token lifetime when allowed by policy. |
access_claims |
No | Requests allowed access-token claims when allowed by policy. |
refresh_mode |
No | M7 lifecycle extension; normally omit it. See below. |
The optional binding policy permits omission but validates a supplied package; the required policy requires a valid complete package. Registration and provider policy determine the mode; request input cannot select it.
The refresh token must belong to the authenticated client_id. Persist the
full replacement package before removing the old package. If the requested
refresh cannot proceed, keep the prior package unless the service has returned
a confirmed replacement or terminal error that makes it unusable.
A refresh request cannot add a scope absent from either the signed lineage or the client's current registration. A requested subset limits the new access token; it does not rewrite the refresh lineage's original maximum scope. If client policy removes authority already present in the lineage, refresh fails and the application must start a new authorization.
The refresh lineage's resource-audience set is immutable. Do not send aud on
a refresh request; obtain a new authorization or grant to change the set.
Refresh also fails when current client policy no longer allows any lineage
audience.
Refresh lifecycle extension
Without refresh_mode, M7 currently uses strict_rotation. Supported modes
are static, strict_rotation, grace_rotation, ack, and
ack_supersede_pending. Most applications should omit this parameter.
Both ACK modes return an unusable pending successor while active R0 remains
active. ACK of the eligible package activates it and revokes R0. The modes
differ when R1 is already pending: ack rejects another R0 refresh with
refresh_in_progress, while ack_supersede_pending lets R0 issue R2,
atomically revokes never-active R1, and makes only R2 eligible for ACK.
ack_supersede_pending is a normal lifecycle, not a recovery-only mode.
The two modes have identical client, refresh-package binding, activation-secret, and conditional DPoP requirements. They require durable, atomic persistence and explicit handling for lost refresh or ACK responses, idempotent ACK retries, and terminal stale activations. See Token acknowledgement for the complete state and error contract.
Client credentials
Use this grant for a machine application acting as itself, never as an end
user. The client must be confidential and be enabled for client_credentials.
| Field | Required | Notes |
|---|---|---|
grant_type |
Yes | client_credentials |
aud |
Yes | One or more exact requested audiences from the client's registered allowed_audiences. Repeat the field for multiple values. |
scope |
No | Requested space-delimited scope string, subject to client policy. |
access_expires |
No | Requested access lifetime. expires_in and expires are accepted aliases. |
claims |
No | Requested JSON claims object. payload is an accepted alias. |
fingerprint |
No | 64-character hexadecimal M7 binding. |
label |
No | A management label for the issued record. |
Example:
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/token' \
--user 'CLIENT_ID:CLIENT_SECRET' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'aud=https://api.example.com' \
--data-urlencode 'scope=file.read file.write' \
--data-urlencode 'access_expires=900'
This flow returns an access token only; it does not issue a refresh token. A
client using token_endpoint_auth_method=none is rejected for this grant.
Device-code exchange
Use this only after obtaining device_code from
/device_authorization:
| Field | Required | Notes |
|---|---|---|
grant_type |
Yes | device_code. The standard urn:ietf:params:oauth:grant-type:device_code identifier is also accepted. |
device_code |
Yes | Secret device code returned by /device_authorization. |
Authenticate the same client that initiated the device flow. If the user has
not approved yet, the response is authorization_pending. Observe the
response's interval; do not poll more frequently. A device code is consumed
after successful pickup.
Token responses
The response varies by grant and client policy. A user-authorized success typically contains:
{
"token_type": "Bearer",
"access_token": "ACCESS_TOKEN",
"expires_in": 900,
"refresh_token": "REFRESH_TOKEN",
"refresh_expires_in": 2592000,
"binding_chain": "BINDING_CHAIN",
"binding_link": "BINDING_LINK",
"scope": "openid profile",
"id_token": "ID_TOKEN"
}
For authorization-code and device-code flows, id_token is returned when
openid was granted. RFC 8693 exchange does not return an ID token. An access token from
client_credentials does not have a user refresh package. Treat every token
and every binding or activation field as a secret.
The response above is the OAuth token-package envelope, not the decoded access token payload. See Access-token formats for the consumer, tenant-member, and OAuth-application JWT claim profiles.
The example is an unbound response. M7 derives token_type from the access JWT
in every supported grant and refresh envelope:
Bearermeans the access token has nocnfsender constraint;DPoPmeans the completecnfobject contains only a validjkt; and- a malformed, legacy, mixed, or ambiguous confirmation shape fails closed
with
server_errorinstead of returning the access token.
For DPoP, retain the private key for the complete lineage and use the exact
authorization and proof contract required by each protected resource. See
DPoP status.
Errors
Errors use OAuth JSON fields such as invalid_request, invalid_client,
invalid_grant, unauthorized_client, invalid_scope, invalid_target, and
unsupported_grant_type. invalid_dpop_proof means the required proof was
missing or the supplied proof was invalid. Do not branch on prose in
error_description.
An unexpected persistence, configuration, dependency, or processing failure
returns server_error with the safe description An unspecified error occurred. Please try again. and an opaque trace_id. Retain the trace ID for
support correlation only. Sanitized internal failure metadata is logged
privately. Raw exception text and downstream diagnostics are never part of the
token response.