Tutorial: Protect Routes and Add API Key Auth
In this tutorial, you will set up the auth middleware to protect routes using session-based authentication, add API key authentication for programmatic access, and implement role-based access control.
Prerequisites
- A Remix V3 project with session middleware configured (see the session-middleware tutorial)
- A working login flow (see the auth tutorial)
What You Will Build
- A session auth scheme that resolves the current user from the session cookie
- Route protection that redirects unauthenticated browser users to
/login - An API key auth scheme for programmatic access
- Role-based access control for admin-only routes
Step 1: Create the Session Auth Scheme
The session auth scheme reads a user ID from the session and looks up the full user record from the database.
// app/auth/schemes.ts
import { createSessionAuthScheme } from 'remix/auth-middleware'
export let sessionScheme = createSessionAuthScheme({
name: 'session',
// Extract the user identifier from the session
read(session) {
let userId = session.get('userId')
return userId ?? null
},
// Look up the full user from the database
async verify(userId) {
let user = await db.findOne(users, {
where: eq(users.columns.id, userId),
})
if (!user) return null
// The returned object is the "identity" available in handlers
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
}
},
})The read function runs synchronously against the in-memory session data. The verify function performs the database lookup. If either returns null, this scheme is skipped and the next scheme is tried.
Step 2: Add Auth Middleware to the Router
Register the auth middleware globally so every request has an auth state:
// app/server.ts
import { createRouter } from 'remix/fetch-router'
import { session } from 'remix/session-middleware'
import { auth } from 'remix/auth-middleware'
import { sessionCookie, sessionStorage } from './session.ts'
import { sessionScheme } from './auth/schemes.ts'
let authMiddleware = auth({
schemes: [sessionScheme],
})
let router = createRouter({
middleware: [
session(sessionCookie, sessionStorage),
authMiddleware, // Must come after session middleware
],
})The auth middleware must come after the session middleware because the session scheme needs the session to be loaded first.
Step 3: Protect Routes with requireAuth
Create a requireLogin middleware that redirects unauthenticated browser users to the login page:
// app/auth/guards.ts
import { requireAuth } from 'remix/auth-middleware'
export let requireLogin = requireAuth({
onUnauthenticated(request) {
// Capture the current URL so we can redirect back after login
let url = new URL(request.url)
let returnTo = encodeURIComponent(url.pathname + url.search)
return new Response(null, {
status: 302,
headers: { Location: `/login?returnTo=${returnTo}` },
})
},
})Apply it to routes that require authentication:
// app/server.ts (continued)
import { Auth } from 'remix/auth-middleware'
import { requireLogin } from './auth/guards.ts'
// Public route -- anyone can access
router.get(homeRoute, async ({ context }) => {
let authState = context.get(Auth)
if (authState.ok) {
return new Response(`Welcome back, ${authState.identity.name}!`)
}
return new Response('Welcome, guest! <a href="/login">Log in</a>')
})
// Protected route -- redirects to /login if not authenticated
router.get(profileRoute, { middleware: [requireLogin] }, async ({ context }) => {
let { identity } = context.get(Auth)
// identity is guaranteed to exist here because requireLogin blocks unauthenticated requests
return new Response(`Profile: ${identity.name} (${identity.email})`)
})
// Protected route -- settings page
router.get(settingsRoute, { middleware: [requireLogin] }, async ({ context }) => {
let { identity } = context.get(Auth)
return new Response(`Settings for ${identity.name}`)
})Step 4: Add API Key Authentication
For API endpoints consumed by scripts or other services, add an API key auth scheme. API keys are passed in a custom header (X-API-Key).
// app/auth/schemes.ts (add to existing file)
import { createAPIAuthScheme } from 'remix/auth-middleware'
export let apiKeyScheme = createAPIAuthScheme({
name: 'api-key',
header: 'X-API-Key',
async verify(apiKey) {
// Look up the API key in the database
let record = await db.findOne(apiKeys, {
where: eq(apiKeys.columns.key, apiKey),
})
if (!record) return null
// Check if the key has been revoked or expired
if (record.revokedAt) return null
if (record.expiresAt && record.expiresAt < new Date()) return null
// Return the identity associated with this key
return {
id: record.userId,
name: record.label,
role: 'api',
}
},
})Update the auth middleware to try both schemes:
// app/server.ts (update the auth middleware)
import { sessionScheme, apiKeyScheme } from './auth/schemes.ts'
let authMiddleware = auth({
schemes: [sessionScheme, apiKeyScheme],
})The order matters: session is tried first (for browser users), then API key (for programmatic clients). The first scheme that returns an identity wins.
Now API clients can authenticate with:
curl -H "X-API-Key: sk_live_abc123" http://localhost:3000/api/dataStep 5: Create an API-Only Auth Guard
For API endpoints, you want a 401 response instead of a redirect:
// app/auth/guards.ts (add to existing file)
export let requireApiAuth = requireAuth({
onUnauthenticated() {
return Response.json(
{ error: 'Authentication required' },
{ status: 401 },
)
},
})Use it on API routes:
router.get(apiDataRoute, { middleware: [requireApiAuth] }, async ({ context }) => {
let { identity, method } = context.get(Auth)
// method tells you which scheme succeeded: 'session' or 'api-key'
return Response.json({ data: await fetchData(), authenticatedVia: method })
})Step 6: Implement Role-Based Access Control
To restrict certain routes to specific roles (e.g. admin-only), create a custom middleware that checks the identity's role:
// app/auth/guards.ts (add to existing file)
import { Auth } from 'remix/auth-middleware'
export function requireRole(role: string) {
return async function roleMiddleware(context, next) {
let authState = context.get(Auth)
if (!authState.ok) {
return new Response('Unauthorized', { status: 401 })
}
if (authState.identity.role !== role) {
return new Response('Forbidden', { status: 403 })
}
return next()
}
}Apply it to admin routes. Note that requireLogin and requireRole are separate middleware -- requireLogin ensures the user is authenticated, and requireRole checks authorization:
let requireAdmin = requireRole('admin')
// Admin-only route
router.get(adminDashboardRoute, {
middleware: [requireLogin, requireAdmin],
}, async ({ context }) => {
let { identity } = context.get(Auth)
return new Response(`Admin Dashboard - ${identity.name}`)
})
// Admin-only API route
router.get(adminApiRoute, {
middleware: [requireApiAuth, requireRole('admin')],
}, async ({ context }) => {
let stats = await getAdminStats()
return Response.json(stats)
})Step 7: Smart Auth Guard (Browser vs API)
If the same route serves both browser users and API clients, create a guard that behaves differently based on the request's Accept header:
// app/auth/guards.ts (add to existing file)
export let requireSmartAuth = requireAuth({
onUnauthenticated(request) {
let accept = request.headers.get('Accept') ?? ''
if (accept.includes('text/html')) {
// Browser request -- redirect to login
let url = new URL(request.url)
return new Response(null, {
status: 302,
headers: {
Location: `/login?returnTo=${encodeURIComponent(url.pathname)}`,
},
})
}
// API request -- return 401 JSON
return Response.json(
{ error: 'Authentication required' },
{ status: 401 },
)
},
})Step 8: Test Everything
Session auth (browser):
- Log in via the login form
- Visit a protected route -- you should see your profile
- Log out and visit the same route -- you should be redirected to
/login
API key auth:
# Authenticated request
curl -H "X-API-Key: sk_live_abc123" http://localhost:3000/api/data
# => { "data": [...], "authenticatedVia": "api-key" }
# Unauthenticated request
curl http://localhost:3000/api/data
# => { "error": "Authentication required" } (401)Role-based access:
# Non-admin user
curl -H "X-API-Key: sk_live_user_key" http://localhost:3000/admin/api
# => "Forbidden" (403)
# Admin user
curl -H "X-API-Key: sk_live_admin_key" http://localhost:3000/admin/api
# => { "stats": [...] }Summary
| Concept | What It Does |
|---|---|
auth({ schemes }) | Tries each scheme in order, sets Auth context key |
createSessionAuthScheme | Reads user ID from session, verifies against database |
createAPIAuthScheme | Reads API key from a custom header, verifies against database |
requireAuth | Blocks unauthenticated requests with a redirect or 401 |
context.get(Auth) | Reads the auth state (ok, identity, method) |
| Custom role middleware | Checks identity.role for authorization |
Next Steps
- Session Overview -- Learn about session storage and the Session API.
- API Reference -- Full documentation of every auth-middleware function and type.