Skip to content
TDACorpIDENTITYSign in

ID token

What the ID token proves, every claim it can carry, and the exact steps to verify it before you start a session.

On this page

The ID token is a signed statement that a person signed in, to your client, just now. It is for your application only. It is not a credential to present to an API: use the access token for that.

What it is#

A JWT with header typ: JWT, an alg of EdDSA or ES256 and a kid. Identity returns it from the token endpoint on the code grant when the openid scope was granted. A refresh response normally carries no ID token, so do not depend on one.

Its lifetime is the same as the access token's, at most 15 minutes (an operator can lower that ceiling). It is a record of an authentication that already happened, so a long life would add nothing.

Claims#

ClaimMeaning
issThe issuer. Must equal the issuer you configured.
subThe person's stable identifier. Key accounts on this. For a client registered with the pairwise subject type it is a value specific to that client's sector, not the person's real identifier.
audYour client id, as a single string.
exp, iatExpiry and issue time, seconds since the epoch.
auth_timeWhen the person last authenticated.
nonceThe value you pushed. Present when you sent one, which is always.
acrThe assurance level reached: aal1, aal2 or aal3. See acr and amr values.
amrThe methods used.
sidThe identity session. Needed to match a back-channel logout.
verThe claim-set version of identity's tokens. Currently 2.
tid, tslugThe organization and its slug, when the sign-in was for an organization.
Profile claimsname, locale, zoneinfo, preferred_username, picture and updated_at with the profile scope. email and email_verified with the email scope. A claim the person has not set is left out. A pairwise client gets no picture.

There is no azp, no at_hash and no roles. Every claim is defined in the claims reference.

Verify it#

Do every step. Skipping one is how sign-in bugs become account takeovers. The SDK performs all of them in handleCallback, and rp.verifyIdToken repeats them for a stored token.

  1. Size and shape. Refuse a value that is not a compact JWS, or is unreasonably large. The SDK refuses more than 16 KiB.
  2. typ is JWT. An access token (at+jwt) or a logout token (logout+jwt) must never pass as an ID token.
  3. alg is on your allow-list, and is the algorithm you import the key under. Take the allow-list from your own configuration, never from the token. The SDK default is EdDSA.
  4. kid is present. Find that key in the JWKS. If it is not there, refetch once.
  5. The signature verifies with that key.
  6. iss equals your configured issuer exactly.
  7. aud equals your client id. If it is a list, azp must equal your client id as well.
  8. exp is in the future and iat is not in the future, within a small clock tolerance. The SDK allows 60 seconds. sub must be present and non-empty.
  9. nonce equals the one you stored for this browser's transaction.
  10. auth_time is right. If you pushed max_age, auth_time is required and must not be older than max_age plus tolerance. It must not be in the future.
  11. acr reaches what you asked for. If you pushed acr_values, the token's level must be at least the lowest you listed.

Only after all of that, create your own session from sub.

Worked example#

Decoded, the payload of a verified ID token looks like this.

{  "iss": "https://identity.tdacorp.in",  "sub": "0194f2b6-7c1e-7a30-9d4e-2f6f0f3b8c11",  "aud": "tdaid_c_example",  "exp": 1790000600,  "iat": 1790000000,  "auth_time": 1789999950,  "nonce": "n-0S6_WzA2Mj",  "acr": "aal2",  "amr": ["hwk", "user"],  "sid": "0194f2b6-0000-7000-8000-000000000001",  "ver": 2,  "name": "Asha Rao",  "email": "asha@example.com",  "email_verified": true}

The values are illustrative. With the SDK:

const { tokens, idToken } = await rp.handleCallback(new URL(request.url), transaction)// idToken.sub, idToken.sid, idToken.acr, idToken.amr, idToken.authTime

Errors#

The SDK throws an IdTokenError whose reason names the failed step: typ, alg, kid_missing, signature, iss, aud, azp, exp, iat, sub, nonce, auth_time, max_age, acr, malformed, or a key-set reason when the key could not be found or the set could not be fetched. When verification fails, no tokens are returned to you, and the refresh token the exchange produced is revoked.

Security notes#

  • Never skip nonce. It ties the token to the browser that started the login. Your stored transaction must be tamper-proof or the check proves nothing.
  • Do not send an ID token to an API. Its audience is your client, not the API. A resource server must refuse it, and yours will.
  • Keep it for sign-out. The ID token is the id_token_hint for end session. An expired one still works as a hint.
  • Do not treat claims as current. An ID token is a snapshot at sign-in. For a fresher profile use userinfo.

Verified against commit ca46b57