Skip to Content
Valyd IDLogin sessions (CSRF)

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 valyd client, 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 valyd client must already be configured with credentials).
  • Prerequisites:
    • A configured valyd client (new ValydClient({...})) from @valyd/sdk (latest).
    • Server-side storage available for the marker (httpOnly cookie, encrypted session, or KV store).

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 valyd client instance from @valyd/sdk (latest), constructed with your clientId, clientSecret, and redirectUri (get the client ID and secret from the Valyd developer portal → your project → Credentials).
  • Server-side storage for the marker: an httpOnly cookie, an encrypted server session, or a KV store. The marker must never be exposed to client-side JavaScript.

Steps

  1. Before redirecting the user to Valyd, create a login session and store the marker. createLoginSession() returns an authorizeState and an HMAC-signed marker. Store the marker server-side (here, an httpOnly cookie with a 10-minute lifetime to match the marker TTL), then redirect to the authorize URL using session.authorizeState as state.

    // 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>" }. The valyd_login cookie is set with the marker, and the browser is redirected (HTTP 302) to the Valyd authorize URL.

  2. 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 to exchangeCode(). 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 call exchangeCode(). IF you are unsure whether the marker is valid: → call await valyd.verifyLoginSession(marker) and branch on the returned valid boolean (it never throws).

Marker / login-session properties

PropertyDetails
Marker formatHMAC-signed string. Signed with your client secret on Valyd’s side.
TTL10 minutes. After that, verifyLoginSession returns { valid: false }.
StorageServer only — httpOnly cookie, encrypted session, or KV. Never expose to JS.
Return valueverifyLoginSession returns { valid: boolean }. It never throws on an invalid marker.
When to verifyOn 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 to exchangeCode().
  • Negative path: clear or wait out the valyd_login cookie (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 state comparison for CSRF.

    • Cause: Valyd substitutes its own opaque session id for the state on 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 the marker server-side, and verifyLoginSession(marker) on the callback.
  • verifyLoginSession returns { 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 * 1000 to match the TTL, ensure it is httpOnly and 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-httpOnly cookie, localStorage), which undermines the CSRF protection.
    • Fix: Store the marker server-side only — an httpOnly cookie, an encrypted server session, or a KV store. Never expose it to JS.