Remix
Remix

auth-middleware

Request-time authentication and route protection for Remix. Use this package to resolve identity into context.get(Auth) from sessions, bearer tokens, API keys, or your own schemes. Pair it with remix/auth when you need browser login routes that call verifyCredentials() or finishExternalAuth(), then rotate the session id with completeAuth() before writing the auth record.

Features

  • Request auth resolution without mutating request objects
  • Route protection with requireAuth() and configurable failure behavior
  • Built-in auth schemes for sessions, bearer tokens, and API keys
  • Ordered fallback across multiple auth schemes
  • Public and private route support with the same resolved auth state
  • Designed to pair with browser login flows that persist session auth records earlier in the request lifecycle

Installation

npm i remix

Usage

The following example shows the request-time half of a session-backed browser login flow:

  • another part of the app has already called completeAuth() and written { userId } into the returned session
  • remix/auth-middleware reads that value, resolves the current user, and protects the dashboard route
import { auth, Auth, createSessionAuthScheme, requireAuth } from 'remix/auth-middleware'
import { createRouter } from 'remix/fetch-router'
import { route } from 'remix/routes'
import type { GoodAuth } from 'remix/auth-middleware'
import { session } from 'remix/session-middleware'

let routes = route({
  app: {
    dashboard: '/dashboard',
  },
})

let router = createRouter({
  middleware: [
    session(sessionCookie, sessionStorage),
    auth({
      schemes: [
        createSessionAuthScheme({
          read(session) {
            return session.get('auth') as { userId: string } | null
          },
          verify(value) {
            return users.getById(value.userId)
          },
          invalidate(session) {
            session.unset('auth')
          },
        }),
      ],
    }),
  ],
})

router.get(routes.app.dashboard, {
  middleware: [requireAuth()],
  handler(context) {
    let auth = context.get(Auth) as GoodAuth<{ id: string; email: string }>

    return Response.json({
      id: auth.identity.id,
      email: auth.identity.email,
      method: auth.method,
    })
  },
})

In this example, createSessionAuthScheme() turns a persisted session auth record back into request auth state, auth() stores that state at context.get(Auth), and requireAuth() rejects anonymous requests.

If you need to create the login route, start an OAuth redirect, finish a provider callback, or write the session auth record in the first place, use remix/auth:

  • verifyCredentials() for direct credentials flows
  • startExternalAuth() and finishExternalAuth() for OAuth and OIDC flows
  • completeAuth() to rotate the session id before writing the auth record that this package reads later

Route Protection

This package includes two middlewares:

  • auth() to resolve auth state and store it in context.get(Auth)
  • requireAuth() to reject requests that aren't authenticated

That separation is intentional so the same auth resolution can support public routes, API routes, and browser routes with different failure behavior.

auth() resolves auth state and stores either { ok: true, identity, method } or { ok: false, error? } in context.get(Auth).

Use requireAuth() after auth() when a route must be authenticated. If auth() did not run first, requireAuth() throws. Otherwise it returns 401 Unauthorized by default, or you can replace that with onFailure(context, auth) to return JSON, redirects, or any other custom response.

Auth challenges are forwarded to WWW-Authenticate automatically when the auth failure included a challenge, so clients that honor those challenges can react without custom header handling.

Auth Schemes

An AuthScheme is any object with a name and an authenticate(context) method. The auth() middleware tries each scheme in order until one returns a success or failure result. If no scheme returns success or failure, the request is treated as anonymous.

This package ships with three built-in auth schemes:

Custom Auth Schemes

If none of the built-in auth schemes match your environment, you can create your own auth scheme easily. A custom scheme usually wraps one auth mechanism behind a small create* factory function and returns an AuthScheme. For example, apps behind a trusted access proxy can authenticate requests from forwarded identity headers instead of sessions or bearer tokens.

import type { RequestContext } from 'remix/fetch-router'
import type { AuthScheme } from 'remix/auth-middleware'

type User = {
  id: string
  role: 'admin' | 'user'
}

function createTrustedProxyAuthScheme(): AuthScheme<User> {
  return {
    name: 'trusted-proxy',
    async authenticate(context: RequestContext) {
      let email = context.headers.get('X-Forwarded-Email')

      if (email == null) {
        return
      }

      let user = await users.getByEmail(email)

      if (user == null) {
        return {
          status: 'failure',
          code: 'invalid_credentials',
          message: 'Unknown forwarded user',
        }
      }

      return {
        status: 'success',
        identity: user,
      }
    },
  }
}

Only use a scheme like this when the app is reachable exclusively through infrastructure you trust to set the headers you rely on. In this case, the X-Forwarded-Email header.

authenticate(context) can return:

  • null, undefined, or no return value to skip this scheme
  • { status: 'success', identity } to authenticate the request
  • { status: 'failure', code?, message?, challenge? } to stop with an auth error

The scheme name becomes auth.method when authentication succeeds.

Simple Auth Cookies

If your app already has an auth cookie and you do not need a session-backed identity lookup, you can use a small custom auth scheme and still rely on requireAuth() for route protection.

import { auth, requireAuth } from 'remix/auth-middleware'
import type { AuthScheme } from 'remix/auth-middleware'
import { createCookie } from 'remix/cookie'
import { createRouter } from 'remix/fetch-router'
import { redirect } from 'remix/response/redirect'

let authCookie = createCookie('__auth', {
  httpOnly: true,
  sameSite: 'lax',
  path: '/',
})

let authCookieScheme: AuthScheme<'demo-user'> = {
  name: 'auth-cookie',
  async authenticate(context) {
    let value = await authCookie.parse(context.headers.get('Cookie'))
    if (value !== '1') {
      return
    }

    return {
      status: 'success',
      identity: 'demo-user',
    }
  },
}

let requireAuthCookie = requireAuth<'demo-user'>({
  onFailure(context) {
    let isFrameRequest = context.request.headers.get('X-Remix-Frame') === 'true'
    if (isFrameRequest) {
      return new Response('<p>Not authorized</p>', {
        status: 401,
        headers: {
          'Content-Type': 'text/html; charset=utf-8',
        },
      })
    }

    return redirect('/login')
  },
})

let router = createRouter({
  middleware: [
    auth({
      schemes: [authCookieScheme],
    }),
  ],
})

router.get('/dashboard', {
  middleware: [requireAuthCookie],
  handler() {
    return new Response('ok')
  },
})

This pattern keeps the auth check app-owned. Use remix/session-middleware and remix/auth when you need server-managed session data, credential verification helpers, or OAuth/OIDC flows.

Related Packages

  • auth - Browser auth primitives for credentials, OAuth, and OIDC flows
  • fetch-router - Router and middleware runtime
  • response - Response helpers like redirects

Related Work

License

See LICENSE