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; and
  • role: 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; and
  • groups: optional active group memberships encoded as sorted, comma-delimited group-slug:role pairs.

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:

  1. validate the JWS signature using the key selected by the protected header's kid;
  2. require the current signed-token issuer and the resource's exact audience;
  3. apply any explicit requester policy to client_id and receiving-app policy to the nested m7.target_client_id; never treat the requester as the target;
  4. require the expected signed token form and principal_type for the route, and bind that form to the requested validation method and authoritative server-side token row;
  5. validate exp, nbf, granted scopes, and any applicable sender constraint;
  6. enforce profile-specific tenant, role, group, or application policy; and
  7. 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.