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.
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.
function createCredentialsAuthProvider<T>(options: {
name: string
parse: (context: RequestContext) => T | Promise<T>
verify: (credentials: T) => AuthIdentity | null | Promise<AuthIdentity | null>
}): CredentialsAuthProvider<T>Options:
| Option | Type | Description |
|---|---|---|
name | string | A unique name for this provider (e.g. 'credentials'). |
parse | (context) => T | Extracts credentials from the request context. Typically reads from FormData. |
verify | (credentials) => AuthIdentity | null | Validates the credentials and returns a user identity object, or null on failure. |
Example:
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.
function createGoogleAuthProvider(options: OAuthProviderOptions): GoogleAuthProviderOptions:
| Option | Type | Description |
|---|---|---|
clientId | string | OAuth client ID from the Google Cloud Console. |
clientSecret | string | OAuth client secret from the Google Cloud Console. |
redirectUri | string | The callback URL registered with Google (e.g. http://localhost:3000/auth/google/callback). |
scopes | string[] | OAuth scopes to request. Common values: 'openid', 'email', 'profile'. |
Google Profile Type:
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:
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.
function createGitHubAuthProvider(options: OAuthProviderOptions): GitHubAuthProviderOptions:
| Option | Type | Description |
|---|---|---|
clientId | string | OAuth client ID from GitHub Developer Settings. |
clientSecret | string | OAuth client secret from GitHub Developer Settings. |
redirectUri | string | The callback URL registered with GitHub. |
scopes | string[] | OAuth scopes. Common values: 'user:email', 'read:user'. |
GitHub Profile Type:
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:
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.
function createMicrosoftAuthProvider(options: OAuthProviderOptions & {
tenant?: string
}): MicrosoftAuthProviderOptions:
| Option | Type | Description |
|---|---|---|
clientId | string | Application (client) ID from the Azure portal. |
clientSecret | string | Client secret from the Azure portal. |
redirectUri | string | The callback URL registered in Azure. |
scopes | string[] | OAuth scopes. Common values: 'openid', 'email', 'profile'. |
tenant | string | Azure AD tenant ID. Defaults to 'common' (any Microsoft account). Use a specific tenant ID for single-tenant apps. |
Microsoft Profile Type:
interface MicrosoftProfile {
id: string
displayName: string
givenName: string
surname: string
mail: string
userPrincipalName: string
}Example:
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.
function createFacebookAuthProvider(options: OAuthProviderOptions): FacebookAuthProviderOptions:
| Option | Type | Description |
|---|---|---|
clientId | string | App ID from the Facebook Developer Console. |
clientSecret | string | App secret from the Facebook Developer Console. |
redirectUri | string | The callback URL registered with Facebook. |
scopes | string[] | OAuth scopes. Common values: 'email', 'public_profile'. |
Facebook Profile Type:
interface FacebookProfile {
id: string
name: string
email: string
picture: {
data: {
url: string
width: number
height: number
}
}
}Example:
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.
function createXAuthProvider(options: OAuthProviderOptions): XAuthProviderOptions:
| Option | Type | Description |
|---|---|---|
clientId | string | OAuth 2.0 client ID from the X Developer Portal. |
clientSecret | string | OAuth 2.0 client secret from the X Developer Portal. |
redirectUri | string | The callback URL registered with X. |
scopes | string[] | OAuth scopes. Common values: 'users.read', 'tweet.read'. |
X Profile Type:
interface XProfile {
id: string
name: string
username: string // @handle
profile_image_url: string
description: string
}Example:
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.
function createAuth0AuthProvider(options: OAuthProviderOptions & {
domain: string
}): Auth0AuthProviderOptions:
| Option | Type | Description |
|---|---|---|
domain | string | Your Auth0 domain (e.g. 'yourapp.auth0.com'). |
clientId | string | Application client ID from the Auth0 dashboard. |
clientSecret | string | Application client secret from the Auth0 dashboard. |
redirectUri | string | The callback URL registered in Auth0. |
scopes | string[] | OAuth scopes. Common values: 'openid', 'email', 'profile'. |
Auth0 Profile Type:
interface Auth0Profile {
sub: string
email: string
email_verified: boolean
name: string
nickname: string
picture: string
updated_at: string
}Example:
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.
function createOktaAuthProvider(options: OAuthProviderOptions & {
domain: string
}): OktaAuthProviderOptions:
| Option | Type | Description |
|---|---|---|
domain | string | Your Okta domain (e.g. 'yourorg.okta.com'). |
clientId | string | Application client ID from the Okta admin console. |
clientSecret | string | Application client secret from the Okta admin console. |
redirectUri | string | The callback URL registered in Okta. |
scopes | string[] | OAuth scopes. Common values: 'openid', 'email', 'profile'. |
Okta Profile Type:
interface OktaProfile {
sub: string
email: string
email_verified: boolean
name: string
given_name: string
family_name: string
locale: string
zoneinfo: string
}Example:
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.
function createOIDCAuthProvider(options: OAuthProviderOptions & {
name: string
issuer: string
}): OIDCAuthProviderOptions:
| Option | Type | Description |
|---|---|---|
name | string | A unique name for this provider (e.g. 'corporate-sso'). |
issuer | string | The OIDC issuer URL. Used to discover endpoints via /.well-known/openid-configuration. |
clientId | string | OAuth client ID from the identity provider. |
clientSecret | string | OAuth client secret from the identity provider. |
redirectUri | string | The callback URL registered with the identity provider. |
scopes | string[] | 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:
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.
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:
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.
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:
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.
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:
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.
function completeAuth(context: RequestContext): SessionExample:
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.
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.
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.
interface OAuthProviderOptions {
clientId: string
clientSecret: string
redirectUri: string
scopes: string[]
}Related
- Authentication Guide -- Comprehensive guide with full examples
- remix/auth-middleware -- Middleware for reading auth state on every request
- remix/session -- Session management for storing user identity
- remix/session-middleware -- Session middleware integration