Access-token formats
M7 issues three access-token profiles: consumer user, tenant member, and OAuth
application. All three are signed JWTs, but their subjects and authorization
boundaries are different. A resource server must select the profile it expects
as part of its own route policy; it must not grant authority merely because an
unverified token contains a recognized principal_type.
This page describes access tokens only. Treat refresh tokens as opaque even though M7 currently signs them as JWTs. An ID token is an OpenID Connect identity assertion for the relying party and must not be used as an API access token.
The signed JWT carried by a JARM response is an authorization-result envelope. Its inner signature uses RS256 or RS512, has the canonical SSO issuer and client-ID audience, and expires after five minutes. Validate its signature, issuer, audience, time and transaction state before inspecting its code or error. It is never an access credential, ID token or refresh token. It is readable by default; registered response encryption wraps it in a compact JWE. Decrypt before validating the inner signature and claims. Local encryption interoperability and scoped live encrypted-response acceptance have passed; see the encryption guide for the remaining wider live matrix.
For signature validation, use the signing profiles and key lookup rules.
Asymmetric provider-signed tokens use published JWKS keys. Personal-lineage access tokens
use their exact-kid PEM key without a provider JWKS entry; their principal and
claim-validation rules remain those of the token profile below. HMAC tokens
use the original client secret with no public key lookup; retain authoritative
issued-token and active-state checks rather than trusting a MAC alone. See
HMAC setup and verification.
HMAC ID tokens require a confidential client; public clients using none
must select an asymmetric algorithm. The linked guide records the locally
tested User.M7/SSO restriction and its pending deployment verification.
For ML-DSA, native signature support does not establish browser storage compatibility. The current Web SDK stores whole tokens in single cookies and is unsuitable for general ML-DSA use. Device/native token responses use a different storage path; every consumer still needs adequate token and header budgets. See post-quantum tokens and browser storage.
Session identity and Logout Tokens
The ID token's sid identifies the original root SSO login and survives refresh
rotation. It is distinct from that token's jti and the User API's local session
record ID. Use the validated issuer and sid to associate a local application
session with logout notifications.
A back-channel Logout Token is a separate signed logout+jwt artifact, optionally
wrapped in the client's required ID-token JWE policy. It carries issuer, receiving
client audience, subject, root sid, time claims, a unique jti and the logout
event, with no nonce. Validate and process it as a notification; never use it
as an access token, refresh token or sign-in assertion. The linked guide gives
the complete receiving contract and current SDK limits.
Three different type fields
An M7 token exchange and signed access token can expose three similarly named values. They are not interchangeable:
| Location | Example | Meaning |
|---|---|---|
| OAuth token response | "token_type": "Bearer" or "DPoP" |
The authorization scheme for the returned access token. DPoP means the token is bound through cnf.jkt. |
| JWT protected header | "typ": "JWT" |
The serialization is a signed JSON Web Token. |
| JWT payload | "typ": "access" |
M7's credential form and lifecycle discriminator. |
Current source emits payload typ = access for consumer, tenant, and OAuth
application access tokens. M7's platform relays still recognize the signed
legacy session form during migration, but new access tokens no longer use it.
Do not accept an arbitrary session JWT as an access token. Validate the full
profile, including signature, issuer, expected audience and client, time
claims, scope, principal type, and server-side active state where required.
Profile discriminator
| Access-token profile | principal_type |
Current payload typ |
Canonical sub |
|---|---|---|---|
| Consumer user | consumer |
access |
M7 consumer user ID |
| Tenant member | tenant |
access |
Tenant member/client ID |
| OAuth application | oauth_client |
access |
Public OAuth client_id |
principal_type states whose authority the token represents. The sub claim
is the stable canonical subject within that profile. Use sub, rather than a
name, email address, or presentation field, as the principal identifier.
Shared access-token envelope
The three profiles use the following shared claims:
| Claim | Meaning |
|---|---|
iss |
New tokens use the canonical authorization-server issuer, https://sso.user.m7.org; configure that exact issuer for validation. |
typ |
Raw credential form: access for every newly issued access-token profile. session is legacy migration compatibility only. |
principal_type |
consumer, tenant, or oauth_client. |
id |
Identity/profile identifier used by M7. It is not a substitute for the profile's canonical sub. |
sub |
Canonical subject for the selected profile. |
client_id |
OAuth client that requested issuance. It identifies the caller, not the human subject or necessarily the receiving application. |
m7.target_client_id |
Issuer-owned receiving application's registered OAuth client UUID, nested inside the m7 object. |
act |
Present for delegated exchange; act.sub identifies the requesting application acting for the unchanged user. |
aud |
Permitted resource audience(s), plus the issuer-added receiving-app UUID. A resource must still require its own exact audience. |
scope |
Granted space-delimited scopes, when scopes were granted. |
cnf |
Optional sender constraint: DPoP key thumbprint jkt, certificate thumbprint x5t#S256, or both when both bindings apply. An unbound token omits cnf. |
iat, nbf, exp |
Issuance, not-before, and expiry timestamps. |
jti |
Unique token/session identifier used for authoritative state checks. |
A response for a token with valid cnf.jkt uses token_type=DPoP.
A certificate-only bound token or an unbound token uses token_type=Bearer;
certificate binding still requires the matching certificate. When both bindings
are present, both must be proved. Malformed or unsupported confirmation shapes
fail closed. A token can also contain
profile-specific claims. Do not infer authority from an unfamiliar claim, and
ignore extension claims unless the resource has an explicit policy for them.
Requesting client and receiving application
For an ordinary app-bound login or machine grant, the requester and receiving application are the same registered client. An exchange changes the receiving application while retaining the requester and user:
| Claim | Bob's original Blog access token | Blog exchanges for Files |
|---|---|---|
sub |
Bob's user UUID | Same Bob UUID |
client_id |
Blog's OAuth client UUID | Blog's OAuth client UUID |
act.sub |
Absent | Blog's OAuth client UUID |
m7.target_client_id |
Blog's OAuth client UUID | Files' OAuth client UUID |
aud |
Registered resource audience(s), plus Blog UUID | Selected Files resource audience, plus Files UUID |
An exchanged consumer access-token payload includes these values (other required access-token fields are omitted from this fragment):
{
"iss": "https://sso.user.m7.org",
"typ": "access",
"principal_type": "consumer",
"sub": "BOB_USER_UUID",
"client_id": "BLOG_OAUTH_CLIENT_UUID",
"act": {"sub": "BLOG_OAUTH_CLIENT_UUID"},
"m7": {"target_client_id": "FILES_OAUTH_CLIENT_UUID"},
"aud": ["https://files.example.com", "FILES_OAUTH_CLIENT_UUID"],
"scope": "files:read"
}
m7 is M7's private JSON-object extension namespace, not an OAuth-standard
target field or a URI-namespaced registered claim. The issuer reserves the entire
object. Requested or configured custom claims cannot overwrite it. The target
is the public OAuth client UUID, not an application-record ID or an owner ID.
Ordinary refresh preserves this target; a new authorized exchange selects its
new target through the approved connection, never through caller-supplied claims.
Require a validated m7.target_client_id when your resource policy restricts
the receiving registered application. Do not substitute client_id, an arbitrary
UUID in aud, or the old flat m7_target_client_id name. A missing target in a
legacy token fails a policy that explicitly requires it; it does not acquire a
target from an audience string. Checking client_id instead restricts the
requesting application. Neither check replaces resource audience, scopes,
principal, current authorization or sender-binding checks. API.User's public
policy uses its documented audiences and scopes rather than a single mandatory
receiving-app UUID.
The issuer-added UUID does not prove ownership of a URL or authorize other
resource audiences. Audience ownership controls remain a separate policy concern.
Introspection returns the available m7 and act metadata after its ordinary
requesting-client authentication check. Delegated refresh preserves these claims
and rechecks the connection's current permission.
This access-token target contract does not redefine ID-token aud/azp, signed
UserInfo audiences, Logout Tokens, or UserInfo's existing m7.user, m7.client
and related identity projections. Refresh tokens remain opaque to clients;
retain the complete refresh bundle rather than reading its claims.
Who may inspect and depend on claims
An OAuth client that obtains an access token should normally treat it as a credential for the target resource, not as the source of its application session. Use a validated ID token for OpenID Connect sign-in and UserInfo for the current scoped identity projection. Client applications must not derive a user, tenant, role, or group decision merely by decoding an access token.
A resource server may inspect and depend on the documented access-token claims only after it has validated the complete token and selected the expected profile from trusted route policy. It may rely on the documented core and profile-specific claims for that validated profile. It must ignore unknown extension claims unless its own public authorization contract assigns them a meaning.
Refresh tokens are signed JWTs in the current implementation but are opaque to clients. Do not decode them or depend on their claims. Store and send the whole refresh package as documented by the token endpoint.
ID tokens are signed OpenID Connect JWTs intended for the relying party. The
relying party may inspect and depend on their standard claims only after
validating the signature, aud, azp, time claims, and nonce when one was
requested. Never send an ID token to a resource API as an access token.
The examples below show decoded payloads with non-secret placeholders. They are claim-shape examples, not tokens that can be used for authentication.
Consumer-user access token
{
"iss": "https://sso.user.m7.org",
"typ": "access",
"principal_type": "consumer",
"id": "IDENTITY_PROFILE_UUID",
"sub": "CONSUMER_USER_UUID",
"name": "alice",
"role": "CONTROL_PLANE_ROLE",
"client_id": "OAUTH_CLIENT_ID",
"m7": {"target_client_id": "OAUTH_CLIENT_ID"},
"aud": ["https://api.example.com", "OAUTH_CLIENT_ID"],
"scope": "openid profile email",
"iat": 1787000000,
"nbf": 1786999995,
"exp": 1787000900,
"jti": "TOKEN_SESSION_UUID"
}
Consumer-specific claims are:
id: the consumer's M7 identity/profile ID;sub: the canonical consumer user ID;name: the consumer's stable handle; androle: the authoritative control-plane role, omitted when no role is set.
Consumer access tokens do not carry the legacy user alias, avatar/profile
presentation data, tenant organization or group claims, or arbitrary
relying-party claim decoration. Obtain presentation data through UserInfo.
Tenant-member access token
{
"iss": "https://sso.user.m7.org",
"typ": "access",
"principal_type": "tenant",
"id": "AUTHENTICATING_IDENTITY_UUID",
"sub": "TENANT_MEMBER_UUID",
"name": "alice",
"org": {
"id": "TENANT_ORG_UUID",
"slug": "example-org"
},
"groups": "engineering:member,owners:admin",
"client_id": "OAUTH_CLIENT_ID",
"m7": {"target_client_id": "OAUTH_CLIENT_ID"},
"aud": ["https://api.example.com", "OAUTH_CLIENT_ID"],
"scope": "openid profile groups",
"iat": 1787000000,
"nbf": 1786999995,
"exp": 1787000900,
"jti": "TOKEN_SESSION_UUID"
}
Tenant-specific claims are:
id: the M7 identity that authenticated the tenant session, which can be a tenant shadow identity or a linked consumer identity;sub: the canonical tenant member/client ID;name: the tenant member's stable handle;org: the single fenced tenant context, containing only its ID and stable slug; andgroups: optional active group memberships encoded as sorted, comma-delimitedgroup-slug:rolepairs.
The groups claim is omitted when the member has no active group membership.
Group slugs and roles cannot contain , or : while this compact format is in
use. Organization and membership status are checked when M7 issues or refreshes
the token; they are not repeated in the payload.
A tenant token must never satisfy consumer/control-plane authorization, even
when its id, name, email, or linked account corresponds to a consumer.
OAuth-application access token
{
"iss": "https://sso.user.m7.org",
"typ": "access",
"principal_type": "oauth_client",
"id": "OAUTH_CLIENT_ID",
"sub": "OAUTH_CLIENT_ID",
"client_id": "OAUTH_CLIENT_ID",
"org": "TENANT_OR_NIL_UUID",
"m7": {"target_client_id": "OAUTH_CLIENT_ID"},
"org_name": "ORGANIZATION_NAME",
"client_name": "APPLICATION_NAME",
"ogp": "ISSUING_CREDENTIAL_UUID",
"aud": ["https://api.example.com", "OAUTH_CLIENT_ID"],
"scope": "files.read files.write",
"iat": 1787000000,
"nbf": 1786999995,
"exp": 1787000900,
"jti": "TOKEN_SESSION_UUID"
}
OAuth-application tokens are issued by client_credentials and represent the
application itself, never a human login. In the current profile, id, sub,
and client_id all identify the OAuth application. The org claim is a UUID
string, not the object used by tenant-member tokens; a personal application can
carry the nil UUID. org_name and optional client_name are context metadata.
ogp is the M7 issuing-credential reference and must not be interpreted as a
human subject.
Application policy can permit additional configured claims. M7-owned claims
win over requested or configured values. An administrative application can
also receive the explicit machine_owner_proxy = true extension; a resource
must reject that mode unless its route policy specifically supports owner
proxy authority.
Validation requirements
Before using any of these claims, a resource server must:
- validate the JWS signature using the key selected by the protected
header's
kid; - require the current signed-token issuer and the resource's exact audience;
- apply any explicit requester policy to
client_idand receiving-app policy to the nestedm7.target_client_id; never treat the requester as the target; - require the expected signed token form and
principal_typefor the route, and bind that form to the requested validation method and authoritative server-side token row; - validate
exp,nbf, granted scopes, and any applicable sender constraint; - enforce profile-specific tenant, role, group, or application policy; and
- perform an online active-state or revocation check when the resource's risk model requires one.
See Discovery and signing keys for M7's per-key lookup model and issuer distinction. Decoding a JWT without completing these checks does not validate it.