Skip to content

remix/auth

The remix/auth package provides authentication providers and flow functions for verifying user identity. It supports credentials-based login, OAuth 2.0 with built-in providers for popular services, and a generic OIDC provider for any OpenID Connect-compatible identity provider.

Installation

The remix/auth package is included with Remix. No additional installation is required.

ts
import {
  createCredentialsAuthProvider,
  createGoogleAuthProvider,
  createGitHubAuthProvider,
  createMicrosoftAuthProvider,
  createFacebookAuthProvider,
  createXAuthProvider,
  createAuth0AuthProvider,
  createOktaAuthProvider,
  createOIDCAuthProvider,
  verifyCredentials,
  startExternalAuth,
  finishExternalAuth,
  completeAuth,
} from 'remix/auth'

Provider Factories

createCredentialsAuthProvider(options)

Creates a provider for email/password (or any custom credentials) authentication.

ts
function createCredentialsAuthProvider<T>(options: {
  name: string
  parse: (context: RequestContext) => T | Promise<T>
  verify: (credentials: T) => AuthIdentity | null | Promise<AuthIdentity | null>
}): CredentialsAuthProvider<T>

Options:

OptionTypeDescription
namestringA unique name for this provider (e.g. 'credentials').
parse(context) => TExtracts credentials from the request context. Typically reads from FormData.
verify(credentials) => AuthIdentity | nullValidates the credentials and returns a user identity object, or null on failure.

Example:

ts
import { createCredentialsAuthProvider } from 'remix/auth'
import bcrypt from 'bcrypt'

let credentialsAuth = createCredentialsAuthProvider({
  name: 'credentials',

  parse(context) {
    let data = context.get(FormData)
    return {
      email: data.get('email') as string,
      password: data.get('password') as string,
    }
  },

  async verify({ email, password }) {
    let user = await db.findOne(users, {
      where: eq(users.columns.email, email),
    })
    if (!user) return null

    let valid = await bcrypt.compare(password, user.password_hash)
    if (!valid) return null

    return { id: user.id, email: user.email, name: user.name, role: user.role }
  },
})

createGoogleAuthProvider(options)

Creates an OAuth 2.0 provider for Google authentication.

ts
function createGoogleAuthProvider(options: OAuthProviderOptions): GoogleAuthProvider

Options:

OptionTypeDescription
clientIdstringOAuth client ID from the Google Cloud Console.
clientSecretstringOAuth client secret from the Google Cloud Console.
redirectUristringThe callback URL registered with Google (e.g. http://localhost:3000/auth/google/callback).
scopesstring[]OAuth scopes to request. Common values: 'openid', 'email', 'profile'.

Google Profile Type:

ts
interface GoogleProfile {
  sub: string        // Google user ID
  email: string
  email_verified: boolean
  name: string
  given_name: string
  family_name: string
  picture: string    // Avatar URL
  locale: string
}

Example:

ts
import { createGoogleAuthProvider, startExternalAuth, finishExternalAuth } from 'remix/auth'

let googleAuth = createGoogleAuthProvider({
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  redirectUri: process.env.GOOGLE_REDIRECT_URI!,
  scopes: ['openid', 'email', 'profile'],
})

// Start the OAuth flow
router.map(googleLoginRoute, async ({ context }) => {
  return startExternalAuth(googleAuth, context)
})

// Handle the callback
router.map(googleCallbackRoute, async ({ context }) => {
  let result = await finishExternalAuth(googleAuth, context)
  if (!result) {
    return new Response('Authentication failed', { status: 401 })
  }

  // result.profile is GoogleProfile
  // result.tokens contains access_token, id_token, etc.
  let user = await findOrCreateUser(result.profile.email, result.profile.name)

  let session = context.get(Session)
  session.set('userId', user.id)

  return new Response(null, { status: 302, headers: { Location: '/' } })
})

createGitHubAuthProvider(options)

Creates an OAuth 2.0 provider for GitHub authentication.

ts
function createGitHubAuthProvider(options: OAuthProviderOptions): GitHubAuthProvider

Options:

OptionTypeDescription
clientIdstringOAuth client ID from GitHub Developer Settings.
clientSecretstringOAuth client secret from GitHub Developer Settings.
redirectUristringThe callback URL registered with GitHub.
scopesstring[]OAuth scopes. Common values: 'user:email', 'read:user'.

GitHub Profile Type:

ts
interface GitHubProfile {
  id: number
  login: string       // GitHub username
  email: string
  name: string
  avatar_url: string
  html_url: string
  bio: string | null
  company: string | null
  location: string | null
}

Example:

ts
import { createGitHubAuthProvider, startExternalAuth, finishExternalAuth } from 'remix/auth'

let githubAuth = createGitHubAuthProvider({
  clientId: process.env.GITHUB_CLIENT_ID!,
  clientSecret: process.env.GITHUB_CLIENT_SECRET!,
  redirectUri: process.env.GITHUB_REDIRECT_URI!,
  scopes: ['user:email'],
})

router.map(githubLoginRoute, async ({ context }) => {
  return startExternalAuth(githubAuth, context)
})

router.map(githubCallbackRoute, async ({ context }) => {
  let result = await finishExternalAuth(githubAuth, context)
  if (!result) {
    return new Response('Authentication failed', { status: 401 })
  }

  // result.profile is GitHubProfile
  let user = await findOrCreateUser(result.profile.email, result.profile.name)

  let session = context.get(Session)
  session.set('userId', user.id)

  return new Response(null, { status: 302, headers: { Location: '/' } })
})

createMicrosoftAuthProvider(options)

Creates an OAuth 2.0 provider for Microsoft (Azure AD / Entra ID) authentication.

ts
function createMicrosoftAuthProvider(options: OAuthProviderOptions & {
  tenant?: string
}): MicrosoftAuthProvider

Options:

OptionTypeDescription
clientIdstringApplication (client) ID from the Azure portal.
clientSecretstringClient secret from the Azure portal.
redirectUristringThe callback URL registered in Azure.
scopesstring[]OAuth scopes. Common values: 'openid', 'email', 'profile'.
tenantstringAzure AD tenant ID. Defaults to 'common' (any Microsoft account). Use a specific tenant ID for single-tenant apps.

Microsoft Profile Type:

ts
interface MicrosoftProfile {
  id: string
  displayName: string
  givenName: string
  surname: string
  mail: string
  userPrincipalName: string
}

Example:

ts
import { createMicrosoftAuthProvider, startExternalAuth, finishExternalAuth } from 'remix/auth'

let microsoftAuth = createMicrosoftAuthProvider({
  clientId: process.env.MICROSOFT_CLIENT_ID!,
  clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
  redirectUri: process.env.MICROSOFT_REDIRECT_URI!,
  scopes: ['openid', 'email', 'profile'],
  tenant: 'common',
})

router.map(microsoftLoginRoute, async ({ context }) => {
  return startExternalAuth(microsoftAuth, context)
})

router.map(microsoftCallbackRoute, async ({ context }) => {
  let result = await finishExternalAuth(microsoftAuth, context)
  if (!result) {
    return new Response('Authentication failed', { status: 401 })
  }

  let user = await findOrCreateUser(result.profile.mail, result.profile.displayName)

  let session = context.get(Session)
  session.set('userId', user.id)

  return new Response(null, { status: 302, headers: { Location: '/' } })
})

createFacebookAuthProvider(options)

Creates an OAuth 2.0 provider for Facebook authentication.

ts
function createFacebookAuthProvider(options: OAuthProviderOptions): FacebookAuthProvider

Options:

OptionTypeDescription
clientIdstringApp ID from the Facebook Developer Console.
clientSecretstringApp secret from the Facebook Developer Console.
redirectUristringThe callback URL registered with Facebook.
scopesstring[]OAuth scopes. Common values: 'email', 'public_profile'.

Facebook Profile Type:

ts
interface FacebookProfile {
  id: string
  name: string
  email: string
  picture: {
    data: {
      url: string
      width: number
      height: number
    }
  }
}

Example:

ts
import { createFacebookAuthProvider, startExternalAuth, finishExternalAuth } from 'remix/auth'

let facebookAuth = createFacebookAuthProvider({
  clientId: process.env.FACEBOOK_CLIENT_ID!,
  clientSecret: process.env.FACEBOOK_CLIENT_SECRET!,
  redirectUri: process.env.FACEBOOK_REDIRECT_URI!,
  scopes: ['email', 'public_profile'],
})

router.map(facebookLoginRoute, async ({ context }) => {
  return startExternalAuth(facebookAuth, context)
})

router.map(facebookCallbackRoute, async ({ context }) => {
  let result = await finishExternalAuth(facebookAuth, context)
  if (!result) {
    return new Response('Authentication failed', { status: 401 })
  }

  let user = await findOrCreateUser(result.profile.email, result.profile.name)

  let session = context.get(Session)
  session.set('userId', user.id)

  return new Response(null, { status: 302, headers: { Location: '/' } })
})

createXAuthProvider(options)

Creates an OAuth 2.0 provider for X (formerly Twitter) authentication.

ts
function createXAuthProvider(options: OAuthProviderOptions): XAuthProvider

Options:

OptionTypeDescription
clientIdstringOAuth 2.0 client ID from the X Developer Portal.
clientSecretstringOAuth 2.0 client secret from the X Developer Portal.
redirectUristringThe callback URL registered with X.
scopesstring[]OAuth scopes. Common values: 'users.read', 'tweet.read'.

X Profile Type:

ts
interface XProfile {
  id: string
  name: string
  username: string          // @handle
  profile_image_url: string
  description: string
}

Example:

ts
import { createXAuthProvider, startExternalAuth, finishExternalAuth } from 'remix/auth'

let xAuth = createXAuthProvider({
  clientId: process.env.X_CLIENT_ID!,
  clientSecret: process.env.X_CLIENT_SECRET!,
  redirectUri: process.env.X_REDIRECT_URI!,
  scopes: ['users.read', 'tweet.read'],
})

router.map(xLoginRoute, async ({ context }) => {
  return startExternalAuth(xAuth, context)
})

router.map(xCallbackRoute, async ({ context }) => {
  let result = await finishExternalAuth(xAuth, context)
  if (!result) {
    return new Response('Authentication failed', { status: 401 })
  }

  let user = await findOrCreateUser(result.profile.username, result.profile.name)

  let session = context.get(Session)
  session.set('userId', user.id)

  return new Response(null, { status: 302, headers: { Location: '/' } })
})

createAuth0AuthProvider(options)

Creates an OAuth 2.0 / OIDC provider for Auth0 authentication.

ts
function createAuth0AuthProvider(options: OAuthProviderOptions & {
  domain: string
}): Auth0AuthProvider

Options:

OptionTypeDescription
domainstringYour Auth0 domain (e.g. 'yourapp.auth0.com').
clientIdstringApplication client ID from the Auth0 dashboard.
clientSecretstringApplication client secret from the Auth0 dashboard.
redirectUristringThe callback URL registered in Auth0.
scopesstring[]OAuth scopes. Common values: 'openid', 'email', 'profile'.

Auth0 Profile Type:

ts
interface Auth0Profile {
  sub: string
  email: string
  email_verified: boolean
  name: string
  nickname: string
  picture: string
  updated_at: string
}

Example:

ts
import { createAuth0AuthProvider, startExternalAuth, finishExternalAuth } from 'remix/auth'

let auth0Auth = createAuth0AuthProvider({
  domain: process.env.AUTH0_DOMAIN!,
  clientId: process.env.AUTH0_CLIENT_ID!,
  clientSecret: process.env.AUTH0_CLIENT_SECRET!,
  redirectUri: process.env.AUTH0_REDIRECT_URI!,
  scopes: ['openid', 'email', 'profile'],
})

router.map(auth0LoginRoute, async ({ context }) => {
  return startExternalAuth(auth0Auth, context)
})

router.map(auth0CallbackRoute, async ({ context }) => {
  let result = await finishExternalAuth(auth0Auth, context)
  if (!result) {
    return new Response('Authentication failed', { status: 401 })
  }

  let user = await findOrCreateUser(result.profile.email, result.profile.name)

  let session = context.get(Session)
  session.set('userId', user.id)

  return new Response(null, { status: 302, headers: { Location: '/' } })
})

createOktaAuthProvider(options)

Creates an OAuth 2.0 / OIDC provider for Okta authentication.

ts
function createOktaAuthProvider(options: OAuthProviderOptions & {
  domain: string
}): OktaAuthProvider

Options:

OptionTypeDescription
domainstringYour Okta domain (e.g. 'yourorg.okta.com').
clientIdstringApplication client ID from the Okta admin console.
clientSecretstringApplication client secret from the Okta admin console.
redirectUristringThe callback URL registered in Okta.
scopesstring[]OAuth scopes. Common values: 'openid', 'email', 'profile'.

Okta Profile Type:

ts
interface OktaProfile {
  sub: string
  email: string
  email_verified: boolean
  name: string
  given_name: string
  family_name: string
  locale: string
  zoneinfo: string
}

Example:

ts
import { createOktaAuthProvider, startExternalAuth, finishExternalAuth } from 'remix/auth'

let oktaAuth = createOktaAuthProvider({
  domain: process.env.OKTA_DOMAIN!,
  clientId: process.env.OKTA_CLIENT_ID!,
  clientSecret: process.env.OKTA_CLIENT_SECRET!,
  redirectUri: process.env.OKTA_REDIRECT_URI!,
  scopes: ['openid', 'email', 'profile'],
})

router.map(oktaLoginRoute, async ({ context }) => {
  return startExternalAuth(oktaAuth, context)
})

router.map(oktaCallbackRoute, async ({ context }) => {
  let result = await finishExternalAuth(oktaAuth, context)
  if (!result) {
    return new Response('Authentication failed', { status: 401 })
  }

  let user = await findOrCreateUser(result.profile.email, result.profile.name)

  let session = context.get(Session)
  session.set('userId', user.id)

  return new Response(null, { status: 302, headers: { Location: '/' } })
})

createOIDCAuthProvider(options)

Creates a generic OpenID Connect provider for any OIDC-compliant identity provider.

ts
function createOIDCAuthProvider(options: OAuthProviderOptions & {
  name: string
  issuer: string
}): OIDCAuthProvider

Options:

OptionTypeDescription
namestringA unique name for this provider (e.g. 'corporate-sso').
issuerstringThe OIDC issuer URL. Used to discover endpoints via /.well-known/openid-configuration.
clientIdstringOAuth client ID from the identity provider.
clientSecretstringOAuth client secret from the identity provider.
redirectUristringThe callback URL registered with the identity provider.
scopesstring[]OAuth scopes to request. Typically 'openid', 'email', 'profile'.

The OIDC provider automatically discovers the authorization endpoint, token endpoint, and userinfo endpoint from the issuer's .well-known/openid-configuration document.

Example:

ts
import { createOIDCAuthProvider, startExternalAuth, finishExternalAuth } from 'remix/auth'

let corporateAuth = createOIDCAuthProvider({
  name: 'corporate-sso',
  issuer: 'https://sso.yourcompany.com',
  clientId: process.env.SSO_CLIENT_ID!,
  clientSecret: process.env.SSO_CLIENT_SECRET!,
  redirectUri: process.env.SSO_REDIRECT_URI!,
  scopes: ['openid', 'email', 'profile'],
})

router.map(ssoLoginRoute, async ({ context }) => {
  return startExternalAuth(corporateAuth, context)
})

router.map(ssoCallbackRoute, async ({ context }) => {
  let result = await finishExternalAuth(corporateAuth, context)
  if (!result) {
    return new Response('Authentication failed', { status: 401 })
  }

  // result.profile follows the standard OIDC claims structure
  let user = await findOrCreateUser(result.profile.email, result.profile.name)

  let session = context.get(Session)
  session.set('userId', user.id)

  return new Response(null, { status: 302, headers: { Location: '/' } })
})

Flow Functions

verifyCredentials(provider, context)

Verifies credentials against a credentials auth provider. Extracts credentials from the context using the provider's parse function, then validates them using the provider's verify function.

ts
function verifyCredentials<T>(
  provider: CredentialsAuthProvider<T>,
  context: RequestContext,
): Promise<AuthIdentity | null>

Returns the user identity object on success, or null if the credentials are invalid.

Example:

ts
import { verifyCredentials } from 'remix/auth'
import { Session } from 'remix/session'

router.map(loginAction, async ({ context }) => {
  let result = await verifyCredentials(credentialsAuth, context)

  if (!result) {
    return showLoginForm('Invalid email or password.')
  }

  let session = context.get(Session)
  session.set('userId', result.id)
  session.regenerateId()

  return new Response(null, { status: 302, headers: { Location: '/' } })
})

startExternalAuth(provider, context)

Initiates an OAuth 2.0 redirect flow. Generates a state parameter for CSRF protection, stores it in the session, and returns a redirect Response to the provider's authorization endpoint.

ts
function startExternalAuth(
  provider: ExternalAuthProvider,
  context: RequestContext,
): Response | Promise<Response>

Returns a Response with status 302 and a Location header pointing to the provider's authorization URL.

Example:

ts
import { startExternalAuth } from 'remix/auth'

router.map(googleLoginRoute, async ({ context }) => {
  return startExternalAuth(googleAuth, context)
})

finishExternalAuth(provider, context)

Handles the OAuth 2.0 callback. Extracts the authorization code and state from the request URL, verifies the state against the session, exchanges the code for tokens, and fetches the user's profile from the provider.

ts
function finishExternalAuth(
  provider: ExternalAuthProvider,
  context: RequestContext,
): Promise<OAuthResult | null>

Returns an OAuthResult on success, or null if authentication failed (invalid state, expired code, provider error, etc.).

Example:

ts
import { finishExternalAuth } from 'remix/auth'

router.map(googleCallbackRoute, async ({ context }) => {
  let result = await finishExternalAuth(googleAuth, context)

  if (!result) {
    return new Response('Authentication failed', { status: 401 })
  }

  // Use result.profile and result.tokens
  let user = await findOrCreateUser(result.profile.email, result.profile.name)

  let session = context.get(Session)
  session.set('userId', user.id)

  return new Response(null, { status: 302, headers: { Location: '/' } })
})

completeAuth(context)

Completes the authentication flow and returns the current session. This is a convenience function that finalizes any pending auth state in the context and returns the session for further manipulation.

ts
function completeAuth(context: RequestContext): Session

Example:

ts
import { completeAuth } from 'remix/auth'

router.map(callbackRoute, async ({ context }) => {
  let result = await finishExternalAuth(provider, context)
  if (!result) {
    return new Response('Authentication failed', { status: 401 })
  }

  let session = completeAuth(context)
  session.set('userId', result.profile.sub)

  return new Response(null, { status: 302, headers: { Location: '/' } })
})

Types

OAuthResult

The result returned by finishExternalAuth on success.

ts
interface OAuthResult {
  provider: string      // The provider name (e.g. 'google', 'github')
  profile: OAuthAccount // Provider-specific profile data
  tokens: OAuthTokens   // OAuth tokens
}

OAuthTokens

OAuth tokens received from the provider after exchanging the authorization code.

ts
interface OAuthTokens {
  access_token: string
  token_type: string        // Usually 'Bearer'
  expires_in?: number       // Token lifetime in seconds
  refresh_token?: string    // Present if offline access was requested
  id_token?: string         // Present for OIDC providers
  scope?: string            // Granted scopes
}

OAuthAccount

A union type representing the provider-specific profile. The actual shape depends on which provider was used (see the profile types listed under each provider factory above).

OAuthProviderOptions

The base options shared by all OAuth provider factories.

ts
interface OAuthProviderOptions {
  clientId: string
  clientSecret: string
  redirectUri: string
  scopes: string[]
}

Released under the MIT License.