Skip to Content
DocsTokens & sessions

Tokens

🔑 Minted by: POST /api/auth/oidc/token · 🎫 Three tokens per login: access, ID, refresh · 📏 Rule of thumb: access token → call APIs, ID token → establish the login, refresh token → renew quietly

Every successful Authorization Code exchange returns three tokens in one top-level JSON. Each has exactly one job — most integration bugs come from using one token for another’s job.

TokenLifetimeJob
Access token~15 minutes (expires_in ≈ 900)Call Valyd resource APIs as the user
ID tokenValidated once at login (exp ≈ 15 min)Prove who logged in, to your backend
Refresh token30 days, rotates on every refreshMint new access tokens without the user

Login sessions

Together, these three tokens are a login session — a signed-in user your backend holds. There is no separate session object to create: your backend keeps the access token (to call APIs, ~15 min) and the rotating refresh token (to renew quietly, 30 days), usually mirrored by your own app session cookie. The login lasts as long as you keep refreshing — up to 30 days per rotating refresh token — and ends at logout, on refresh-token theft detection, or after 30 days of silence.

🧭 One word, two things. A login session (this page) is unrelated to a verification session (one person’s run through a check, ending in a decision). “Session expired” from a resource API means refresh the access token; EXPIRED from the decision API means create a new verification session. A dead login session never invalidates a verification result, and a finished verification session never logs anyone in.

Access token

Sent as Authorization: Bearer … to /userinfo, /licenses, /verifications, and to the Verification API as valyd_access_token for account-connected checks. It’s scope-gated: it can only reach what the user approved on the consent screen.

Decoded example payload (illustrative):

{ "iss": "https://idp.valyd.work", "sub": "valyd_f895da61d5174b81b8dd6a4e3b417339", "aud": "YOUR_CLIENT_ID", "iat": 1755600000, "exp": 1755600900, "scope": "openid profile verifications" }

Use it for: calling Valyd APIs on the user’s behalf; attaching to a verification session so the proof saves to their account.

Never use it for: identifying the user in your app (that’s the ID token’s job), or storing long-term — it dies in ~15 minutes; refresh instead.

Treat it as opaque. Its internal format is Valyd’s to change. Don’t parse it, don’t build logic on its claims — pass it in the Authorization header and let the API validate it.

ID token

An RS256-signed JWT — the login receipt. Your backend validates it once at login and uses its claims to create your own session.

Decoded example payload (illustrative):

{ "iss": "https://idp.valyd.work", "sub": "valyd_f895da61d5174b81b8dd6a4e3b417339", "aud": "YOUR_CLIENT_ID", "iat": 1755600000, "exp": 1755600900, "nonce": "RANDOM_NONCE_FROM_AUTHORIZE", "name": "John Doe", "preferred_username": "john.doe", "id_verified": true }

Claim notes: sub is the stable valyd_… id — use it as your primary key; aud must equal your client_id; nonce must equal the value you sent on /authorize (replay protection); id_verified tells you the account passed identity verification.

Use it for: establishing the login on your backend, keying the user by sub, and later as the id_token_hint on logout.

Never send it to an API. It is not an access credential — Valyd endpoints will reject it, and an ID token accepted as an API credential anywhere is a security bug. It also never belongs in a URL or in browser storage.

Always validate before trusting: signature (RS256/JWKS), iss, aud, exp, nonce. An unvalidated ID token is just attacker-writable JSON.

Refresh token

An opaque string (rfrsh_… — not a JWT, nothing to decode) held only on your backend:

{ "refresh_token": "rfrsh_abc123…", "what_you_can_read_from_it": "nothing — it is an opaque credential, not a JWT" }

Use it for: minting a new access token at the token endpoint with grant_type: "refresh_token", from your backend, with your client credentials.

Never use it for: calling APIs, or anywhere client-side. It’s the longest-lived credential in the system — treat it like a password.

Rotation is on. Every refresh revokes the token you sent and returns a new one — persist the new value every time. Replaying a rotated-away token is treated as theft and revokes the user’s entire refresh-token family for your client. Full mechanics: Refresh & logout flow.

Validating tokens

Let a library do it. The @valyd/sdk handleCallback() / exchangeCode() verify the ID token’s RS256 signature against discovery/JWKS plus issuer, audience, expiry, and nonce before returning. Any standard OIDC library pointed at https://idp.valyd.work/api/.well-known/openid-configuration does the same.

Validating manually (no SDK): fetch the signing keys from the JWKS at https://idp.valyd.work/api/auth/oidc/jwks.json, verify the RS256 signature, then check iss === "https://idp.valyd.work", aud === your client_id, exp in the future, and nonce === the value you sent. Never accept alg: "none" or an unexpected algorithm.

Last updated on