Skip to Content
ReferenceJWT claim catalog

JWT claim catalog

Every claim Synq emits on access tokens, ID tokens, m2m tokens, and device-flow tokens. The complete shape for each token type.

This is the page to read when implementing a custom verifier or adding scope-based authorization to your code.

Access token (access_token)

Issued by every grant type. Audience is the brand slug.

interface AccessTokenPayload { // Required claims sub: string // user id (m2m: client_id) aud: string // brand slug iss: string // 'https://synq.id' scope: string // granted scopes, space-separated iat: number // issued-at, unix seconds exp: number // expires-at, unix seconds // Always present on user-authed tokens azp: string // client_id that obtained the token // Present when the user authenticated auth_time?: number // unix seconds — when user authed // Present on device-flow access tokens act?: { type: 'agent' client_id: string // the agent app's client_id } }

Claim meanings

  • sub — the subject. For user-authed tokens, this is the Synq user id. For m2m tokens (client_credentials grant), this is the client_id (no user is involved).
  • aud — the audience. For access tokens, this is the Brand slug. The Brand slug lets the same backend service accept tokens from any App under the same Brand (web + mobile + CLI share the brand audience).
  • scope — space-separated granted scopes. Always includes whatever the App requested ∩ what the user consented to.
  • azp — authorized party. The client_id of the App that obtained this token. Useful when one backend serves multiple Apps under the same Brand and needs to know which App is calling.
  • auth_time — when the user actually authenticated. Useful for “step-up” authorization (require re-auth for sensitive actions if auth_time is older than N minutes).
  • act — actor. Present only on device-flow tokens. Lets your backend differentiate “user signed in directly” from “agent signed in on behalf of user via device flow”, and apply tighter scope policies for the latter if desired.

Verifying

import { jwtVerify, createRemoteJWKSet } from 'jose' const JWKS = createRemoteJWKSet(new URL('https://synq.id/.well-known/jwks.json')) const { payload } = await jwtVerify<AccessTokenPayload>(token, JWKS, { issuer: 'https://synq.id', audience: ['marketplace', 'admin-tools'], // accept multiple clockTolerance: '5 seconds', }) if (!payload.scope?.split(' ').includes('wallets:read')) { throw new Error('forbidden') }

ID token (id_token)

Issued alongside the access token when openid is in the requested scope. Audience is your client_id.

interface IdTokenPayload { // Required sub: string // user id (same as access_token.sub) aud: string // your client_id (different from access aud) iss: string // 'https://synq.id' iat: number exp: number auth_time: number // Returned to round-trip nonce — VERIFY THIS nonce?: string // 'email' scope email?: string email_verified?: boolean // 'profile' scope given_name?: string family_name?: string preferred_username?: string picture?: string // URL locale?: string // BCP-47, e.g. 'en-US' // Brand-specific claims (only when an explicit brand scope grants them) // Example: a brand-defined 'wallets:profile' scope might add: // wallets?: Array<{ address: string, blockchain: string, isPrimary: boolean }> }

Verifying nonce

The ID token’s nonce claim must match the nonce you generated in step 2 of the Quickstart:

import { jwtVerify, createRemoteJWKSet } from 'jose' const JWKS = createRemoteJWKSet(new URL('https://synq.id/.well-known/jwks.json')) const { payload } = await jwtVerify<IdTokenPayload>(idToken, JWKS, { issuer: 'https://synq.id', audience: process.env.SYNQ_CLIENT_ID, // your client_id, NOT brand slug }) if (payload.nonce !== savedNonce) { throw new Error('nonce mismatch — possible replay') }

Differences from access_token

access_tokenid_token
audbrand slugyour client_id
azpyesno
noncenoyes (when sent)
Profile claimsnoyes (when scopes granted)
Usageauthorize backend callsrender the user; capture email
Use caserepeated, every requestone-shot, at sign-in

m2m access token (client_credentials grant)

Same shape as access token, with:

  • sub === client_id (no end user)
  • No auth_time, no act
  • No id_token issued (m2m clients don’t get profile info)
  • No refresh_token issued

The scope claim contains the scopes the m2m client requested and that are allowed on its App configuration.

interface M2mAccessTokenPayload { sub: string // client_id aud: string // brand slug iss: string // 'https://synq.id' azp: string // client_id (same as sub for m2m) scope: string iat: number exp: number }

Distinguishing m2m from user tokens

A common pattern: your backend needs to know if a token is m2m (internal service) or user (front-end app). Check whether sub equals azp — m2m tokens always have them equal; user tokens always have sub = user_id and azp = client_id.

function isM2mToken(payload: AccessTokenPayload) { return payload.sub === payload.azp }

Or check auth_time — m2m never has it.

Device-flow access token

Same shape as access token, with act present:

{ sub: 'user_abc', aud: 'marketplace', azp: 'cli-app', scope: 'openid profile', iat: 1717000000, exp: 1717003600, auth_time: 1717000000, act: { type: 'agent', client_id: 'cli-app' } }

Use act to apply tighter scope policies for agent-issued tokens. For example, your backend might reject act tokens from writing sensitive data unless the user re-authenticated within the last 5 minutes (Date.now() - auth_time*1000 < 5*60*1000).

JWT header (for completeness)

Every Synq JWT has this header:

{ alg: 'RS256', typ: 'JWT', kid: '<key id — matches one in JWKS>' }

Reject tokens whose alg is not RS256. Synq does not issue HS256 or RS512 tokens.

Custom claims via brand scopes

A brand can register custom scopes (see Scopes and consent). Granted brand scopes appear in the access token’s scope claim. They do not add new top-level claims — your backend reads scope and decides what each one means.

If you need additional data shipped in the token itself (e.g. a wallet address as a top-level claim), open a support request — that’s configurable on the brand and not exposed in the public API yet.

Mental model summary

  • sub = subject. User id (or client_id for m2m).
  • aud = audience. Brand slug on access tokens; your client_id on ID tokens.
  • azp = authorized party = client_id that obtained the token.
  • scope = granted scopes, space-separated.
  • act = present on device-flow tokens; identifies agents.
  • nonce = present on ID tokens; verify against what you sent.
  • Always RS256, always kid references JWKS.
Last updated on