UNPKG

@workos/authkit-tanstack-react-start

Version:

The WorkOS library for TanStack React Start provides convenient helpers for authentication and session management using WorkOS & AuthKit with TanStack React Start.

740 lines (526 loc) 17.7 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 ```bash npm install @workos/authkit-tanstack-react-start ``` ```bash pnpm add @workos/authkit-tanstack-react-start ``` ## Quickstart ### Environment Variables Create a `.env` file in your project root with the following required variables: ```bash 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): ```bash 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`: ```typescript 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`: ```typescript 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. 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`: ```typescript 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 1. Go to [WorkOS Dashboard](https://dashboard.workos.com) 2. Navigate to **Redirects** and add your callback URL: `http://localhost:3000/api/auth/callback` 3. (Optional) Set a default **Logout URI** under Redirects for sign-out redirects ## Usage ### Server-Side Authentication Use `getAuth()` in route loaders or server functions to access the current session: ```typescript import { createFileRoute, redirect } from '@tanstack/react-router'; import { getAuth, getSignInUrl } from '@workos/authkit-tanstack-react-start'; export const Route = createFileRoute('/dashboard')({ loader: async () => { const { user } = await getAuth(); if (!user) { const signInUrl = await getSignInUrl(); throw redirect({ href: signInUrl }); } 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: ```typescript '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):** ```typescript 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):** ```typescript const { signOut } = useAuth(); await signOut({ returnTo: '/goodbye' }); ``` ### Organization Switching Switch the active organization for multi-org users: **Server-side:** ```typescript 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:** ```typescript const { switchToOrganization, organizationId } = useAuth(); await switchToOrganization('org_456'); // Auth state updates automatically ``` ### Protected Routes Use layout routes to protect multiple pages: ```typescript // src/routes/_authenticated.tsx import { createFileRoute, redirect } from '@tanstack/react-router'; import { getAuth, getSignInUrl } from '@workos/authkit-tanstack-react-start'; export const Route = createFileRoute('/_authenticated')({ loader: async ({ location }) => { const { user } = await getAuth(); if (!user) { const signInUrl = await getSignInUrl({ data: { returnPathname: location.pathname }, }); throw redirect({ href: signInUrl }); } 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. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript // 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. ```typescript 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. ```typescript 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. ```typescript 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, }, }, }); ``` ### Client Hooks Available from `@workos/authkit-tanstack-react-start/client`. Requires `<AuthKitProvider>` wrapper. #### `useAuth(options?)` Access authentication state and methods in client components. ```typescript 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. ```typescript 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. ```typescript 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()` Processes authentication on every request. Validates tokens, refreshes sessions, and provides auth context to server functions. Already shown in setup, but can be imported separately if needed. ## TypeScript This library is fully typed. Common types: ```typescript 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: ```typescript 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 ```typescript // Get sign-in URL in loader export const Route = createFileRoute('/')({ loader: async () => { const { user } = await getAuth(); const signInUrl = await getSignInUrl(); return { user, signInUrl }; }, component: HomePage, }); function HomePage() { const { user, signInUrl } = Route.useLoaderData(); if (!user) { return <a href={signInUrl}>Sign In with AuthKit</a>; } return <div>Welcome, {user.firstName}!</div>; } ``` ### Protected Route Layout ```typescript // src/routes/_authenticated.tsx import { createFileRoute, redirect } from '@tanstack/react-router'; import { getAuth, getSignInUrl } from '@workos/authkit-tanstack-react-start'; export const Route = createFileRoute('/_authenticated')({ loader: async ({ location }) => { const { user } = await getAuth(); if (!user) { const signInUrl = await getSignInUrl({ data: { returnPathname: location.pathname }, }); throw redirect({ href: signInUrl }); } return { user }; }, }); // All child routes require authentication: // - _authenticated/dashboard.tsx // - _authenticated/settings.tsx ``` ### Organization Switcher ```typescript 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):** ```typescript loader: async () => { const { user, organizationId, role } = await getAuth(); return { user, organizationId, role }; }; ``` **Component (from loader data):** ```typescript function MyPage() { const { user } = Route.useLoaderData(); // ... } ``` **Client hook (reactive):** ```typescript function MyClientComponent() { const { user, loading } = useAuth(); // Updates on session changes } ``` ## Troubleshooting ### "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: ```typescript // 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:** ```typescript beforeLoad: async () => { const { user } = await getAuth(); // ❌ Runs on client during hydration }; ``` **Right:** ```typescript 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: ```bash 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 - [WorkOS AuthKit Documentation](https://workos.com/docs/user-management/authkit) - [TanStack Start Documentation](https://tanstack.com/start/latest) - [WorkOS Node SDK](https://github.com/workos/workos-node) ## License MIT