JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 42111
  • Score
    100M100P100Q167480F
  • License MIT

Framework-agnostic authentication library for WorkOS with pluggable storage adapters

Package Exports

  • @workos/authkit-session

Readme

@workos/authkit-session

[!WARNING] This is prerelease software. APIs may change without notice.

Toolkit for building WorkOS AuthKit framework integrations.

Handles JWT verification, session encryption, and token refresh orchestration. You build the framework-specific glue.

Installation

pnpm add @workos/authkit-session

What This Library Provides

Layer Class Purpose
Core AuthKitCore JWT verification (JWKS with caching), session encryption (AES-256-CBC), token refresh orchestration
Operations AuthOperations WorkOS API calls: signOut, refreshSession, authorization URLs
Helpers CookieSessionStorage Base class with secure cookie defaults
Orchestration AuthService Reference implementation combining all layers

Quick Start

1. Configure

WORKOS_CLIENT_ID=your-client-id
WORKOS_API_KEY=your-api-key
WORKOS_REDIRECT_URI=https://yourdomain.com/auth/callback
WORKOS_COOKIE_PASSWORD=must-be-at-least-32-characters-long-secret

Or programmatically:

import { configure } from '@workos/authkit-session';

configure({
  clientId: 'your-client-id',
  apiKey: 'your-api-key',
  redirectUri: 'https://yourdomain.com/auth/callback',
  cookiePassword: 'must-be-at-least-32-characters-long-secret',
});

2. Create Storage Adapter

import { CookieSessionStorage } from '@workos/authkit-session';

export class MyFrameworkStorage extends CookieSessionStorage<
  Request,
  Response
> {
  async getCookie(request: Request, name: string): Promise<string | null> {
    const header = request.headers.get('cookie');
    if (!header) return null;
    for (const part of header.split(';')) {
      const [k, ...rest] = part.trim().split('=');
      if (k === name) return decodeURIComponent(rest.join('='));
    }
    return null;
  }

  // Optional: override if your framework can mutate responses
  protected async applyHeaders(
    response: Response | undefined,
    headers: Record<string, string | string[]>,
  ): Promise<{ response: Response }> {
    const newResponse = response
      ? new Response(response.body, {
          status: response.status,
          statusText: response.statusText,
          headers: new Headers(response.headers),
        })
      : new Response();

    for (const [key, value] of Object.entries(headers)) {
      if (Array.isArray(value)) {
        for (const v of value) newResponse.headers.append(key, v);
      } else {
        newResponse.headers.append(key, value);
      }
    }

    return { response: newResponse };
  }
}

CookieSessionStorage provides this.cookieName, this.cookieOptions, and generic setCookie/clearCookie/serializeCookie primitives. getSession/saveSession/clearSession are one-line wrappers — you only implement getCookie.

3. Create Service

import { createAuthService } from '@workos/authkit-session';

export const authService = createAuthService({
  sessionStorageFactory: config => new MyFrameworkStorage(config),
});

4. Implement Middleware

export const authMiddleware = () => {
  return createMiddleware().server(async args => {
    const { auth, refreshedSessionData } = await authService.withAuth(
      args.request,
    );

    const result = await args.next({
      context: { auth: () => auth },
    });

    // CRITICAL: Persist refreshed tokens to cookie
    if (refreshedSessionData) {
      const { headers } = await authService.saveSession(
        undefined,
        refreshedSessionData,
      );
      const setCookie = headers?.['Set-Cookie'];
      if (setCookie) {
        const newResponse = new Response(result.response.body, {
          status: result.response.status,
          statusText: result.response.statusText,
          headers: result.response.headers,
        });
        // Append each entry — never `.set()` with an array (comma-joined
        // Set-Cookie is not a valid single HTTP header).
        for (const v of Array.isArray(setCookie) ? setCookie : [setCookie]) {
          newResponse.headers.append('Set-Cookie', v);
        }
        return { ...result, response: newResponse };
      }
    }

    return result;
  });
};

If you skip applying Set-Cookie, refreshed tokens never persist. Next request sees the old expired token → infinite refresh loop.

AuthResult Type

withAuth() returns a discriminated union. If auth.user exists, all other properties exist:

const { auth } = await authService.withAuth(request);

if (!auth.user) {
  return redirect('/login');
}

// TypeScript knows these exist (no ! needed)
auth.sessionId; // string
auth.accessToken; // string
auth.claims.sid; // string

Configuration Options

Environment Variable Config Key Description
WORKOS_CLIENT_ID clientId WorkOS client ID
WORKOS_API_KEY apiKey WorkOS API key
WORKOS_REDIRECT_URI redirectUri OAuth callback URL
WORKOS_COOKIE_PASSWORD cookiePassword 32+ char encryption key
WORKOS_COOKIE_NAME cookieName Cookie name (default: wos-session)
WORKOS_COOKIE_MAX_AGE cookieMaxAge Cookie lifetime in seconds
WORKOS_COOKIE_DOMAIN cookieDomain Cookie domain
WORKOS_COOKIE_SAME_SITE cookieSameSite lax, strict, or none

Environment variables override programmatic config.

API Overview

AuthService Methods

// Authentication
authService.withAuth(request)                    // → { auth, refreshedSessionData? }
authService.handleCallback(request, response, { code, state })
authService.getSession(request)                  // → Session | null
authService.saveSession(response, sessionData)   // → { response?, headers? }
authService.clearSession(response)

// WorkOS Operations
authService.signOut(sessionId, { returnTo })     // → { logoutUrl, response?, headers? }
authService.refreshSession(session, organizationId?)
authService.switchOrganization(session, organizationId)

// URL Generation — write verifier cookie, return { url, response?, headers? }
authService.createAuthorization(response, options)
authService.createSignIn(response, options)
authService.createSignUp(response, options)

// Error-path cleanup for the PKCE verifier cookie
// (response may be `undefined` for headers-only adapters)
authService.clearPendingVerifier(response, { redirectUri? })

This library binds every OAuth sign-in to a PKCE code verifier, so a leaked state value on its own cannot be used to complete a session hijack.

The verifier is sealed into a single blob that serves two roles:

  1. It is sent to WorkOS as the OAuth state query parameter.
  2. It is set as a short-lived HTTP-only cookie (wos-auth-verifier, 10 min).

The cookie is written and read through SessionStorage. Callers don't see sealed blobs or cookie options:

// Sign in: library writes the verifier cookie via storage, returns the URL + headers
const { url, headers } = await authService.createSignIn(response, {
  returnPathname: '/dashboard',
});
return new Response(null, {
  status: 302,
  headers: { ...headers, Location: url },
});

// Callback: library reads the verifier via storage, byte-compares, then exchanges
await authService.handleCallback(request, response, {
  code,
  state, // from URL
});

On success, handleCallback returns a Set-Cookie entry in headers as a string[] with two values — the session cookie AND a clear for the verifier cookie. Adapters must append each entry as its own Set-Cookie HTTP header (never comma-join). The bag key is case-insensitive — mergeHeaderBags preserves the adapter's casing — so look it up that way:

const setCookie =
  result.headers?.['Set-Cookie'] ?? result.headers?.['set-cookie'];
if (setCookie) {
  for (const v of Array.isArray(setCookie) ? setCookie : [setCookie]) {
    response.headers.append('Set-Cookie', v);
  }
}

Mismatched state and cookie raise OAuthStateMismatchError. A missing cookie (typical cause: Set-Cookie stripped by a proxy) raises PKCECookieMissingError. On either error path — or any early bail-out before handleCallback runs — call authService.clearPendingVerifier(response) to emit a delete header.

Direct Access (Advanced)

For maximum control, use the primitives directly:

import {
  AuthKitCore,
  AuthOperations,
  getConfigurationProvider,
  getWorkOS,
  sessionEncryption,
} from '@workos/authkit-session';

const config = getConfigurationProvider().getConfig();
const client = getWorkOS();
const core = new AuthKitCore(config, client, sessionEncryption);
const operations = new AuthOperations(core, client, config, sessionEncryption);

// Use core.validateAndRefresh(), core.encryptSession(), etc.

Technical Details

  • JWKS Caching: Keys fetched on-demand, cached for process lifetime. jose handles key rotation automatically.
  • Token Refresh: validateAndRefresh refreshes when verifyToken fails (i.e. when the access token is expired or invalid). isTokenExpiring(token, buffer) is available as a separate helper for callers that want to proactively refresh before expiry.
  • Session Encryption: AES-256-CBC + SHA-256 HMAC via iron-webcrypto.
  • Lazy Initialization: createAuthService() defers initialization until first use, allowing configure() to be called later.

Reference Implementation

See @workos/authkit-tanstack-start for a complete example.

License

MIT