remix/auth-middleware
The remix/auth-middleware package provides middleware that determines the current user's identity on every request. It does not block requests on its own --- it attaches authentication state to the context. Use requireAuth to enforce authentication on specific routes.
Installation
The remix/auth-middleware package is included with Remix. No additional installation is required.
import {
auth,
requireAuth,
createSessionAuthScheme,
createBearerTokenAuthScheme,
createAPIAuthScheme,
Auth,
} from 'remix/auth-middleware'auth(options)
Creates middleware that determines the current user's identity by trying each configured auth scheme in order. The first scheme that returns a valid identity wins. If no scheme succeeds, the auth state is set to { ok: false }.
function auth(options: {
schemes: AuthScheme[]
}): MiddlewareOptions:
| Option | Type | Description |
|---|---|---|
schemes | AuthScheme[] | An array of auth schemes to try, in order. |
The middleware sets the Auth context key, which downstream middleware and handlers can read.
Example:
import { auth, createSessionAuthScheme, createBearerTokenAuthScheme } from 'remix/auth-middleware'
let authMiddleware = auth({
schemes: [sessionScheme, bearerScheme],
})
let router = createRouter({
middleware: [
session(sessionCookie, sessionStorage),
authMiddleware,
],
})Auth Context Key
The Auth context key provides access to the authentication state set by the auth() middleware.
import { Auth } from 'remix/auth-middleware'
let authState = context.get(Auth)Auth Type
The value of Auth is a discriminated union:
type AuthState =
| { ok: true; identity: AuthIdentity; method: string }
| { ok: false }When authentication succeeds (ok: true):
| Property | Type | Description |
|---|---|---|
ok | true | Indicates the user is authenticated. |
identity | AuthIdentity | The user identity object returned by the scheme's verify function. |
method | string | The name of the auth scheme that succeeded (e.g. 'session', 'bearer'). |
When authentication fails (ok: false):
| Property | Type | Description |
|---|---|---|
ok | false | Indicates no user was authenticated. |
Example:
import { Auth } from 'remix/auth-middleware'
router.map(homeRoute, async ({ context }) => {
let authState = context.get(Auth)
if (authState.ok) {
return html`<p>Welcome, ${authState.identity.name}!</p>`
}
return html`<p>Please <a href="/login">log in</a>.</p>`
})requireAuth(options)
Creates middleware that blocks unauthenticated requests. If no auth scheme succeeded, it either redirects to a login page (for browser requests) or returns a 401 response (for API requests).
function requireAuth(options?: {
onUnauthenticated?: (request: Request) => Response | Promise<Response>
}): MiddlewareOptions:
| Option | Type | Description |
|---|---|---|
onUnauthenticated | (request) => Response | Optional. Called when the request is not authenticated. Return a Response (e.g. a redirect or 401). If omitted, returns a plain 401 Unauthorized response. |
Example with redirect:
import { requireAuth } from 'remix/auth-middleware'
let requireLogin = requireAuth({
onUnauthenticated(request) {
let url = new URL(request.url)
return new Response(null, {
status: 302,
headers: {
Location: `/login?returnTo=${encodeURIComponent(url.pathname)}`,
},
})
},
})
// Apply to individual routes
router.map(profileRoute, { middleware: [requireLogin] }, async ({ context }) => {
let authState = context.get(Auth)
// authState.ok is guaranteed to be true here
return html`<h1>Hello, ${authState.identity.name}!</h1>`
})Example with 401 response (for APIs):
let requireApiAuth = requireAuth()
router.map(apiRoute, { middleware: [requireApiAuth] }, async ({ context }) => {
let authState = context.get(Auth)
return Response.json({ user: authState.identity })
})Auth Schemes
createSessionAuthScheme(options)
Creates an auth scheme that reads credentials from a session cookie. This is the most common scheme for browser-based web applications.
function createSessionAuthScheme<T>(options: {
name: string
read: (session: Session) => T | null
verify: (value: T) => AuthIdentity | null | Promise<AuthIdentity | null>
}): AuthSchemeOptions:
| Option | Type | Description |
|---|---|---|
name | string | A unique name for this scheme (e.g. 'session'). Used as the method in AuthState. |
read | (session) => T | null | Extracts an identifier (e.g. user ID) from the session. Return null if no identifier is found. |
verify | (value) => AuthIdentity | null | Looks up the full user identity from the identifier. Return null if the user no longer exists. |
Example:
import { createSessionAuthScheme } from 'remix/auth-middleware'
let sessionScheme = createSessionAuthScheme({
name: 'session',
read(session) {
let userId = session.get('userId')
return userId ?? null
},
async verify(userId) {
let user = await db.findOne(users, {
where: eq(users.columns.id, userId),
})
if (!user) return null
return { id: user.id, email: user.email, name: user.name, role: user.role }
},
})createBearerTokenAuthScheme(options)
Creates an auth scheme that reads a bearer token from the Authorization header. Common for API authentication.
function createBearerTokenAuthScheme(options: {
name: string
verify: (token: string) => AuthIdentity | null | Promise<AuthIdentity | null>
}): AuthSchemeThe scheme extracts the token from the Authorization: Bearer <token> header automatically.
Options:
| Option | Type | Description |
|---|---|---|
name | string | A unique name for this scheme (e.g. 'bearer'). |
verify | (token) => AuthIdentity | null | Validates the token and returns the user identity, or null if invalid. |
Example:
import { createBearerTokenAuthScheme } from 'remix/auth-middleware'
let bearerScheme = createBearerTokenAuthScheme({
name: 'bearer',
async verify(token) {
let tokenRecord = await db.findOne(apiTokens, {
where: eq(apiTokens.columns.token, token),
})
if (!tokenRecord) return null
if (tokenRecord.expires_at && tokenRecord.expires_at < Date.now()) {
return null
}
let user = await db.findOne(users, {
where: eq(users.columns.id, tokenRecord.user_id),
})
if (!user) return null
return { id: user.id, email: user.email, name: user.name, role: user.role }
},
})Clients send requests with:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...createAPIAuthScheme(options)
Creates an auth scheme that reads an API key from a custom header.
function createAPIAuthScheme(options: {
name: string
header: string
verify: (apiKey: string) => AuthIdentity | null | Promise<AuthIdentity | null>
}): AuthSchemeOptions:
| Option | Type | Description |
|---|---|---|
name | string | A unique name for this scheme (e.g. 'api-key'). |
header | string | The HTTP header to read the API key from (e.g. 'X-API-Key'). |
verify | (apiKey) => AuthIdentity | null | Validates the API key and returns the user identity, or null if invalid. |
Example:
import { createAPIAuthScheme } from 'remix/auth-middleware'
let apiKeyScheme = createAPIAuthScheme({
name: 'api-key',
header: 'X-API-Key',
async verify(apiKey) {
let record = await db.findOne(apiKeys, {
where: eq(apiKeys.columns.key, apiKey),
})
if (!record) return null
return { id: record.user_id, name: record.label, role: 'api' }
},
})Clients send requests with:
X-API-Key: sk_live_abc123...Writing Custom Auth Schemes
An auth scheme is any object that implements the AuthScheme interface:
interface AuthScheme {
name: string
authenticate: (context: RequestContext) => AuthIdentity | null | Promise<AuthIdentity | null>
}The authenticate method receives the full request context and returns either an AuthIdentity object or null.
Example -- JWT auth scheme:
import jwt from 'jsonwebtoken'
let jwtScheme: AuthScheme = {
name: 'jwt',
async authenticate(context) {
let authHeader = context.request.headers.get('Authorization')
if (!authHeader?.startsWith('Bearer ')) return null
let token = authHeader.slice(7)
try {
let payload = jwt.verify(token, process.env.JWT_SECRET!) as {
sub: string
email: string
name: string
role: string
}
return {
id: payload.sub,
email: payload.email,
name: payload.name,
role: payload.role,
}
} catch {
return null
}
},
}Use the custom scheme like any built-in scheme:
let authMiddleware = auth({
schemes: [sessionScheme, jwtScheme],
})Combining Multiple Schemes
Pass multiple schemes to try them in order. The first scheme that returns a valid identity wins:
let authMiddleware = auth({
schemes: [sessionScheme, bearerScheme, apiKeyScheme],
})This is useful when your application serves both browser users (session cookies) and API clients (bearer tokens or API keys) from the same routes.
The method property on the AuthState tells you which scheme succeeded:
let authState = context.get(Auth)
if (authState.ok) {
console.log(`Authenticated via ${authState.method}`)
// "Authenticated via session" or "Authenticated via bearer"
}Type Helpers
WithAuth
A type helper for handler parameters that includes the Auth context key:
import type { WithAuth } from 'remix/auth-middleware'
// Use in handler type annotations
async function handler({ context }: WithAuth) {
let authState = context.get(Auth) // AuthState
}WithRequiredAuth
A type helper for handlers behind requireAuth, where authentication is guaranteed to have succeeded:
import type { WithRequiredAuth } from 'remix/auth-middleware'
// Auth is guaranteed to be { ok: true, identity, method }
async function handler({ context }: WithRequiredAuth) {
let authState = context.get(Auth)
// authState.ok is true -- no need to check
let user = authState.identity
}Complete Example
// app/auth-setup.ts
import {
auth,
requireAuth,
createSessionAuthScheme,
createBearerTokenAuthScheme,
Auth,
} from 'remix/auth-middleware'
import { Session } from 'remix/session'
// Session scheme for browser users
let sessionScheme = createSessionAuthScheme({
name: 'session',
read(session) {
return session.get('userId') ?? null
},
async verify(userId) {
let user = await db.findOne(users, {
where: eq(users.columns.id, userId),
})
if (!user) return null
return { id: user.id, email: user.email, name: user.name, role: user.role }
},
})
// Bearer token scheme for API clients
let bearerScheme = createBearerTokenAuthScheme({
name: 'bearer',
async verify(token) {
let record = await db.findOne(apiTokens, {
where: eq(apiTokens.columns.token, token),
})
if (!record) return null
let user = await db.findOne(users, {
where: eq(users.columns.id, record.user_id),
})
if (!user) return null
return { id: user.id, email: user.email, name: user.name, role: user.role }
},
})
// Auth middleware -- tries session first, then bearer
export let authMiddleware = auth({
schemes: [sessionScheme, bearerScheme],
})
// Require auth -- redirects browser users, 401s API clients
export let requireLogin = requireAuth({
onUnauthenticated(request) {
let accept = request.headers.get('Accept') ?? ''
if (accept.includes('text/html')) {
let url = new URL(request.url)
return new Response(null, {
status: 302,
headers: { Location: `/login?returnTo=${encodeURIComponent(url.pathname)}` },
})
}
return new Response('Unauthorized', { status: 401 })
},
})// app/server.ts
import { createRouter } from 'remix/fetch-router'
import { session } from 'remix/session-middleware'
import { authMiddleware, requireLogin } from './auth-setup.ts'
import { Auth } from 'remix/auth-middleware'
let router = createRouter({
middleware: [
session(sessionCookie, sessionStorage),
authMiddleware,
],
})
// Public route -- auth state is available but not required
router.map(homeRoute, async ({ context }) => {
let authState = context.get(Auth)
if (authState.ok) {
return html`<p>Welcome, ${authState.identity.name}!</p>`
}
return html`<p><a href="/login">Log in</a></p>`
})
// Protected route -- auth is required
router.map(profileRoute, { middleware: [requireLogin] }, async ({ context }) => {
let { identity } = context.get(Auth)
return html`<h1>${identity.name}</h1><p>${identity.email}</p>`
})Related
- Authentication Guide -- Full walkthrough with multiple providers
- remix/auth -- Auth providers and OAuth flow functions
- remix/session -- Session API reference
- Middleware -- How middleware works in Remix