Skip to Content
DocsAdd Connect with Valyd

Add Connect with Valyd

🔑 Auth: client_id + client_secret (server-side) · 👤 Standard OpenID Connect — Connect can also serve as your sign-in · 📖 After connecting: read the account with a Bearer access token

Prefer the concept pages? See Flows for the picture-first walkthrough and Tokens for what each returned token is for.

These raw-HTTP examples demonstrate the protocol. For production, use a maintained OIDC library or @valyd/sdk — they validate issuer, audience, signature, expiry, state, nonce, and PKCE for you.

Prerequisites

  • Client ID and Client Secret (get these from the Developer Portal → your app → Credentials: https://dev.valyd.work  — the portal lists apps as “projects”; same object).
  • A registered redirect/callback URL matching what you send as redirect_uri.
  • Environment variables set on your server:

Steps

  1. Construct the authorization URL. Redirect users to the OIDC authorization endpoint with your client id, redirect URI, scopes, and a freshly generated random state (and nonce):

    https://idp.valyd.work/api/auth/oidc/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope={scopes}&state={state}&nonce={nonce}

    Parameters:

    ParameterRequiredDescription
    client_idYesYour application’s Client ID from the Developer Portal (https://dev.valyd.work ).
    redirect_uriYesThe URL to redirect to after authentication. Must match the URL registered on your application.
    response_typeYesMust be code.
    scopeYesSpace-separated list of scopes, URL-encoded. MUST include openid. Example: openid%20profile%20verifications.
    stateYesA random value you generate and store. Echoed back unchanged on the callback — compare it there (CSRF protection).
    nonceRecommendedA random value bound into the id_token. Verify the nonce claim after the exchange (replay protection).

    Expected output: A fully-formed URL string. Example with encoded scopes openid profile verifications:

    https://idp.valyd.work/api/auth/oidc/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/callback&response_type=code&scope=openid%20profile%20verifications&state=RANDOM_STATE&nonce=RANDOM_NONCE
  2. Create and store one secure transaction (recommended: SDK). The SDK generates strong state, nonce, and S256 PKCE values together. Store the returned object in a server-side session:

    import { ValydClient } from "@valyd/sdk"; const valyd = new ValydClient({ clientId: process.env.VALYD_CLIENT_ID!, clientSecret: process.env.VALYD_CLIENT_SECRET!, redirectUri: "https://yourapp.com/callback", }); const transaction = valyd.createAuthorizationRequest({ scope: ["profile", "verifications", "zkp"], // "openid" is added automatically }); req.session.valydOidc = transaction; // server-side only; includes the PKCE verifier res.redirect(transaction.url);

    Expected output: HTTP 302 to the OIDC authorize endpoint with state, nonce, and code_challenge. The full transaction stays server-side for step 6.

  3. User consents on the Valyd consent screen. Valyd shows the consent screen with the requested scopes and, on approval, issues a one-time authorization code. (Scopes must be enabled on your app in the Developer Portal before they can be requested — the same model as Google’s consent-screen scopes.)

    Expected output: Valyd redirects the user’s browser to your callback URL with the code attached. Codes are single-use and short-lived — exchange immediately.

  4. Receive the callback on your server. The user is redirected to your registered callback URL with the authorization code and your original state:

    https://yourapp.com/callback?code=AUTH_CODE_HERE&state=RANDOM_STATE

    Callback query parameters:

    ParameterDescription
    codeThe one-time authorization code. Single-use; exchange immediately.
    stateThe exact state value you sent on /authorize, echoed back unchanged. Compare it to your stored value.

    Expected output: Your /callback route is invoked with code and state (and possibly error) present in the query string.

  5. CSRF check — compare the state. The callback state must strictly equal the value you stored in step 2. Reject the request on any mismatch, before touching the code.

    const stored = readStoredOAuthValues(req); // your cookie/session read if (!stored?.state || req.query.state !== stored.state) { return res.status(400).send("state mismatch"); }

    Expected output: On a legitimate flow the values are identical and processing continues. On a mismatch (missing cookie, forged callback), respond HTTP 400 and stop.

  6. Exchange the code for tokens, then fetch the user (server-side). Exchange the code immediately (it is single-use). The callback handler in your stack — pick your language once and every code block on the page follows:

// Recommended: @valyd/sdk import { ValydClient } from "@valyd/sdk"; const valyd = new ValydClient({ clientId: process.env.VALYD_CLIENT_ID, clientSecret: process.env.VALYD_CLIENT_SECRET, redirectUri: process.env.VALYD_REDIRECT_URI, }); app.get("/callback", async (req, res) => { const transaction = req.session.valydOidc; if (!transaction) return res.status(400).send("login transaction missing"); delete req.session.valydOidc; const callbackUrl = new URL(req.originalUrl, process.env.VALYD_REDIRECT_URI).toString(); const { tokens, user } = await valyd.handleCallback(callbackUrl, { transaction }); // ...set your own app session, then redirect to /dashboard });

handleCallback() compares state, sends the PKCE verifier, exchanges the code, verifies RS256/JWKS plus issuer/audience/expiry/nonce, and fetches UserInfo.

Expected output: exchangeCode(code) returns tokens (accessToken, refreshToken, idToken, expiresIn, scope); getUserInfo(accessToken) returns the user’s profile data. Then set your own app session and redirect (e.g. to /dashboard).

Token exchange without the SDK (raw HTTP)

The token endpoint is POST https://idp.valyd.work/api/auth/oidc/token with a JSON body. The response is a standard top-level token JSONaccess_token, refresh_token, id_token, expires_in, scope, token_type at the root (no data wrapper).

Request shape:

POST /api/auth/oidc/token HTTP/1.1 Host: idp.valyd.work Content-Type: application/json { "grant_type": "authorization_code", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "code": "AUTH_CODE_HERE", "redirect_uri": "https://yourapp.com/callback" }

Send the same redirect_uri you used at /authorize — Valyd validates it against the code. Authorization codes are bound to the client they were issued to and are single-use — exchange them as soon as your callback fires.

Expected output: HTTP 200 with a top-level JSON body:

{ "access_token": "eyJhbGciOi...", "refresh_token": "rfrsh_abc123...", "id_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 900, "scope": "openid profile verifications" }

The id_token is an RS256-signed JWT — validate its signature against the JWKS at https://idp.valyd.work/api/auth/oidc/jwks.json and check that its nonce claim equals the nonce you sent on /authorize.

Renewing an access token

Access tokens are short-lived (expires_in ≈ 900 seconds). Exchange the refresh_token at the same token endpoint — POST https://idp.valyd.work/api/auth/oidc/token — from your backend, with your client credentials:

{ "grant_type": "refresh_token", "refresh_token": "rfrsh_abc123...", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET" }

The refresh token is validated against the client it was issued to, so a token leaked from one app cannot be used by another. Rotation is on: every refresh returns a new refresh_token and revokes the one you sent, so always persist the returned value. Replaying a rotated-away token is treated as theft and revokes every refresh token for that user and app.

With the SDK this is one call — const next = await valyd.auth.refreshToken(stored) — then persist both next.accessToken and next.refreshToken.

In all raw-HTTP examples, replace YOUR_CLIENT_ID and YOUR_CLIENT_SECRET with your real values (get these from the Developer Portal → your app → Credentials: https://dev.valyd.work ). Never expose YOUR_CLIENT_SECRET in client-side code — the token exchange must run on your server.

Auth-flow decision tree

IF the request just hit your login/start route: → generate random state + nonce (crypto.randomBytes), store them (httpOnly cookie / server session), build the URL with valyd.auth.getAuthorizationUrl({ state, nonce, scope }), and res.redirect(url). IF the request hit your /callback route: → read code, state, error from the query string IF error is set OR code is missing: → return HTTP 400 (error ?? "missing code"). STOP. IF code is present: → continue to the CSRF check below. CSRF check (do this on every callback): → compare req.query.state to the state you stored before the redirect IF they differ (or the stored value is missing): → return HTTP 400 "state mismatch". STOP. IF they match: → proceed to token exchange. Token exchange: → const tokens = await valyd.auth.exchangeCode(code) // must run server-side; codes are single-use IF the exchange returns invalid_grant: → the code expired or was already used; restart from the login route. → verify tokens.idToken's `nonce` claim equals the nonce you stored → const user = await valyd.auth.getUserInfo(tokens.accessToken) → clear the stored state/nonce, set your own app session, redirect to /dashboard. IF you are not using Node / the SDK: → POST https://idp.valyd.work/api/auth/oidc/token with JSON { grant_type: "authorization_code", client_id, client_secret, code, redirect_uri } and read the token from the TOP-LEVEL response field access_token.

Verification

  • Confirm the redirect: opening your login route returns HTTP 302 with a Location header beginning https://idp.valyd.work/api/auth/oidc/authorize?client_id=...&redirect_uri=...&response_type=code&scope=openid...&state=....

  • After consenting, confirm your /callback route receives code and state query parameters, and that state equals the value you sent.

  • Confirm the token exchange succeeds:

    curl -i -X POST https://idp.valyd.work/api/auth/oidc/token \ -H "Content-Type: application/json" \ -d '{"grant_type":"authorization_code","client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET","code":"AUTH_CODE_HERE","redirect_uri":"https://yourapp.com/callback"}'

    Expected: HTTP 200 and a JSON body containing top-level access_token, refresh_token, and id_token.

Common errors

  1. state mismatch on the callback (legitimate logins rejected).

    • Cause: The stored state was lost before the callback — cookie not set, blocked by the browser, expired, or overwritten by a second parallel login attempt.
    • Fix: Store state in an httpOnly, sameSite: "lax" cookie (or server session) on the login route and compare strictly on the callback: req.query.state === storedState. This comparison IS the CSRF protection — do not remove it.
  2. “missing code” / invalid_grant (HTTP 400 on callback or 4xx from /token).

    • Cause: No code in the callback (user denied or an error was returned), the code was already exchanged (single-use), or the redirect_uri in the token request differs from the authorize request.
    • Fix: When the callback carries error or no code, return HTTP 400 and restart the flow. Exchange the code immediately, exactly once, with the same redirect_uri.
  3. redirect_uri mismatch (authorization rejected by Valyd).

    • Cause: The redirect_uri sent on the authorization request does not exactly match the URL registered for your application.
    • Fix: Set VALYD_REDIRECT_URI (and the SDK redirectUri) to the exact callback URL registered in the Developer Portal (https://dev.valyd.work ), matching scheme, host, and path.
  4. invalid_scope / missing openid.

    • Cause: The scope parameter omitted openid, or a requested scope is not enabled on your app in the Developer Portal.
    • Fix: Always include openid (the SDK adds it automatically) and enable every requested scope on the app before requesting it.
Last updated on