Add authentication to your Astro site with AuthKit
A step by step tutorial to sign-in, sign-up, and protected routes in Astro, using WorkOS AuthKit.
Astro's island architecture makes it fast, but authentication has always meant stitching together your own session handling, middleware, and OAuth flow by hand. WorkOS just released a dedicated AuthKit SDK for Astro that handles all of it in a single integration: session validation, automatic token refresh, PKCE sign-in, route protection, and a set of ready-made components.
This guide walks through adding AuthKit to an Astro app from scratch. By the end, you'll have protected routes, a working sign-in flow, and a session you can read from both server pages and client islands.
What you'll need
- An Astro project rendered on demand (SSR), since AuthKit's middleware and routes run per request. If your project is currently static, you'll add a server adapter as part of setup.
- A free WorkOS account, available at workos.com.
- Node 18 or later.
Step 1: Install the integration
From your project root, run:
This single command installs the node adapter (swap it for your deployment target's adapter if you're not using Node) and wires up the AuthKit integration in astro.config.mjs. Without a server adapter, AuthKit throws at startup, since its middleware and routes need to run per request rather than at build time.
If you'd rather set it up by hand, add this to astro.config.mjs:
Either way, you'll also need the WorkOS Node SDK as a peer dependency. The astro add command installs it automatically; if you're setting things up manually, add it yourself:
Step 2: Set your environment variables
AuthKit reads its configuration through astro:env at runtime, so none of these values get bundled into client code. Add them to your .env file:
You can generate a cookie password with:
Then, in the WorkOS dashboard, open the Redirects page and add http://localhost:4321/callback as a redirect URI. That's the last piece of setup. /login, /signup, /callback, and /logout routes now exist automatically, and Astro.locals.auth is populated (and fully typed) on every request.
Step 3: Protect your routes
Tell the integration which routes require a signed-in user:
protectedRoutes accepts plain prefixes, where /dashboard also matches everything nested under it, or path-to-regexp patterns for more precise matching, like /orgs/:slug or /files/:path*. Anonymous visitors who hit a protected page get redirected to sign-in. Anonymous fetch() calls from a client island get a 401 JSON response instead, so client-side code can handle the failure directly rather than following a redirect it can't act on.
Step 4: Read the session on the server
Inside any .astro file, the session is already available on Astro.locals:
auth is a discriminated union on user. Once you check if (auth.user), TypeScript narrows sessionId, accessToken, and claims to their non-optional types, so you get autocomplete and type safety without extra casting.
You can also guard a page or an API route imperatively, which is useful when a redirect alone isn't specific enough:
auth.has() checks roles, permissions, entitlements, or feature flags, and ANDs multiple checks together when you pass more than one.
Step 5: Add sign-in and sign-out UI
AuthKit ships server-rendered components for the common cases, so the unmatched branch never even reaches the browser on request-rendered pages:
Show takes 'signed-in', 'signed-out', an object of role, permission, entitlement, or feature flag checks, or a predicate function, and renders its fallback slot when the check fails.
Step 6: Read the session from client islands
Server components cover most cases, but interactive islands (React, Vue, Svelte, Preact, or Solid) need a client-side view of the session too. AuthKit ships a small nanostores based store that works the same way across every framework, and never includes the access token in what reaches the browser.
Drop this once in your layout so the store hydrates synchronously, before any island renders:
Then read it from an island. For React specifically, AuthKit includes zero-dependency hooks so you don't need to add @nanostores/react yourself:
Check isLoaded before rendering based on user. It's what tells you the difference between "the store hasn't hydrated yet" and "this person is actually signed out."
A note on prerendered pages
If a page sets export const prerender = true, it builds without a request, so the middleware has nothing to validate at build time and locals.auth is signed out by default. On those pages, SignedIn, SignedOut, and Show automatically defer to the client store and resolve after hydration instead. If a prerendered page must never ship gated content in its static HTML at all, add the serverOnly prop to exclude it entirely.
Handling organizations and webhooks
Two things come up often enough to be worth calling out separately.
Switching a session's active organization refreshes the access token to match and updates the session cookie:
And verifying an incoming WorkOS webhook is a one-line call that gives you back a parsed, signature-checked event:
Next steps
At this point you have working sign-in, sign-up, protected routes, and a session that's readable from both the server and client islands, with no manual cookie handling anywhere in your code. From here, the @workos/authkit-astro GitHub repo has a full example app in its example/ directory that's worth cloning if you want to see a complete, runnable project, including organization switching and prerendered pages side by side.