Skip to main content

Audiences & tokens

An application is an audience​

Every access token is minted for a specific audience — the service that will verify it. In Latch Vector that audience is an application: you register one, give it an identifier (a URI by convention, e.g. https://api.yourcompany.com), and configure your services with that value as their SSO_AUDIENCE.

A token minted for https://api.acme.com is only valid at services that verify against https://api.acme.com. One application per service, or per group of services that trust the same tokens.

Access tokens​

  • Format: signed JWT, RS256. Verify with the JWKS the service publishes — the SDKs do this for you and pin the algorithm.
  • Lifetime: short (15 minutes). Don't try to extend it; refresh instead.
  • Claims: the subject (user), their active organization (not necessarily their home one — see switching organizations below), and the permissions granted to them there. A device_id claim is present for device sessions.
{
"token_use": "access",
"uid": 8123,
"org_id": 57,
"org_name": "Cardiology",
"tenant_id": 1000042,
"org_path": "/1000042/57/",
"scope_self": ["/1000042/57/"],
"scope_subtree": ["/1000042/57/"],
"permissions": ["USER_MANAGE", "invoice.approve"],
"aud": "https://api.acme.com",
"iss": "https://sso.yourcompany.com",
"exp": 1735689600
}
ClaimWhat it is
subThe user's email.
uidThe user's stable numeric id — use this to key your own data, not sub.
org_id / org_name / org_pathThe active organization — the one this token acts as.
tenant_idThe hard isolation boundary this org belongs to.
scope_selfPaths this token may act on directly — always includes its own org.
scope_subtreePaths this token's reach extends below; empty if it doesn't.
permissionsCodes granted at the active org, scoped to aud (system codes only on a management token).
device_idPresent only on a device-bound (mobile) session.

scope_self / scope_subtree are what a resource server checks against a resource's own org path — a single indexed prefix comparison, not a call back to Latch Vector. Never trust anything else in the payload as an access-control input.

Switching organizations​

A person belonging to more than one organization gets a token scoped to their home org at login. To act as another one they belong to:

POST /api/users/me/active-organization
{ "organizationId": 903, "audience": "https://api.acme.com" }

returns a fresh token pair scoped to that org. GET /api/users/me/memberships lists the organizations available to switch into. The switch is remembered on the refresh-token session, so refreshing afterwards keeps the same active org rather than reverting to home.

Custom token claims​

Regulated plan

This is part of the by-arrangement plan tier — contact us to enable it.

Migrating a front end that was built against another identity provider? Add your own claim names to every access token, alongside the standard ones above — never replacing them, so nothing that already verifies a token has to change.

Console: Applications → Token claims. Or via the API:

GET /api/token-claims # the stored mapping
PUT /api/token-claims # save it — { mapping, enabled }
POST /api/token-claims/preview # what it would add to *your own* token

Only the organization that owns the product may set this — on a white-labelled branch it is the product owner's to define, same as the applications and the permission vocabulary themselves (see White-labeling).

A mapping entry copies one standard claim under a new name, gathers several into one array, or writes a constant:

{
"add": [
{ "key": "realm_access.roles", "from": "roles" },
{ "key": "organisations", "from": "memberships" },
{ "key": "tenant", "value": "acme" }
]
}

key may use . to nest (realm_access.roles becomes an object). Sources you may read from:

SourceShape
permissionsList of codes, for the active org.
rolesList of role names behind those codes, for the active org.
membershipsList of { id, name, path } — every org the user belongs to.
org_id / org_name / org_pathThe active org.
scope_self / scope_subtreeThis token's reach.
tenant_id, uid, sub, device_idAs in the standard claims above.

A standard claim name (sub, permissions, org_id…) can never be written to — the compiler refuses it outright, which is what keeps every SDK working unchanged. roles and memberships cost an extra lookup only when a saved mapping actually references them, so a tenant without this feature — or one whose mapping doesn't use either — pays nothing extra per login. That lookup is also why POST /preview shows neither of them: preview reads only what is already on your own live token, so a mapping that reads roles or memberships previews as if that source were absent.

The management token​

Some calls (creating users, roles, applications…) are management calls. They use an ordinary user access token minted with the audience omitted — you get one by logging in with audience set equal to the issuer:

const sso = new SsoClient({ issuer, audience: issuer }); // aud omitted on the wire
const { tokens } = await sso.login(serviceEmail, servicePassword);
const mgmt = new ManagementClient({ issuer, token: tokens.accessToken });

The signed-in account needs the relevant permissions (USER_MANAGE, ORG_MANAGE, ROLE_MANAGE, CLIENT_MANAGE, AUDIT_VIEW). Use a dedicated service account, not a person.

Refresh tokens​

A refresh token is an opaque selector.verifier string — not a JWT. It is:

  • Single-use and rotating — every refresh returns a new one and kills the old.
  • Reuse-detected — replaying a rotated token is treated as a compromise and revokes the whole session family (RFC 9700 containment).
  • Longer-lived than the access token — see Sessions & refresh.

Social login (Google / Microsoft)​

Your app runs its own Google or Microsoft sign-in (its own UI and OAuth client) and posts the resulting id_token to POST /api/auth/social/{provider} with audience set to your application's identifier. The SSO verifies the token against the provider's keys and resolves it to a pre-provisioned user — it never runs the OAuth flow itself, so there is no client secret to store.

To stop your app trusting a token minted for some other app, register the OAuth client ID(s) your id_tokens are issued for, per application:

  • In the console: Applications → Manage → Social login — paste your Google and/or Microsoft client ID(s) (comma-separated).
  • Via the API/SDK: googleClientIds / microsoftClientIds on create/update application.

A social login to your app is then accepted only if the id_token's aud names one of your client IDs. Leave them blank to fall back to the platform default. Client IDs are public identifiers, not secrets.

Machine-to-machine​

For server-to-server access with no user, register an API client and use the OAuth 2.0 client credentials grant at POST /oauth2/token. You get an access token scoped to the client's granted scopes.

Bind the client to an application (applicationId) and its aud is that application's identifier, so a resource server verifies it exactly as it verifies a user token; without one, aud falls back to the client id. Step by step: Machine-to-machine.