### Start Development Server Source: https://github.com/workos/workos-remix/blob/main/README.md Starts the Remix development server, allowing you to preview the application locally. The app will be accessible at http://localhost:3000. ```bash npm run dev ``` -------------------------------- ### Install Dependencies with npm Source: https://github.com/workos/workos-remix/blob/main/README.md Installs project dependencies using npm. This is a standard step for setting up Node.js projects. ```bash git clone https://github.com/workos/remix-mfa cd remix-mfa npm install ``` -------------------------------- ### Initialize Prisma Database Source: https://github.com/workos/workos-remix/blob/main/README.md Pushes the Prisma schema to the database, creating the necessary tables. This command initializes the SQLite database file for the project. ```bash npx prisma db push ``` -------------------------------- ### WorkOS Server Library Source: https://github.com/workos/workos-remix/blob/main/README.md Initializes the WorkOS SDK with the API key. This library is used to interact with the WorkOS API for MFA functionalities. ```typescript import WorkOS from '@workos/node'; const workos = new WorkOS(process.env.WORKOS_API_KEY); export default workos; ``` -------------------------------- ### Session Management Utility Source: https://github.com/workos/workos-remix/blob/main/README.md Handles session creation, retrieval, and destruction using secure cookies. This utility is essential for maintaining user authentication state. ```typescript import { createCookieSessionStorage } from '@remix-run/node'; export const sessionStorage = createCookieSessionStorage({ cookie: { name: '_session', // all the same rules that apply to express sessions apply here secret: process.env.SESSION_SECRET, // secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 60 * 60 * 24 * 7, // 1 week path: '/', sameSite: 'lax', }, }); export async function getSession(request: Request) { const cookie = request.headers.get('cookie'); return sessionStorage.getSession(cookie); } export async function destroySession(request: Request) { const session = await getSession(request); return sessionStorage.destroySession(session); } ``` -------------------------------- ### Configure Environment Variables Source: https://github.com/workos/workos-remix/blob/main/README.md Sets up necessary environment variables for the application, including WorkOS API key and session secret. This is crucial for authentication and API integration. ```bash cp .env.example .env WORKOS_API_KEY='' # Generate a random secret for SESSION_SECRET openssl rand -base64 32 ``` -------------------------------- ### Prisma Schema Configuration Source: https://github.com/workos/workos-remix/blob/main/README.md Defines the database schema using Prisma's declarative language. This file specifies the structure of your database tables and their relationships. ```prisma datasource db { provider = "sqlite" url = "file:./prisma/data.db" } generator client { provider = "prisma-client-js" } model User { id String @id @default(cuid()) email String @unique passwordHash String mfaEnabled Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } ``` -------------------------------- ### Define User and Password Models with Prisma Source: https://context7.com/workos/workos-remix/llms.txt Defines the database schema for a Remix application using Prisma and SQLite. It includes a 'User' model with fields for authentication and MFA tracking, and a 'Password' model for storing hashed passwords. Setup commands for database migration and studio are provided. ```prisma // prisma/schema.prisma datasource db { provider = "sqlite" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } model User { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt email String @unique password Password? phoneNumber String? totpFactorId String? smsFactorId String? } model Password { hash String user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String @unique } // Setup commands: // npx prisma db push # Create database and tables // npx prisma studio # Open database GUI ``` -------------------------------- ### Initialize WorkOS SDK for MFA in Node.js Source: https://context7.com/workos/workos-remix/llms.txt Sets up the WorkOS SDK client for Node.js applications, requiring the WORKOS_API_KEY from environment variables. The initialized 'workos' instance provides methods for MFA operations like enrolling, challenging, and verifying factors. ```typescript // app/lib/workos.server.ts import WorkOS from '@workos-inc/node'; export const workos = new WorkOS(process.env.WORKOS_API_KEY); // The workos instance provides access to MFA operations: // - workos.mfa.enrollFactor({ type: 'totp' | 'sms', ... }) // - workos.mfa.challengeFactor({ authenticationFactorId: string }) // - workos.mfa.verifyFactor({ authenticationChallengeId: string, code: string }) // Environment configuration (.env file): // WORKOS_API_KEY='sk_test_...' # Get from WorkOS dashboard // SESSION_SECRET='random_string' # Generate with: openssl rand -base64 32 // DATABASE_URL='file:./data.db' # SQLite database location ``` -------------------------------- ### User Login with MFA using WorkOS Source: https://context7.com/workos/workos-remix/llms.txt Handles user authentication via email and password, followed by MFA if enabled. Supports TOTP and SMS factors, automatically creating challenges. Verifies MFA codes and creates user sessions upon successful authentication. Dependencies include 'verifyLogin' from './user.server', 'workos' from './lib/workos.server', and 'createUserSession' from './utils/session.server'. ```typescript // app/routes/login.tsx import { verifyLogin } from '~/models/user.server'; import { workos } from '~/lib/workos.server'; import { createUserSession } from '~/utils/session.server'; export const action: ActionFunction = async ({ request }) => { const formData = await request.formData(); const { _action, ...values } = Object.fromEntries(formData); switch (_action) { case 'login': // Verify credentials const user = await verifyLogin(values.email as string, values.password); if (!user) { return json({ errors: { email: 'Invalid email or password' } }, { status: 400 }); } // If no MFA enabled, create session immediately if (!user.totpFactorId && !user.smsFactorId) { return createUserSession({ request, userId: user.id, remember: values.remember === 'on', redirectTo: '/', }); } // If TOTP enabled, create authentication challenge if (user.totpFactorId) { const authenticationChallenge = await workos.mfa.challengeFactor({ authenticationFactorId: user.totpFactorId, }); return { userId: user.id, authenticationChallenge, totpFactorId: user.totpFactorId, }; } // If SMS enabled, create SMS challenge if (user.smsFactorId) { const authenticationChallenge = await workos.mfa.challengeFactor({ authenticationFactorId: user.smsFactorId, }); return { userId: user.id, authenticationChallenge, smsFactorId: user.smsFactorId, phoneNumber: user.phoneNumber.substring(user.phoneNumber.length - 3), }; } case 'verify': // Verify the MFA code const response = await workos.mfa.verifyFactor({ authenticationChallengeId: values.authenticationChallengeId as string, code: values.authenticationCode as string, }); if (!response.valid) { return json({ errors: { verificationCode: 'Invalid verification code' } }, { status: 400 }); } // Create session after successful MFA verification return createUserSession({ request, userId: values.userId as string, remember: values.remember === 'on', redirectTo: '/', }); } }; // Usage: POST to /login // Step 1: { _action: 'login', email: 'user@example.com', password: 'pass123' } // Response: Returns authenticationChallenge if MFA enabled, or redirects if not // Step 2: { _action: 'verify', authenticationChallengeId: 'challenge_123', authenticationCode: '123456' } // Response: Redirects to / with session cookie if code is valid ``` -------------------------------- ### User Registration Endpoint in Remix Source: https://context7.com/workos/workos-remix/llms.txt Handles new user account creation via email and password. It validates email format and password length, checks for duplicate users, hashes the password, creates a new user record, and establishes a session for the newly registered user. This endpoint expects form data containing email and password. ```typescript // app/routes/signup.tsx import { createUser, getUserByEmail } from '~/models/user.server'; import { createUserSession } from '~/utils/session.server'; import { validateEmail } from '~/utils/validation.server'; export const action: ActionFunction = async ({ request }) => { const formData = await request.formData(); const email = formData.get('email') as string; const password = formData.get('password') as string; const redirectTo = formData.get('redirectTo') || '/'; // Validate email format if (!validateEmail(email)) { return json({ errors: { email: 'Email is invalid' } }, { status: 400 }); } // Validate password length if (!password || password.length < 8) { return json({ errors: { password: 'Password must be at least 8 characters' } }, { status: 400 }); } // Check if user already exists const existingUser = await getUserByEmail(email); if (existingUser) { return json({ errors: { email: 'A user already exists with this email' } }, { status: 400 }); } // Create user with hashed password const user = await createUser(email, password); // Create session and redirect return createUserSession({ request, userId: user.id, remember: false, redirectTo, }); }; // Usage: POST to /signup // Form data: { email: 'user@example.com', password: 'securepassword123' } // Response: Redirects to / with session cookie set ``` -------------------------------- ### Manage User Settings with Remix Actions and Loaders Source: https://context7.com/workos/workos-remix/llms.txt Handles user account settings like password changes, 2FA configuration, and account deletion using Remix loaders and actions. It requires user authentication and interacts with server-side models for data operations. Errors are returned with appropriate status codes. ```typescript // app/routes/settings.tsx import { updatePassword, disable2FA, deleteUser, getUserAuthFactors } from '~/models/user.server'; import { requireUser, requireUserId } from '~/utils/session.server'; import { displayToast } from '~/utils/displayToast.server'; export const loader: LoaderFunction = async ({ request }) => { const userId = await requireUserId(request); const userAuthFactors = await getUserAuthFactors(userId); return userAuthFactors; // { smsFactorId: string | null, totpFactorId: string | null } }; export const action: ActionFunction = async ({ request }) => { const user = await requireUser(request); const { session } = await getSession(request); const formData = await request.formData(); const { _action } = Object.fromEntries(formData); switch (_action) { case 'updatePassword': const currentPassword = formData.get('currentPassword') as string; const newPassword = formData.get('newPassword') as string; const result = await updatePassword(user.id, currentPassword, newPassword); if (result) { displayToast(session, 'Your password has been updated successfully', 'success'); return redirect('/settings', { headers: { 'Set-Cookie': await cookieSessionStorage.commitSession(session), }, }); } return json({ errors: { message: 'Current password is incorrect' } }, { status: 400 }); case 'toggle2FA': if (!user.totpFactorId && !user.smsFactorId) { // No 2FA enabled, redirect to setup return redirect('/settings/two-factor-authentication'); } // Disable 2FA await disable2FA(user.id); displayToast(session, 'You have successfully disabled 2FA', 'success'); return redirect('/settings', { headers: { 'Set-Cookie': await cookieSessionStorage.commitSession(session), }, }); case 'deleteAccount': await deleteUser(user.id); return redirect('/signup'); } }; // Usage examples: // Change password: POST { _action: 'updatePassword', currentPassword: 'old', newPassword: 'new123' } // Toggle 2FA: POST { _action: 'toggle2FA' } // Delete account: POST { _action: 'deleteAccount' } ``` -------------------------------- ### Session Management Utilities in Remix Source: https://context7.com/workos/workos-remix/llms.txt Provides essential utilities for managing user sessions in a Remix application using secure cookie-based storage. Includes functions for creating sessions, requiring authentication before accessing routes, and logging out users. Relies on '@remix-run/node' for session handling and './user.server' for user data retrieval. Environment variables like SESSION_SECRET are crucial for security. ```typescript // app/utils/session.server.ts import { createCookieSessionStorage } from '@remix-run/node'; import { getUserById } from '~/models/user.server'; export const cookieSessionStorage = createCookieSessionStorage({ cookie: { name: '__session', httpOnly: true, path: '/', sameSite: 'lax', secrets: [process.env.SESSION_SECRET], secure: process.env.NODE_ENV === 'production', }, }); // Get current user ID from session export async function getUserId(request: Request): Promise { const { session } = await getSession(request); return session.get('userId'); } // Require authentication, redirect to login if not authenticated export async function requireUserId(request: Request, redirectTo?: string): Promise { const userId = await getUserId(request); if (!userId) { const searchParams = new URLSearchParams([['redirectTo', redirectTo || new URL(request.url).pathname]]); throw redirect(`/login?${searchParams}`); } return userId; } // Get full user object, ensuring they're authenticated export async function requireUser(request: Request) { const userId = await requireUserId(request); const user = await getUserById(userId); if (user) return user; throw await logout(request); } // Log out user by destroying session export async function logout(request: Request) { const { session } = await getSession(request); return redirect('/', { headers: { 'Set-Cookie': await cookieSessionStorage.destroySession(session), }, }); } // Usage in a protected route: export const loader: LoaderFunction = async ({ request }) => { const user = await requireUser(request); // User is guaranteed to be authenticated here return json({ email: user.email }); }; ``` -------------------------------- ### Create User with Hashed Password Source: https://context7.com/workos/workos-remix/llms.txt Creates a new user in the database with a securely hashed password using bcryptjs. It assumes a Prisma schema for user and password data. ```typescript import bcrypt from 'bcryptjs'; import { prisma } from '~/lib/prisma.server'; // Create new user with hashed password export async function createUser(email: string, password: string) { const hashedPassword = await bcrypt.hash(password, 10); return await prisma.user.create({ data: { email, password: { create: { hash: hashedPassword, }, }, }, }); } ``` -------------------------------- ### Enroll and Verify MFA Factors with WorkOS in Remix Source: https://context7.com/workos/workos-remix/llms.txt This TypeScript code handles the backend logic for Multi-Factor Authentication (MFA) enrollment using WorkOS within a Remix application. It supports both TOTP and SMS factor types, including QR code generation for TOTP and phone number verification for SMS. It interfaces with WorkOS SDK and local user models for persistence. ```typescript // app/routes/settings.two-factor-authentication.tsx import { workos } from '~/lib/workos.server'; import { enrollAuthenticationFactor, updatePhoneNumber } from '~/models/user.server'; import { requireUser } from '~/utils/session.server'; export const action: ActionFunction = async ({ request }) => { const user = await requireUser(request); const formData = await request.formData(); const { _action, ...values } = Object.fromEntries(formData); switch (_action) { case 'selectFactor': if (values.authenticationFactorType === 'totp') { // Enroll TOTP factor and get QR code const authenticationFactor = await workos.mfa.enrollFactor({ type: 'totp', issuer: 'Remix with WorkOS', user: user.email, }); const authenticationChallenge = await workos.mfa.challengeFactor({ authenticationFactorId: authenticationFactor.id, }); return { authenticationFactor, authenticationChallenge, step: 1 }; } // SMS factor requires phone number input return { setupSms: true, step: 1 }; case 'phoneNumber': // Enroll SMS factor with phone number const authenticationFactor = await workos.mfa.enrollFactor({ type: 'sms', phoneNumber: values.phoneNumber as string, }); const authenticationChallenge = await workos.mfa.challengeFactor({ authenticationFactorId: authenticationFactor.id, }); await updatePhoneNumber(user.id, values.phoneNumber as string); return { setupSms: true, authenticationFactor, authenticationChallenge, step: 1, }; case 'verify': // Verify the code from authenticator app or SMS const response = await workos.mfa.verifyFactor({ authenticationChallengeId: values.authenticationChallengeId as string, code: values.authenticationCode as string, }); if (!response.valid) { return { errors: { verificationCode: 'Invalid verification code, please try again' }, authenticationFactor: { type: values.authenticationFactorType }, authenticationChallenge: { id: values.authenticationChallengeId }, step: 1, }; } // Save factor ID to user record await enrollAuthenticationFactor(user.id, { smsFactorId: values.authenticationFactorType === 'sms' ? response.challenge.authentication_factor_id : undefined, totpFactorId: values.authenticationFactorType === 'totp' ? response.challenge.authentication_factor_id : undefined, }); return { step: 2 }; // Success, show confirmation } }; // Usage flow: // 1. POST { _action: 'selectFactor', authenticationFactorType: 'totp' } // Response: { authenticationFactor: { totp: { qr_code: 'data:image/png;base64,...' } } } // 2. User scans QR code with authenticator app // 3. POST { _action: 'verify', authenticationChallengeId: 'challenge_123', authenticationCode: '123456' } // Response: { step: 2 } on success ``` -------------------------------- ### Enroll User in MFA Factors Source: https://context7.com/workos/workos-remix/llms.txt Enrolls a user in Multi-Factor Authentication (MFA) by associating their account with specific factor IDs (e.g., TOTP, SMS). This operation is performed using Prisma to update the user record. ```typescript // Enroll user in MFA factors export async function enrollAuthenticationFactor( userId: string, factors: { totpFactorId?: string; smsFactorId?: string } ) { return await prisma.user.update({ where: { id: userId }, data: { totpFactorId: factors.totpFactorId, smsFactorId: factors.smsFactorId, }, }); } ``` -------------------------------- ### Verify User Login Credentials Source: https://context7.com/workos/workos-remix/llms.txt Verifies a user's login credentials by comparing the provided password with the stored hashed password. It retrieves user data including the password hash from the database using Prisma. ```typescript // Verify login credentials export async function verifyLogin(email: string, password: string) { const userWithPassword = await prisma.user.findUnique({ where: { email }, include: { password: true }, }); if (!userWithPassword || !userWithPassword.password) { return null; } const isValid = await bcrypt.compare(password, userWithPassword.password.hash); if (!isValid) { return null; } const { password: _password, ...userWithoutPassword } = userWithPassword; return userWithoutPassword; } ``` -------------------------------- ### Update User Password Source: https://context7.com/workos/workos-remix/llms.txt Updates a user's password after verifying their current password. This function hashes the new password using bcryptjs and updates the user's record in the database via Prisma. ```typescript // Update user password export async function updatePassword(userId: string, currentPassword: string, newPassword: string) { const userWithPassword = await prisma.user.findUnique({ where: { id: userId }, include: { password: true }, }); if (!userWithPassword || !userWithPassword.password) { return null; } const isValid = await bcrypt.compare(currentPassword, userWithPassword.password.hash); if (!isValid) { return null; } const hashedPassword = await bcrypt.hash(newPassword, 10); return await prisma.user.update({ where: { id: userId }, data: { password: { update: { hash: hashedPassword, }, }, }, }); } ``` -------------------------------- ### Disable All 2FA Factors Source: https://context7.com/workos/workos-remix/llms.txt Disables all two-factor authentication (2FA) factors for a given user by setting their associated factor IDs to null. This uses Prisma to update the user's record. ```typescript // Disable all 2FA factors export async function disable2FA(userId: string) { return await prisma.user.update({ where: { id: userId }, data: { smsFactorId: null, totpFactorId: null, }, }); } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.