Skip to content

Latest commit

 

History

History
822 lines (588 loc) · 21.1 KB

File metadata and controls

822 lines (588 loc) · 21.1 KB

AuthKit TanStack Start

Authentication and session management for TanStack Start applications using WorkOS AuthKit.

Note

This library is designed for TanStack Start v1.0+. TanStack Start is currently in beta - expect some API changes as the framework evolves.

Installation

npm install @workos/authkit-tanstack-react-start
pnpm add @workos/authkit-tanstack-react-start

Quickstart

Environment Variables

Create a .env file in your project root with the following required variables:

WORKOS_CLIENT_ID="client_..."      # Get from WorkOS dashboard
WORKOS_API_KEY="sk_test_..."       # Get from WorkOS dashboard
WORKOS_REDIRECT_URI="http://localhost:3000/api/auth/callback"
WORKOS_COOKIE_PASSWORD="..."       # Min 32 characters

Generate a secure cookie password (32+ characters):

openssl rand -base64 24

Optional Configuration

Variable Default Description
WORKOS_COOKIE_MAX_AGE 34560000 (400 days) Cookie lifetime in seconds
WORKOS_COOKIE_NAME wos-session Session cookie name
WORKOS_COOKIE_DOMAIN None Cookie domain (for multi-domain sessions)
WORKOS_COOKIE_SAMESITE lax SameSite attribute (lax, strict, none)
WORKOS_API_HOSTNAME api.workos.com WorkOS API hostname

Setup (3 Steps)

1. Configure Middleware

Create or update src/start.ts:

import { createStart } from '@tanstack/react-start';
import { authkitMiddleware } from '@workos/authkit-tanstack-react-start';

export const startInstance = createStart(() => ({
  requestMiddleware: [authkitMiddleware()],
}));

2. Create Callback Route

Create src/routes/api/auth/callback.tsx:

import { createFileRoute } from '@tanstack/react-router';
import { handleCallbackRoute } from '@workos/authkit-tanstack-react-start';

export const Route = createFileRoute('/api/auth/callback')({
  server: {
    handlers: {
      GET: handleCallbackRoute(),
    },
  },
});

Make sure this matches your WORKOS_REDIRECT_URI environment variable.

3. Create Sign-In Endpoint

Create a route that initiates the AuthKit sign-in flow. This route is used as the Sign-in endpoint (also known as initiate_login_uri) in your WorkOS dashboard settings.

Create src/routes/api/auth/sign-in.tsx:

import { createFileRoute } from '@tanstack/react-router';
import { getSignInUrl } from '@workos/authkit-tanstack-react-start';

export const Route = createFileRoute('/api/auth/sign-in')({
  server: {
    handlers: {
      GET: async ({ request }: { request: Request }) => {
        const returnPathname = new URL(request.url).searchParams.get('returnPathname');
        const url = await getSignInUrl(returnPathname ? { data: { returnPathname } } : undefined);
        return new Response(null, {
          status: 307,
          headers: { Location: url },
        });
      },
    },
  },
});

In the WorkOS dashboard Redirects page, set the Sign-in endpoint to match this route (e.g., http://localhost:3000/api/auth/sign-in).

Important

The sign-in endpoint is required for features like impersonation to work correctly. Without it, WorkOS-initiated flows (such as impersonating a user from the dashboard) will fail because they cannot complete the PKCE/CSRF verification that this library enforces on every callback.

4. Add Provider (Optional - only needed for client hooks)

If you want to use useAuth() or other client hooks, wrap your app with AuthKitProvider in src/routes/__root.tsx:

import { AuthKitProvider } from '@workos/authkit-tanstack-react-start/client';
import { Outlet, createRootRoute } from '@tanstack/react-router';

export const Route = createRootRoute({
  component: RootComponent,
});

function RootComponent() {
  return (
    <AuthKitProvider>
      <Outlet />
    </AuthKitProvider>
  );
}

If you're only using server-side authentication (getAuth() in loaders), you can skip this step.

WorkOS Dashboard Configuration

Open the Redirects page in the WorkOS dashboard and configure:

  1. Redirect URIs — add your callback URL: http://localhost:3000/api/auth/callback
  2. Sign-in endpoint — set to the route from step 3 above: http://localhost:3000/api/auth/sign-in. Required for WorkOS-initiated flows like dashboard impersonation.
  3. Sign-out redirect — where to send users after sign-out. If unset, WorkOS falls back to the App homepage URL; if neither is set, WorkOS shows an error page.

Usage

Server-Side Authentication

Use getAuth() in route loaders or server functions to access the current session:

import { createFileRoute, redirect } from '@tanstack/react-router';
import { getAuth } from '@workos/authkit-tanstack-react-start';

export const Route = createFileRoute('/dashboard')({
  loader: async () => {
    const { user } = await getAuth();

    if (!user) {
      throw redirect({ href: '/api/auth/sign-in' });
    }

    return { user };
  },
  component: DashboardPage,
});

function DashboardPage() {
  const { user } = Route.useLoaderData();
  return <div>Welcome, {user.firstName}!</div>;
}

Client-Side Hooks

For client components that need reactive auth state, use the useAuth() hook:

'use client'; // Not actually needed in TanStack Start, but shows intent

import { useAuth } from '@workos/authkit-tanstack-react-start/client';

function ProfileButton() {
  const { user, loading, signOut } = useAuth();

  if (loading) return <div>Loading...</div>;

  if (!user) return <a href="/signin">Sign In</a>;

  return (
    <div>
      <span>{user.email}</span>
      <button onClick={() => signOut()}>Sign Out</button>
    </div>
  );
}

Signing Out

Server-side (in route loader):

import { signOut } from '@workos/authkit-tanstack-react-start';

export const Route = createFileRoute('/logout')({
  loader: async () => {
    await signOut(); // Redirects to WorkOS logout, then back to '/'
  },
});

Client-side (from useAuth hook):

const { signOut } = useAuth();

await signOut({ returnTo: '/goodbye' });

Organization Switching

Switch the active organization for multi-org users:

Server-side:

import { switchToOrganization } from '@workos/authkit-tanstack-react-start';

// In a server function or loader
const auth = await switchToOrganization({
  data: { organizationId: 'org_456' },
});

// Session now has org_456's role, permissions, etc.

Client-side:

const { switchToOrganization, organizationId } = useAuth();

await switchToOrganization('org_456');
// Auth state updates automatically

Protected Routes

Use layout routes to protect multiple pages:

// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router';
import { getAuth } from '@workos/authkit-tanstack-react-start';

export const Route = createFileRoute('/_authenticated')({
  loader: async ({ location }) => {
    const { user } = await getAuth();

    if (!user) {
      const returnPathname = encodeURIComponent(location.pathname);
      throw redirect({ href: `/api/auth/sign-in?returnPathname=${returnPathname}` });
    }

    return { user };
  },
});

// Now all routes under _authenticated require auth:
// - _authenticated/dashboard.tsx
// - _authenticated/profile.tsx
// etc.

API Reference

Server Functions

These functions can be called from route loaders, server functions, or server route handlers.

getAuth()

Retrieves the current user session.

const { user } = await getAuth();

if (user) {
  console.log(user.email);
  console.log(user.firstName);
}

Returns: UserInfo | NoUserInfo

UserInfo fields:

  • user - The authenticated user object
  • sessionId - WorkOS session ID
  • organizationId - Active organization (if in org context)
  • role - User's role in the organization
  • roles - Array of role strings
  • permissions - Array of permission strings
  • entitlements - Array of entitlement strings
  • featureFlags - Array of feature flag strings
  • impersonator - Impersonator details (if being impersonated)
  • accessToken - JWT access token

signOut(options?)

Signs out the current user and redirects to WorkOS logout.

await signOut();
await signOut({ data: { returnTo: '/goodbye' } });

Options:

  • returnTo - Path to redirect to after logout (default: /)

switchToOrganization(options)

Switches to a different organization and refreshes the session with new claims.

const auth = await switchToOrganization({
  data: {
    organizationId: 'org_123',
    returnTo: '/dashboard', // optional
  },
});

Options:

  • organizationId - The organization ID to switch to (required)
  • returnTo - Path to redirect to if auth fails

Returns: UserInfo with updated organization claims

getSignInUrl(options?)

Generates a sign-in URL for redirecting to AuthKit.

// Basic usage
const url = await getSignInUrl();

// With return path
const url = await getSignInUrl({
  data: { returnPathname: '/dashboard' },
});

Options:

  • returnPathname - Path to return to after sign-in

getSignUpUrl(options?)

Generates a sign-up URL for redirecting to AuthKit.

const url = await getSignUpUrl();
const url = await getSignUpUrl({
  data: { returnPathname: '/onboarding' },
});

Options:

  • returnPathname - Path to return to after sign-up

getAuthorizationUrl(options)

Advanced: Generate a custom authorization URL with full control.

const url = await getAuthorizationUrl({
  data: {
    screenHint: 'sign-in',
    returnPathname: '/dashboard',
    redirectUri: 'https://example.com/callback', // override default
  },
});

Options:

  • screenHint - 'sign-in' or 'sign-up'
  • returnPathname - Return path after authentication
  • redirectUri - Override the default redirect URI

Route Handlers

handleCallbackRoute

Handles the OAuth callback from WorkOS. Use this in your callback route.

Basic usage:

import { createFileRoute } from '@tanstack/react-router';
import { handleCallbackRoute } from '@workos/authkit-tanstack-react-start';

export const Route = createFileRoute('/api/auth/callback')({
  server: {
    handlers: {
      GET: handleCallbackRoute(),
    },
  },
});

With hooks for custom logic:

export const Route = createFileRoute('/api/auth/callback')({
  server: {
    handlers: {
      GET: handleCallbackRoute({
        onSuccess: async ({ user, authenticationMethod }) => {
          // Create user record in your database
          await db.users.upsert({ id: user.id, email: user.email });
          // Track analytics
          analytics.track('User Signed In', { method: authenticationMethod });
        },
        onError: ({ error, request }) => {
          // Custom error handling
          console.error('Auth failed:', error);
          return new Response(JSON.stringify({ error: 'Authentication failed' }), {
            status: 500,
            headers: { 'Content-Type': 'application/json' },
          });
        },
      }),
    },
  },
});

Options:

  • onSuccess?: (data) => Promise<void> - Called after successful authentication with user data, tokens, and authentication method
  • onError?: ({ error, request }) => Response - Custom error handler that returns a Response
  • returnPathname?: string - Override the redirect path after authentication (defaults to state or /)

Client Hooks

Available from @workos/authkit-tanstack-react-start/client. Requires <AuthKitProvider> wrapper.

useAuth(options?)

Access authentication state and methods in client components.

import { useAuth } from '@workos/authkit-tanstack-react-start/client';

function MyComponent() {
  const { user, loading, signOut } = useAuth();

  if (loading) return <div>Loading...</div>;
  if (!user) return <div>Not signed in</div>;

  return (
    <div>
      <p>{user.email}</p>
      <button onClick={() => signOut()}>Sign Out</button>
    </div>
  );
}

Options:

  • ensureSignedIn?: boolean - If true, automatically triggers sign-in flow for unauthenticated users

Returns: AuthContextType with:

  • user - Current user or null
  • loading - Loading state
  • sessionId, organizationId, role, roles, permissions, entitlements, featureFlags, impersonator
  • getAuth() - Refresh auth state
  • refreshAuth(options) - Refresh session with optional org switch
  • signOut(options) - Sign out
  • switchToOrganization(orgId) - Switch organizations

useAccessToken()

Manage access tokens with automatic refresh.

import { useAccessToken } from '@workos/authkit-tanstack-react-start/client';

function ApiCaller() {
  const { accessToken, loading, getAccessToken } = useAccessToken();

  const callApi = async () => {
    const token = await getAccessToken(); // Always fresh

    const response = await fetch('/api/data', {
      headers: { Authorization: `Bearer ${token}` },
    });
  };

  return <button onClick={callApi}>Fetch Data</button>;
}

Returns:

  • accessToken - Current token (may be stale)
  • loading - Loading state
  • error - Last error or null
  • refresh() - Manually refresh token
  • getAccessToken() - Get guaranteed fresh token

useTokenClaims()

Parse and decode JWT claims from the access token.

import { useTokenClaims } from '@workos/authkit-tanstack-react-start/client';

function ClaimsDisplay() {
  const claims = useTokenClaims();

  if (!claims) return null;

  return (
    <div>
      <p>Session ID: {claims.sid}</p>
      <p>Organization: {claims.org_id}</p>
      <p>Role: {claims.role}</p>
    </div>
  );
}

Middleware

authkitMiddleware(options?)

Processes authentication on every request. Validates tokens, refreshes sessions, and provides auth context to server functions.

import { authkitMiddleware } from '@workos/authkit-tanstack-react-start';

// Basic usage
authkitMiddleware();

// With custom redirect URI (e.g., for Vercel preview deployments)
authkitMiddleware({
  redirectUri: 'https://preview-123.example.com/api/auth/callback',
});

Options:

  • redirectUri - Override the default redirect URI from WORKOS_REDIRECT_URI. Useful for dynamic environments like preview deployments.

TypeScript

This library is fully typed. Common types:

import type {
  User,
  Session,
  UserInfo,
  NoUserInfo,
  Impersonator
} from '@workos/authkit-tanstack-react-start';

// User object from WorkOS
const user: User = {
  id: string;
  email: string;
  firstName: string | null;
  lastName: string | null;
  emailVerified: boolean;
  profilePictureUrl: string | null;
  // ... more fields
};

// Auth result from getAuth()
const auth: UserInfo | NoUserInfo = await getAuth();

Route loaders get full type inference:

export const Route = createFileRoute('/profile')({
  loader: async () => {
    const { user } = await getAuth();
    return { user }; // Fully typed
  },
  component: ProfilePage,
});

function ProfilePage() {
  const { user } = Route.useLoaderData(); // user is typed!
}

How It Works

Server-Side Flow

  1. Middleware runs on every request - validates/refreshes session, stores auth in context
  2. Route loaders call getAuth() - retrieves auth from middleware context
  3. No client bundle bloat - server functions create RPC boundaries automatically

Client-Side Flow (with Provider)

  1. Provider wraps app - provides auth context to hooks
  2. Hooks call server actions - fetch auth state via RPC
  3. State updates automatically - on tab focus, refresh, org switch

Why the Provider is Optional

  • Server-only apps: Just use getAuth() in loaders - no provider needed
  • Client hooks needed: Add provider to use useAuth(), useAccessToken(), etc.
  • Flexibility: Start server-only, add client hooks later

Common Patterns

Sign In Flow

Link to the sign-in endpoint you created in setup step 3. The endpoint handles generating the AuthKit URL and setting the PKCE cookie.

export const Route = createFileRoute('/')({
  loader: async () => {
    const { user } = await getAuth();
    return { user };
  },
  component: HomePage,
});

function HomePage() {
  const { user } = Route.useLoaderData();

  if (!user) {
    return <a href="/api/auth/sign-in">Sign In with AuthKit</a>;
  }

  return <div>Welcome, {user.firstName}!</div>;
}

Protected Route Layout

// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router';
import { getAuth } from '@workos/authkit-tanstack-react-start';

export const Route = createFileRoute('/_authenticated')({
  loader: async ({ location }) => {
    const { user } = await getAuth();

    if (!user) {
      const returnPathname = encodeURIComponent(location.pathname);
      throw redirect({ href: `/api/auth/sign-in?returnPathname=${returnPathname}` });
    }

    return { user };
  },
});

// All child routes require authentication:
// - _authenticated/dashboard.tsx
// - _authenticated/settings.tsx

Organization Switcher

import { useAuth } from '@workos/authkit-tanstack-react-start/client';

function OrgSwitcher() {
  const { organizationId, switchToOrganization } = useAuth();

  return (
    <select
      value={organizationId || ''}
      onChange={(e) => switchToOrganization(e.target.value)}
    >
      <option value="org_123">Acme Corp</option>
      <option value="org_456">Other Company</option>
    </select>
  );
}

Accessing User in Multiple Places

Loader (server-side):

loader: async () => {
  const { user, organizationId, role } = await getAuth();
  return { user, organizationId, role };
};

Component (from loader data):

function MyPage() {
  const { user } = Route.useLoaderData();
  // ...
}

Client hook (reactive):

function MyClientComponent() {
  const { user, loading } = useAuth();
  // Updates on session changes
}

Troubleshooting

Missing required auth parameter when impersonating from the WorkOS dashboard

This error occurs when WorkOS-initiated flows (like dashboard impersonation) redirect directly to your callback URL without going through your application's sign-in flow. Because this library enforces PKCE/CSRF verification on every callback, the request is rejected when the required state parameter is missing.

Fix: Configure a sign-in endpoint in your WorkOS dashboard so impersonation flows route through your app first, letting PKCE/state be set up before redirecting to WorkOS.

"AuthKit middleware is not configured"

You forgot to add authkitMiddleware() to src/start.ts. See step 1 in setup.

"useAuth must be used within an AuthKitProvider"

You're calling useAuth() but haven't wrapped your app with <AuthKitProvider>. See step 3 in setup.

If you don't need client hooks, use getAuth() in loaders instead.

Environment variable errors on startup

The middleware validates configuration on first request. If you see errors about missing variables:

  1. Check your .env file exists
  2. Verify all required variables are set
  3. Ensure WORKOS_COOKIE_PASSWORD is 32+ characters
  4. Restart your dev server after changing env vars

Types not working / Import errors

Make sure you're importing from the right path:

// Server functions
import { getAuth, signOut } from '@workos/authkit-tanstack-react-start';

// Client hooks
import { useAuth } from '@workos/authkit-tanstack-react-start/client';

Don't import client hooks in server code or vice versa.

"can only be called on the server"

You're trying to call a server function from a beforeLoad hook or client component.

Wrong:

beforeLoad: async () => {
  const { user } = await getAuth(); // ❌ Runs on client during hydration
};

Right:

loader: async () => {
  const { user } = await getAuth(); // ✅ Server-only during SSR
};

Use useAuth() client hook for client components, or move logic to a loader.

Example Application

Check the /example directory for a complete working application demonstrating:

  • Server-side authentication in loaders
  • Client-side hooks with provider
  • Protected routes
  • Organization switching
  • Sign in/out flows
  • Access token management

Run it locally:

cd example
pnpm install
pnpm dev

Framework Compatibility

  • TanStack Start: v1.132.0+
  • TanStack Router: v1.132.0+
  • React: 18.0+
  • Node.js: 18+

Related

License

MIT