Skip to content
TDACorpIDENTITYSign in

Access token

The access token's format, lifetime and audience, and the checks your API makes before it trusts one.

On this page

An access token is what a client presents to an API. It is not for the client to read: your application treats it as opaque and sends it. The API behind it reads it. This page is for the API.

Format#

A JWT conforming to RFC 9068. The header carries typ: at+jwt, the signing alg (EdDSA or ES256, the algorithm registered for the client that asked) and a kid. The payload is below. A refresh token, by contrast, is opaque and is never a JWT.

ClaimNotes
iss, sub, aud, exp, iat, nbf, jtiAlways present.
client_idThe client the token was issued to.
scopeSpace-separated granted scopes.
verClaim-set version. Currently 2.
kindThe kind of caller: human, service, workload or agent.
av, pv, pvt, rvAuthorization version counters. rv is the organization's resource version and is present only with tid.
azpWhen the token has a resource audience or came from an exchange: the client that asked.
tid, tslugWhen the sign-in was for an organization, and on a client_credentials token for a service client linked to an organization.
sid, acr, amr, auth_timeFor a token issued for a person. Not on a client_credentials token.
cnfThe key the token is bound to. See DPoP verification.
actOn an exchanged token: who is acting for whom.
authorization_detailsEchoed from the request, when one was made.

All claims are defined in the claims reference. There is no roles claim; ask the decision API what a person may do.

Lifetime#

exp minus iat is at most 900 seconds (15 minutes), and is set per client at or below that. A client_credentials token has the client's access token lifetime like any other: 600 seconds by default, at most 900. A client cannot be registered with a longer life, and an operator can lower the ceiling for the whole deployment, so read exp rather than assuming 900. Refresh by the refresh grant before expiry, or when an API answers 401.

Audience#

A token has exactly one aud, a single string, never a list.

How the token was requestedaud
NormallyThe client id of the client that asked.
With a resource parameterThat one registered resource address, and azp is the client id.
By token exchangeThe client id of the service being called.

The resource's product must still be installed for the organization when the token is minted, at the code exchange and at every refresh, not only when the request was authorized. A token request names at most one resource. To reach several resources, ask once for each. Your API accepts a token only when aud is its own identifier: the resource address it is registered under, or its client id. A token for any other audience is refused, even if the signature is perfect. That is the check that stops one service replaying a token meant for another.

Verify the token in your API#

The SDK's createResourceServer does all of this. See Resource-server verification. To do it yourself:

  1. Read the Authorization header. The scheme is DPoP for a bound token and Bearer otherwise.
  2. Decode the header. typ must be at+jwt and alg must be on your allow-list: EdDSA and ES256, or just the one your tokens use. Never accept none, an HS algorithm or RS256. Take the algorithm from your configuration, not the token.
  3. Find the key by kid in the JWKS and verify the signature.
  4. iss equals the issuer you trust, exactly.
  5. aud is a single string and equals your own identifier.
  6. exp is in the future and nbf is not. Allow a few seconds of clock difference (the SDK's default is 5 seconds, and it refuses a setting above 300). exp minus iat is at most 900.
  7. jti is present. sub is a non-empty string.
  8. If the token has cnf.jkt, the request must use the DPoP scheme with a valid proof for this exact request and key; see DPoP verification. A bound token sent as Bearer is refused. If it has no cnf, the DPoP scheme is refused.
  9. Check that scope includes what the route needs.
  10. Only then act on sub, tid and the rest.

Verification is local and fast, and it cannot see what happened since the token was issued: a revocation, an ended session or a deactivated person. For a sensitive action, or if you need to react inside the 15 minutes, ask introspection.

Worked example#

Decoded header and payload of a token issued for your API.

{ "typ": "at+jwt", "alg": "EdDSA", "kid": "ex-2026-10" }
{  "iss": "https://identity.tdacorp.in",  "sub": "0194f2b6-7c1e-7a30-9d4e-2f6f0f3b8c11",  "aud": "https://api.example.com",  "azp": "tdaid_c_example",  "client_id": "tdaid_c_example",  "jti": "kq3T0u1mZb0Yx2yV7h1m6Q",  "iat": 1790000000,  "nbf": 1790000000,  "exp": 1790000600,  "scope": "openid profile",  "ver": 2,  "tid": "0194f2b6-1111-7000-8000-00000000aaaa",  "tslug": "acme",  "sid": "0194f2b6-0000-7000-8000-000000000001",  "acr": "aal2",  "amr": ["hwk", "user"],  "auth_time": 1789999950,  "av": 7,  "cnf": { "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I" },  "kind": "human"}

The values are illustrative.

Errors#

On refusal answer 401 with a WWW-Authenticate challenge and {"error": "invalid_token"}, or invalid_dpop_proof for a proof failure. Do not say which claim failed. If the token is valid but its sign-in is too old or too weak for the route, answer 401 with insufficient_user_authentication and the acr_values and max_age you need (RFC 9470); the SDK's requireAuthentication does this and a client answers it with rp.stepUpUrl, see Resource-server verification. Answer 403 with insufficient_scope when the token is valid but lacks a scope.

Security notes#

  • Never accept an ID token as an access token. Its typ is JWT and its audience is a client; both checks refuse it.
  • Never accept a token for another audience because it came from the same issuer.
  • Treat sub as an opaque stable key, not an email address. On an access token it is always the person's real identifier, even for a client that sees a pairwise sub in its ID token (claims reference).
  • Do not log tokens. They are credentials until they expire.
  • Refuse a token with no exp or too long a life. A token with exp minus iat over 900 cannot have come from identity.

Verified against commit ca46b57