# 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](https://m7.org/docs/api/sso.user.m7.org/response-modes.md#signed-authorization-responses-jarm)
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](https://m7.org/docs/api/sso.user.m7.org/response-encryption.md) 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](https://m7.org/docs/api/sso.user.m7.org/discovery-and-keys.md#signing-profiles).
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](https://m7.org/docs/api/sso.user.m7.org/registration.md#hmac-token-signing).
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](https://m7.org/docs/api/sso.user.m7.org/discovery-and-keys.md#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](https://m7.org/docs/api/sso.user.m7.org/logout-notifications.md).

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):

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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](https://m7.org/docs/api/sso.user.m7.org/discovery-and-keys.md) for M7's per-key
lookup model and issuer distinction. Decoding a JWT without completing these
checks does not validate it.
