Framework-agnostic authentication library for WorkOS with pluggable storage adapters
npm install @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.
``bash`
pnpm add @workos/authkit-session
| Layer | Class | Purpose |
| ------------- | ---------------------- | --------------------------------------------------------------------------------------------------- |
| Core | AuthKitCore | JWT verification (JWKS with caching), session encryption (AES-256-CBC), token refresh orchestration |AuthOperations
| Operations | | WorkOS API calls: signOut, refreshSession, authorization URLs |CookieSessionStorage
| Helpers | | Base class with secure cookie defaults |AuthService
| Orchestration | | Reference implementation combining all layers |
`bash`
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:
`typescript
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',
});
`
`typescript
import {
CookieSessionStorage,
parseCookieHeader,
} from '@workos/authkit-session';
export class MyFrameworkStorage extends CookieSessionStorage<
Request,
Response
> {
async getSession(request: Request): Promise
const cookieHeader = request.headers.get('cookie');
if (!cookieHeader) return null;
return parseCookieHeader(cookieHeader)[this.cookieName] ?? null;
}
// Optional: override if your framework can mutate responses
protected async applyHeaders(
response: Response | undefined,
headers: Record
): Promise<{ response: Response }> {
const newResponse = response
? new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: new Headers(response.headers),
})
: new Response();
Object.entries(headers).forEach(([key, value]) => {
newResponse.headers.append(key, value);
});
return { response: newResponse };
}
}
`
CookieSessionStorage provides this.cookieName, this.cookieOptions, buildSetCookie(), and implements saveSession()/clearSession() using your applyHeaders().
`typescript
import { createAuthService } from '@workos/authkit-session';
export const authService = createAuthService({
sessionStorageFactory: config => new MyFrameworkStorage(config),
});
`
`typescript
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,
);
if (headers?.['Set-Cookie']) {
const newResponse = new Response(result.response.body, {
status: result.response.status,
statusText: result.response.statusText,
headers: result.response.headers,
});
newResponse.headers.set('Set-Cookie', headers['Set-Cookie']);
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.
withAuth() returns a discriminated union. If auth.user exists, all other properties exist:
`typescript
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
`
| 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.
`typescript
// 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, clearCookieHeader }
authService.refreshSession(session, organizationId?)
authService.switchOrganization(session, organizationId)
// URL Generation
authService.getAuthorizationUrl(options)
authService.getSignInUrl(options)
authService.getSignUpUrl(options)
`
For maximum control, use the primitives directly:
`typescript
import {
AuthKitCore,
AuthOperations,
getWorkOS,
getConfigurationProvider,
} from '@workos/authkit-session';
const config = getConfigurationProvider();
const client = getWorkOS(config.getConfig());
const core = new AuthKitCore(config, client, encryption);
const operations = new AuthOperations(core, client, config);
// Use core.validateAndRefresh(), core.encryptSession(), etc.
`
- JWKS Caching: Keys fetched on-demand, cached for process lifetime. jose handles key rotation automatically.iron-webcrypto
- Token Expiry Buffer: 60 seconds default. Tokens refresh before actual expiry.
- Session Encryption: AES-256-CBC + SHA-256 HMAC via .createAuthService()
- Lazy Initialization: defers initialization until first use, allowing configure() to be called later.
See @workos/authkit-tanstack-start` for a complete example.
MIT