WorkOS

WorkOS AuthStrategy — verifies WorkOS AuthKit / SSO JWTs via JWKS (no API key) and maps org_id → tenantId.

@voltro/plugin-auth-workos integrates WorkOS AuthKit / SSO: an AuthStrategy (conforming to @voltro/protocol) that verifies WorkOS-issued JWTs against the JWKS endpoint (public-key verify — no API key), maps org_id to the framework's tenantId, and composes with other strategies (your password cookie, an API key, another IdP). It's a thin specialization of the shared jwtBearerStrategy — it adds WorkOS's defaults and performs no verification of its own. Verified claims arrive on the Subject as metadata.provider === 'workos' + metadata.claims.

Install

pnpm add @voltro/plugin-auth-workos

Wiring

Add workosStrategy(...) to auth.strategies in your api's app.config.ts. The built-in signed-cookie password strategy always runs first; your strategies run after it, in order, until one matcheds (or one faileds).

// app.config.ts
import { workosStrategy } from '@voltro/plugin-auth-workos'

export default {
  type: 'api' as const,
  name: 'myApi',
  store: 'postgres' as const,
  auth: {
    strategies: [
      workosStrategy({
        clientId: process.env.WORKOS_CLIENT_ID!,   // client_01H… — builds JWKS URL + audience
        // defaultTenantId: 'public',               // fallback when claims expose no org_id
      }),
    ],
  },
}

clientId is the only required option. From it: the JWKS URL defaults to https://api.workos.com/sso/jwks/<clientId>, the issuer to https://api.workos.com, and the audience to the clientId. The cookie defaults to wos-session (set by AuthKit's cookie-mode SDK; pass null for header-only).

Tenant resolution maps from claims.org_id, then defaultTenantId. Supply tenantIdFromClaims(claims) to derive it (returning null is a failed verdict). Verified claims arrive as WorkosClaims (sub, email, org_id, role, permissions, …).

To accept your own password sessions AND WorkOS during a migration, build the chain explicitly with composeAuthStrategies (see auth strategies):

import { composeAuthStrategies } from '@voltro/protocol'
import { voltroPasswordStrategy } from '@voltro/plugin-auth'
import { workosStrategy } from '@voltro/plugin-auth-workos'

const resolve = composeAuthStrategies([
  voltroPasswordStrategy(),
  workosStrategy({ clientId: process.env.WORKOS_CLIENT_ID! }),
])

Options

Option Default Notes
clientId — (required) client_01H…. Builds JWKS URL + audience.
jwksUrl https://api.workos.com/sso/jwks/<clientId> Override.
issuer https://api.workos.com Override.
cookieName 'wos-session' AuthKit cookie mode; null for header-only.
tenantIdFromClaims (uses org_id) Derive tenantId; nullfailed.
defaultTenantId (none) Fallback when claims expose no org_id.
scopesFromClaims (none) Map claims (AuthKit's permissions[] / role) → Subject.scopes.

The audience is the clientId; algorithms are pinned to ['RS256', 'ES256'] (no alg-confusion).

Environment variables

None read directly — pass clientId explicitly (typically from your own defineEnv-declared vars).

Security

Server-only; fails closed — a forged, expired, or wrong-audience token, or one with no resolvable tenant, yields failed, never matched. JWKS public-key verify only; no WorkOS API key is ever handled.

Hosted-login primitives (SSO login)

Beyond the verify-only workosStrategy, the package exports two primitives for the hosted-login flow — WorkOS logs the user in and your app mints its own session (an alternate front door alongside password login, not a replacement session authority):

  • workosAuthorizationUrl({ clientId, redirectUri, state? }) → the AuthKit login URL to redirect the browser to.
  • workosAuthenticateWithCode({ clientId, apiKey, code }) → exchanges the callback code for a WorkosProfile (workosUserId, email, firstName, lastName, organizationId). Server-only (carries the API key).

Both are transport-thin (raw fetch, no @workos-inc/node dependency). Wire them into a GET /auth/workos/login + GET /auth/workos/callback route pair, find-or-provision your user in the callback, then issueSession(...). Full worked example: WorkOS SSO login.

Passwordless (Magic Auth)

For passwordless login — an email one-time code, no password and no browser redirect — the package exports the WorkOS Magic Auth pair:

  • workosSendMagicAuthCode({ apiKey, email }) → creates + emails a one-time code to email. Server-only (the API key is sent as a Bearer token).
  • workosAuthenticateWithMagicAuth({ clientId, apiKey, email, code }) → verifies the submitted code and returns the same WorkosProfile as the code exchange.

Wire them into a POST /auth/passwordless/start (send the code) + POST /auth/passwordless/verify (verify → find-or-provision → issueSession(...)) route pair — the same mint-your-own-session model as the hosted-login flow, without the redirect. Both are transport-thin (raw fetch).

See also