@roostjs/auth
WorkOS-backed authentication with session management, middleware guards, CSRF protection, RBAC, and multi-tenancy support.
Installation
bun add @roostjs/authConfiguration
Required environment variables:
WORKOS_API_KEY=sk_test_...
WORKOS_CLIENT_ID=client_...Required application config:
{
auth: {
redirectUrl: 'https://example.com/auth/callback',
}
}Register the service provider:
import { AuthServiceProvider } from '@roostjs/auth';
app.register(AuthServiceProvider);AuthServiceProvider
Registers SessionManager and WorkOSClient in the container.
Also registers the /auth/login, /auth/callback, and
/auth/logout routes on the application.
WorkOS Clients
RoostWorkOSClient
The real WorkOS client implementation. Wraps the WorkOS SDK and resolves credentials
from WORKOS_API_KEY and WORKOS_CLIENT_ID environment variables.
Registered automatically by AuthServiceProvider.
FakeWorkOSClient
In-memory WorkOS client for use in tests. Returns synthetic users and tokens without making HTTP calls to WorkOS.
WorkOSClientToken
The DI container token used to resolve the active WorkOSClient implementation.
SessionManager API
Manages session creation, validation, and destruction using Cloudflare KV storage with a sliding TTL.
constructor(kv: KVNamespace, secret: string)
Construct with a KV namespace for session storage and a secret for signing session tokens.
KVSessionStore
Low-level session store that reads and writes session data to Cloudflare KV. Used
internally by SessionManager. Resolve from the container when you need direct
KV session access without going through SessionManager.
Session Utilities
parseCookie(cookieHeader: string): Record<string, string>
Parse a Cookie request header string into a key-value record.
buildSetCookie(name: string, value: string, options?: CookieOptions): string
Build a Set-Cookie header value string.
parseJwtExpiry(token: string): number | null
Extract the expiration timestamp from a JWT without verifying its signature.
Returns the exp claim as a Unix timestamp, or null if absent.
async resolveUser(request: Request): Promise<RoostUser | null>
Parse and validate the session cookie from the request. Returns the associated
RoostUser, or null if the session is missing, invalid, or expired.
async createSession(user: WorkOSUser, organizationId?: string): Promise<{ sessionId: string; response: Response }>
Create a new KV session entry for the user. Returns a Response with the
Set-Cookie header set and the generated session ID.
async destroySession(sessionId: string): Promise<Response>
Delete the session from KV. Returns a Response with the session cookie
cleared.
Built-in Auth Routes
Registered automatically by AuthServiceProvider.
GET /auth/login
Redirects the user to the WorkOS-hosted login page. Accepts optional query parameters:
organization_id (string) and return_to (URL).
GET /auth/callback
WorkOS OAuth callback endpoint. Exchanges the authorization code for a session,
creates a KV session, and redirects to return_to or /.
GET /auth/logout
Destroys the current session and redirects to /.
Route Handler Functions
These are the underlying handler functions used by the built-in routes. Import them directly when you need to mount auth routes in a custom location.
handleCallback(request: Request, sessionManager: SessionManager, workosClient: WorkOSClient, redirectUrl: string): Promise<Response>
Handle the WorkOS OAuth callback. Exchanges the code for a session and redirects.
handleLogout(request: Request, sessionManager: SessionManager, redirectTo?: string): Promise<Response>
Destroy the current session and redirect. Defaults to redirecting to /auth/login.
createLoginHandler(workosClient: WorkOSClient, redirectUrl: string): (request: Request) => Promise<Response>
Create a login route handler that redirects to the WorkOS authorization URL.
Middleware
AuthMiddleware
Requires the request to have a valid session. Redirects to /auth/login
if no valid session is present.
GuestMiddleware
Requires the request to have no valid session. Redirects to / if the
user is already authenticated.
RoleMiddleware
Requires the authenticated user to have one of the specified roles in the current
organization. Accepts one or more role strings as middleware arguments.
Returns 403 Forbidden if the user's role is not in the allowed list.
app.useMiddleware(RoleMiddleware, 'admin', 'owner');CsrfMiddleware
Double-submit cookie CSRF protection. Validates that the _csrf form field
or X-CSRF-Token header matches the CSRF cookie value on state-mutating
requests (POST, PUT, PATCH, DELETE).
OrgResolver API
Extracts organization identity from an incoming request using one or more resolution strategies tried in order.
constructor(strategies: OrgResolutionStrategy[])
Construct with an ordered array of strategy names. Available strategies:
'subdomain', 'path-prefix', 'header'.
resolve(request: Request): { slug: string } | null
Attempt each strategy in order. Returns { slug } on first match,
or null if no strategy matches.
Resolution Strategies
'subdomain'
Extracts the first subdomain from the request hostname. Ignores www and
api. Example: acme.example.com → acme.
'path-prefix'
Extracts the organization slug from the second path segment.
Example: /org/acme/dashboard → acme.
'header'
Reads the X-Org-Slug request header.
TenantScopeMiddleware
TenantScopeMiddleware is exported from @roostjs/orm, not @roostjs/auth, but it is designed to be wired together with OrgResolver from @roostjs/auth. See the full API reference in the @roostjs/orm reference — TenantScopeMiddleware.
OrgResolver satisfies the OrgResolvable interface required by TenantScopeMiddleware:
// OrgResolver.resolve() returns { slug: string } | null
// TenantScopeMiddleware expects OrgResolvable: { resolve(request: Request): { slug: string } | null }
// These are structurally compatible — no adapter needed.
import { OrgResolver } from '@roostjs/auth';
import { TenantScopeMiddleware, TenantContext } from '@roostjs/orm';
const resolver = new OrgResolver(['subdomain']);
const middleware = new TenantScopeMiddleware(
resolver,
async (slug) => db.findOrgBySlug(slug),
ctx,
);Types
interface RoostUser {
id: string;
email: string;
firstName: string;
lastName: string;
organizationId?: string;
memberships: OrganizationMembership[];
}
interface OrganizationMembership {
organizationId: string;
organizationSlug: string;
role: string;
}
type OrgResolutionStrategy = 'subdomain' | 'path-prefix' | 'header';