Skip to content

Identity and tokens

Chat accepts one thing: a bearer token issued by the Identity Server for the Chat API. There are two kinds, one for your signed-in users and one for your backend. This page explains both, how a user gets theirs, and the one naming trap that causes most first-day errors.

User token Machine token
Acts as The signed-in user (sub is the user) Your backend client, not a user
Issued by Token exchange in your backend The client_credentials grant
Audience (aud) chat-api chat-api
Scope chat:access chat:sync or granular scopes such as chat:messages.send
What it may do What the user’s Chat permissions allow, in conversations they belong to What its scopes allow, in your own tenant and application only
Used from Your web or mobile app Your backend only

Chat reads only Authorization: Bearer <token>. It checks the signature, issuer, audience, expiry and scope, then checks the context in the URL. A missing or invalid token is a 401 invalid_token. A route that wants the other kind of token answers 403 principal_not_allowed. A missing scope answers 403 insufficient_scope.

A scope only opens the door. What a user may do comes from their Chat permissions, see Chat permissions. What a machine may do comes from its scopes, see Machine scopes.

The Chat API is a resource at the Identity Server, and it has two names. Mixing them up is the most common mistake.

Value Example Used by For
Resource identifier https://chat.centralcodestation.com/api Your backend resource= on the token request: which API the token is for
Audience chat-api Chat only Checking the token’s aud claim

The Identity Server looks the resource up by its identifier and writes the audience into the token. Asking for resource=chat-api fails with invalid_target. Your code never reads aud, so it needs only the identifier. A token you got for your own API has a different aud and Chat rejects it.

One token request yields a token for one resource. Your app already holds a token for your own API, so your backend swaps it for a Chat token with OAuth token exchange (RFC 8693). The app needs no redirect, no second sign-in and no extra OAuth client.

Your app --(your API token)--> Your backend GET /chat-token
Your backend --token exchange (backend client, resource=Chat API,
scope=chat:access)--> Identity Server
Your backend <-- user token: sub = the user, aud = chat-api, no refresh token
Your app <-- { access_token, expires_in }
Your app --(Bearer user token)--> Chat API

How it behaves:

  • The user must have signed in. The Identity Server will not mint a token for an arbitrary user id. The subject token must be your own API’s access token, not an ID token.
  • Short lifetime. An exchanged token lives at most 10 minutes, and never longer than the user’s own token.
  • No refresh token. Renew by exchanging again. Your app caches the token and asks your route again about a minute before it expires, so the Identity Server is not called per request.
  • Ordinary to Chat. The token’s client_id is your backend client, and it also carries an act claim that Chat ignores. Chat checks permissions and membership on every call, as for any user token.
  • No DPoP. Chat accepts only bearer tokens and verifies no proofs. Leave DPoP out of the exchange.
  • Revocation. Signing out, disabling the user or removing them from the tenant stops further exchanges. A token already issued works until it expires, which is why it is short. Chat Administration changes can also answer 401 token_revoked.

Realtime and push use the same token. Your getAccessToken function is called again for each new realtime token and each push registration.

A token issued to a client of your tenant application works only in your tenant and application. Any other context answers 403 client_context_mismatch. This applies to machine tokens and to exchanged user tokens alike. See Tenants and applications.

Chat’s own web client signs users in with its own routes. Tenant applications never call them.