Login sessions (CSRF protection)
Agent Quick-Start
- Source URL: https://docs.valyd.work/docs/login-sessions
- Credentials / env vars needed: none directly (relies on the configured
valydclient, which uses your client ID/secret — get these from the Valyd developer portal → your project → Credentials) - Files an integrator edits: server route handlers (the login redirect route and the callback route)
- Estimated steps: 2
- Can complete without human input: YES — this is a code-only CSRF mechanism using SDK methods; no portal action is required for the mechanism itself (though the
valydclient must already be configured with credentials). - Prerequisites:
- A configured
valydclient (new ValydClient({...})) from@valyd/sdk(latest). - Server-side storage available for the marker (httpOnly cookie, encrypted session, or KV store).
- A configured
The classic OAuth CSRF check — generate a random state, then compare what the IdP echoes — does NOT work for Valyd TPSSO, because Valyd returns its own session id on the callback. The SDK ships with a purpose-built mechanism: login sessions.
The problem: Comparing the callback state against the value you sent will always fail — Valyd substitutes its own opaque session id on the redirect back.
The solution: Call createLoginSession() before the redirect and store the marker server-side. On the callback, call verifyLoginSession(marker).
Prerequisites
- A configured
valydclient instance from@valyd/sdk(latest), constructed with yourclientId,clientSecret, andredirectUri(get the client ID and secret from the Valyd developer portal → your project → Credentials). - Server-side storage for the marker: an
httpOnlycookie, an encrypted server session, or a KV store. The marker must never be exposed to client-side JavaScript.
Steps
-
Before redirecting the user to Valyd, create a login session and store the marker.
createLoginSession()returns anauthorizeStateand an HMAC-signedmarker. Store the marker server-side (here, anhttpOnlycookie with a 10-minute lifetime to match the marker TTL), then redirect to the authorize URL usingsession.authorizeStateasstate.// 1. Before redirecting the user to Valyd const session = await valyd.createLoginSession(); // → { authorizeState: "...", marker: "v1.<sig>.<payload>" } res.cookie("valyd_login", session.marker, { httpOnly: true, sameSite: "lax", secure: true, maxAge: 10 * 60 * 1000, // 10 minutes (matches marker TTL) }); res.redirect(valyd.getAuthorizationUrl({ state: session.authorizeState, scope: ["profile", "verifications"], }));Expected output:
createLoginSession()resolves to{ authorizeState: "...", marker: "v1.<sig>.<payload>" }. Thevalyd_logincookie is set with the marker, and the browser is redirected (HTTP 302) to the Valyd authorize URL. -
On the callback, verify the stored marker. Read the marker from your server-side storage and pass it to
verifyLoginSession(marker). It returns{ valid: boolean }and never throws on an invalid marker.// 2. On the callback const marker = req.cookies.valyd_login; const { valid } = await valyd.verifyLoginSession(marker); if (!valid) { // Expired login, missing cookie, or tampered marker. return res.status(400).send("Invalid login session"); }Expected output: On a legitimate login within the TTL,
verifyLoginSession(marker)returns{ valid: true }. Otherwise it returns{ valid: false }and this route responds HTTP 400 with body"Invalid login session".IF the marker cookie is present and within its 10-minute TTL and untampered: →
{ valid: true }; proceed toexchangeCode(). IF the marker is expired (older than 10 minutes), the cookie is missing, or the marker was tampered with: →{ valid: false }; respond HTTP 400"Invalid login session"and stop. Do NOT callexchangeCode(). IF you are unsure whether the marker is valid: → callawait valyd.verifyLoginSession(marker)and branch on the returnedvalidboolean (it never throws).
Marker / login-session properties
| Property | Details |
|---|---|
| Marker format | HMAC-signed string. Signed with your client secret on Valyd’s side. |
| TTL | 10 minutes. After that, verifyLoginSession returns { valid: false }. |
| Storage | Server only — httpOnly cookie, encrypted session, or KV. Never expose to JS. |
| Return value | verifyLoginSession returns { valid: boolean }. It never throws on an invalid marker. |
| When to verify | On the callback, before exchangeCode(). |
A full Express example is available in the SDK repo: https://github.com/valyd/idp-sdk/blob/HEAD/examples/express-login.ts
Verification
- Happy path: complete a real login. On the callback,
verifyLoginSession(marker)should return{ valid: true }and the request should proceed toexchangeCode(). - Negative path: clear or wait out the
valyd_logincookie (or modify a character in the marker), then hit the callback.verifyLoginSession(marker)should return{ valid: false }and the route should respond HTTP 400"Invalid login session".
Common errors
-
Using OAuth
statecomparison for CSRF.- Cause: Valyd substitutes its own opaque session id for the
stateon the callback, so comparing it to the value you sent always fails (and offers no protection). - Fix: Use the login-session mechanism:
createLoginSession()before the redirect, store themarkerserver-side, andverifyLoginSession(marker)on the callback.
- Cause: Valyd substitutes its own opaque session id for the
-
verifyLoginSessionreturns{ valid: false }for a legitimate user.- Cause: The marker expired (more than 10 minutes elapsed before the callback), the cookie was not sent back, or the marker was altered in transit/storage.
- Fix: Set the marker cookie with
maxAge: 10 * 60 * 1000to match the TTL, ensure it ishttpOnlyand returned on the callback, and have the user complete login within 10 minutes. Store the marker verbatim — do not re-encode or truncate it.
-
Exposing the marker to client-side JavaScript.
- Cause: Storing the marker somewhere readable by the browser (non-
httpOnlycookie, localStorage), which undermines the CSRF protection. - Fix: Store the marker server-side only — an
httpOnlycookie, an encrypted server session, or a KV store. Never expose it to JS.
- Cause: Storing the marker somewhere readable by the browser (non-