AuthKit with Astro
🌱 This post is in the growth phase. It may still be
useful as it grows up.
These instructions are a step-by-step guide to setting up AuthKit with Astro. Each step will result in some observable change that should illustrate completion.
The guide tracks each steup of the WorkOS SSO authentiaction flow:
Contents
Warning
As of this writing, @workos-inc/node
v7 has an issue that causes it to fail in Cloudflare Workers .
The current workaround is to downgrade to v6 — which has everything needed for this tutorial.
Set up environment
Install WorkOS Node SDK
Set secrets in local environment
WORKOS_API_KEY=#COPY FROM WORKOS DASHBOARD
WORKOS_CLIENT_ID=#COPY FROM WORKOS DASHBOARD
WORKOS_REDIRECT_URI=#LOCAL PATH TO AUTH CALLBACK ENDPOINT
WORKOS_COOKIE_PASSWORD=#32 RANDOM CHARACTER PASSWORD
Direct users to Hosted AuthKit
Create /sign-in redirect endpoint
import type {APIRoute} from 'astro'
export const GET : APIRoute = async () => {
'/sign-in redirect endpoint. Not Implemented.'
// disable prerendering in 'hyrbrid' mode
export const prerender = false
Generate the authorization URL
import type {APIRoute} from 'astro'
import {WorkOS} from '@workos-inc/node'
const workos = new WorkOS ( import . meta .env. WORKOS_API_KEY )
export const GET : APIRoute = async () => {
workos.userManagement. getAuthorizationUrl ({
redirectUri: import . meta .env. WORKOS_REDIRECT_URI ,
clientId: import . meta .env. WORKOS_CLIENT_ID ,
'/sign-in redirect endpoint. Not Implemented.' ,
Redirect user to authorization URL
export const GET : APIRoute = async ( { redirect } ) => {
workos.userManagement. getAuthorizationUrl ({
redirectUri: import . meta .env. WORKOS_REDIRECT_URI ,
clientId: import . meta .env. WORKOS_CLIENT_ID ,
return new Response (authorizationUrl)
return redirect (authorizationUrl)
Final /sign-in redirect endpoint
import {WorkOS} from '@workos-inc/node'
import type {APIRoute} from 'astro'
const workos = new WorkOS ( import . meta .env. WORKOS_API_KEY )
export const GET : APIRoute = async ({ redirect }) => {
workos.userManagement. getAuthorizationUrl ({
redirectUri: import . meta .env. WORKOS_REDIRECT_URI ,
clientId: import . meta .env. WORKOS_CLIENT_ID ,
return redirect (authorizationUrl)
// required in `hybrid` rendering mode
export const prerender = false
Create auth callback endpoint
import type {APIRoute} from 'astro'
export const GET : APIRoute = async ({}) => {
'Auth callback endpoint. Not implemented.'
export const GET : APIRoute = async ( { request } ) => {
new URL (request.url).searchParams. get ( 'code' )
'Auth callback endpoint. Not implemented.'
Exchange authorization code for user Profile
import type {APIRoute} from 'astro'
import {WorkOS} from '@workos-inc/node'
const workos = new WorkOS ( import . meta .env. WORKOS_API_KEY )
export const GET : APIRoute = async ({ request }) => {
new URL (request.url).searchParams. get ( 'code' )
await workos.userManagement. authenticateWithCode ({
clientId: import . meta .env. WORKOS_CLIENT_ID ,
return new Response (code)
return new Response ( JSON . stringify (session))
Encrypt session
import type {APIRoute} from 'astro'
import {WorkOS} from '@workos-inc/node'
import {sealData} from 'iron-session'
const workos = new WorkOS ( import . meta .env. WORKOS_API_KEY )
export const GET : APIRoute = async ({ request }) => {
new URL (request.url).searchParams. get ( 'code' )
await workos.userManagement. authenticateWithCode ({
clientId: import . meta .env. WORKOS_CLIENT_ID ,
const encryptedSession = await sealData (session, {
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
return new Response (session)
return new Response (encryptedSession)
Set cookie, using encrypted session
export const GET : APIRoute = async ({
new URL (request.url).searchParams. get ( 'code' )
await workos.userManagement. authenticateWithCode ({
clientId: import . meta .env. WORKOS_CLIENT_ID ,
const encryptedSession = await sealData (session, {
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
cookies. set ( 'wos-session' , encryptedSession, {
return new Response (encryptedSession)
Redirect user to authenticated route
export const GET : APIRoute = async ({
new URL (request.url).searchParams. get ( 'code' )
await workos.userManagement. authenticateWithCode ({
const encryptedSession = await sealData (session, {
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
cookies. set ( 'wos-session' , encryptedSession, {
return new Response (encryptedSession)
return redirect ( '/dashboard' )
Final auth callback endpoint
import type {APIRoute} from 'astro'
import {WorkOS} from '@workos-inc/node'
import {sealData} from 'iron-session'
const workos = new WorkOS ( import . meta .env. WORKOS_API_KEY )
export const GET : APIRoute = async ({
new URL (request.url).searchParams. get ( 'code' )
await workos.userManagement. authenticateWithCode ({
clientId: import . meta .env. WORKOS_CLIENT_ID ,
const encryptedSession = await sealData (session, {
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
cookies. set ( 'wos-session' , encryptedSession, {
return redirect ( '/dashboard' )
// required in `hybrid` rendering mode
export const prerender = false
Create single protected page
// disable prerendering in 'hyrbrid' mode
export const prerender = false
< pre >< code >{ JSON . stringify ( 'Not implemented.' , null , ' \t ' )}</ code ></ pre >
Read encrypted session from cookie. Redirect in not present.
// disable prerendering in 'hyrbrid' mode
export const prerender = false
const cookie = Astro.cookies. get ( 'wos-session' )
return Astro. redirect ( '/sign-in' )
< pre >< code >{ JSON . stringify ( "Not implemented" cookie , null , ' \t ' )}</ code ></ pre >
Decript session
import {unsealData} from 'iron-session'
// disable prerendering in 'hyrbrid' mode
export const prerender = false
const cookie = Astro.cookies. get ( 'wos-session' )
return Astro. redirect ( '/sign-in' )
const session = await unsealData (cookie.value, {
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
< pre >< code >{ JSON . stringify ( cookie session , null , ' \t ' )}</ code ></ pre >
Verify the user session with a JWT
import {WorkOS} from '@workos-inc/node'
import {createRemoteJWKSet, jwtVerify} from 'jose'
import {unsealData} from 'iron-session'
const cookie = Astro.cookies. get ( 'wos-session' )
return Astro. redirect ( '/sign-in' )
const session = await unsealData (cookie.value, {
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
const workos = new WorkOS ( import . meta .env. WORKOS_API_KEY )
const JWKS = createRemoteJWKSet (
workos.userManagement. getJwksUrl (
import . meta .env. WORKOS_CLIENT_ID
let verifiedSession = await jwtVerify (session.accessToken, JWKS )
export const prerender = false
< pre >< code >{ JSON . stringify ( session verifiedSession , null , ' \t ' )}</ code ></ pre >
Redirect to /sign-in route if session is invalid
let verifiedSession = await jwtVerify (session.accessToken, JWKS )
verifiedSession = await jwtVerify (session.accessToken, JWKS )
return Astro. redirect ( '/sign-in' )
Display session data
< pre >< code >{ JSON . stringify (verifiedSession, null , ' \t ' )}</ code ></ pre >
< h1 >Hello {session.user.lastName}!</ h1 >
Final protected user page
import {WorkOS} from '@workos-inc/node'
import {createRemoteJWKSet, jwtVerify} from 'jose'
import {unsealData} from 'iron-session'
const cookie = Astro.cookies. get ( 'wos-session' )
return Astro. redirect ( '/sign-in' )
const session = await unsealData (cookie.value, {
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
const workos = new WorkOS ( import . meta .env. WORKOS_API_KEY )
const JWKS = createRemoteJWKSet (
workos.userManagement. getJwksUrl (
import . meta .env. WORKOS_CLIENT_ID
verifiedSession = await jwtVerify (session.accessToken, JWKS )
return Astro. redirect ( '/sign-in' )
export const prerender = false
< h1 >Hello {session.user.first_name} {session.user.last_name} !</ h1 >
Automatically refresh session with session refreshToken
import {WorkOS} from '@workos-inc/node'
import {createRemoteJWKSet, jwtVerify} from 'jose'
import { sealData, unsealData} from 'iron-session'
const cookie = Astro.cookies. get ( 'wos-session' )
return Astro. redirect ( '/sign-in' )
const session = await unsealData (cookie.value, {
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
const workos = new WorkOS ( import . meta .env. WORKOS_API_KEY )
const JWKS = createRemoteJWKSet (
workos.userManagement. getJwksUrl (
import . meta .env. WORKOS_CLIENT_ID
verifiedSession = await jwtVerify (session.accessToken, JWKS )
await workos.userManagement. authenticateWithRefreshToken ({
clientId: import . meta .env. WORKOS_CLIENT_ID ,
refreshToken: session.refreshToken,
const encryptedRefreshedSession = await sealData (
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
encryptedRefreshedSession,
return Astro. redirect ( '/sign-in' )
export const prerender = false
Hello {session.user.last_name}!
Let’s break that down.
Set access token duration in the WorkOS dashboard
By default, WorkOS sessions require refreshing every 5 minutes.
That duration can be configured in the WorkOS dashboard.
To build our integration, we’ll set the duration to the minimum value of 1 minute.
Attempt to refresh session if jwtVerify fails
Once authenticated, our access token is now valid for 1 minute.
Add a an early reaturn to observe the error.
verifiedSession = await jwtVerify (session.accessToken, JWKS )
return Astro. redirect ( '/sign-in' )
When verification fails, we’ll see this error:
JWTExpired: "exp" claim timestamp check failed
Attempt to refresh session
Add a try-catch block to refresh the session.
For starters, let’s render the session.refreshToken
to ensure we have the value in scope.
verifiedSession = await jwtVerify (session.accessToken, JWKS )
return new Response (session.refreshToken);
return Astro. redirect ( '/sign-in' )
Authenticate with refresh token
In the try block, call the authenticateWithRefreshToken
method with the refresh token.
const refreshedSession = await workos.userManagement. authenticateWithRefreshToken ({
clientId: import . meta .env. WORKOS_CLIENT_ID ,
refreshToken: session.refreshToken,
return new Response ( session.refreshToken refreshedSession );
Encrypt the refreshed session
const refreshedSession = await workos.userManagement. authenticateWithRefreshToken ({
clientId: import . meta .env. WORKOS_CLIENT_ID ,
refreshToken: session.refreshToken,
const encryptedRefreshedSession = await sealData (
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
return new Response ( refreshedSession encryptedRefreshedSession );
Set cookie with new encrypeted session
const refreshedSession = await workos.userManagement. authenticateWithRefreshToken ({
clientId: import . meta .env. WORKOS_CLIENT_ID ,
refreshToken: session.refreshToken,
const encryptedRefreshedSession = await sealData (
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
encryptedRefreshedSession,
return new Response (refreshedSessionencryptedRefreshedSession);
console. log ( 'session refreshed successfully' )
return Astro. redirect ( '/sign-in' )
Create Auth Middleware
import {defineMiddleware} from 'astro:middleware'
export const onRequest = defineMiddleware (
async ( context , next ) => {
Skip middleware for un-protected routes
import {defineMiddleware} from 'astro:middleware'
import match from 'picomatch'
export const onRequest = defineMiddleware (
async ( context , next ) => {
String ( new URL (context.request.url).pathname)
Move page-specific auth into middleware.ts (changing Astro
global to context
)
import {WorkOS} from '@workos-inc/node'
import {createRemoteJWKSet, jwtVerify} from 'jose'
import { sealData, unsealData} from 'iron-session'
const cookie = Astro.cookies. get ( 'wos-session' )
return Astro. redirect ( '/sign-in' )
const session = await unsealData (cookie.value, {
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
const workos = new WorkOS ( import . meta .env. WORKOS_API_KEY )
const JWKS = createRemoteJWKSet (
workos.userManagement. getJwksUrl (
import . meta .env. WORKOS_CLIENT_ID
verifiedSession = await jwtVerify (session.accessToken, JWKS )
await workos.userManagement. authenticateWithRefreshToken ({
clientId: import . meta .env. WORKOS_CLIENT_ID ,
refreshToken: session.refreshToken,
const encryptedRefreshedSession = await sealData (
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
encryptedRefreshedSession,
return Astro. redirect ( '/sign-in' )
export const prerender = false
Hello {session.user.last_name}!
import {defineMiddleware} from 'astro:middleware'
import match from 'picomatch'
import {WorkOS} from '@workos-inc/node'
import {createRemoteJWKSet, jwtVerify} from 'jose'
import {sealData, unsealData} from 'iron-session'
export const onRequest = defineMiddleware (
async ( context , next ) => {
String ( new URL (context.request.url).pathname)
const cookie = context .cookies. get ( 'wos-session' )
return context . redirect ( '/sign-in' )
const session = await unsealData (cookie.value, {
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
const workos = new WorkOS ( import . meta .env. WORKOS_API_KEY )
const JWKS = createRemoteJWKSet (
workos.userManagement. getJwksUrl (
import . meta .env. WORKOS_CLIENT_ID
verifiedSession = await jwtVerify (
await workos.userManagement. authenticateWithRefreshToken (
clientId: import . meta .env. WORKOS_CLIENT_ID ,
refreshToken: session.refreshToken,
const encryptedRefreshedSession = await sealData (
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,
encryptedRefreshedSession,
return context . redirect ( '/sign-in' )
NOTE: we should probably eliminate the other session grab in the pgaes first
Add type data
(Best place for this is in workos-next
)[https://github.com/workos/authkit-nextjs/blob/main/src/interfaces.ts ]
import {defineMiddleware} from 'astro:middleware'
import match from 'picomatch'
import {WorkOS} from '@workos-inc/node'
import type {User} from '@workos-inc/node'
import {createRemoteJWKSet, jwtVerify} from 'jose'
import {sealData, unsealData} from 'iron-session'
export const onRequest = defineMiddleware (
async ( context , next ) => {
const session : Session = await unsealData (cookie.value, {
password: import . meta .env. WORKOS_COOKIE_PASSWORD ,