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_idclaim 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
}
| Claim | What it is |
|---|---|
sub | The user's email. |
uid | The user's stable numeric id — use this to key your own data, not sub. |
org_id / org_name / org_path | The active organization — the one this token acts as. |
tenant_id | The hard isolation boundary this org belongs to. |
scope_self | Paths this token may act on directly — always includes its own org. |
scope_subtree | Paths this token's reach extends below; empty if it doesn't. |
permissions | Codes granted at the active org, scoped to aud (system codes only on a management token). |
device_id | Present 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
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:
| Source | Shape |
|---|---|
permissions | List of codes, for the active org. |
roles | List of role names behind those codes, for the active org. |
memberships | List of { id, name, path } — every org the user belongs to. |
org_id / org_name / org_path | The active org. |
scope_self / scope_subtree | This token's reach. |
tenant_id, uid, sub, device_id | As 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/microsoftClientIdson 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.