Skip to content

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

What You Will Build

  1. A session auth scheme that resolves the current user from the session cookie
  2. Route protection that redirects unauthenticated browser users to /login
  3. An API key auth scheme for programmatic access
  4. 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.

ts
// 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:

ts
// 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:

ts
// 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:

ts
// 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).

ts
// 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:

ts
// 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:

bash
curl -H "X-API-Key: sk_live_abc123" http://localhost:3000/api/data

Step 5: Create an API-Only Auth Guard

For API endpoints, you want a 401 response instead of a redirect:

ts
// 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:

ts
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:

ts
// 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:

ts
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:

ts
// 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):

  1. Log in via the login form
  2. Visit a protected route -- you should see your profile
  3. Log out and visit the same route -- you should be redirected to /login

API key auth:

bash
# 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:

bash
# 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

ConceptWhat It Does
auth({ schemes })Tries each scheme in order, sets Auth context key
createSessionAuthSchemeReads user ID from session, verifies against database
createAPIAuthSchemeReads API key from a custom header, verifies against database
requireAuthBlocks unauthenticated requests with a redirect or 401
context.get(Auth)Reads the auth state (ok, identity, method)
Custom role middlewareChecks 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.

Released under the MIT License.