Skip to main content

epic-auth

Guide on authentication, sessions, OAuth, 2FA, and passkeys for Epic Stack

Source facts

Repository
epicweb-dev/epic-stack
Last source activity
January 30, 2026 at 00:59
Detected SKILL.md language
English
Stars
5,538
Forks
463

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
epic-auth
description
Guide on authentication, sessions, OAuth, 2FA, and passkeys for Epic Stack
categories
["authentication","sessions","oauth","security"]
# Epic Stack: Authentication ## When to use this skill Use this skill when you need to: - Implement user authentication - Work with sessions and cookies - Configure OAuth providers (GitHub, Google, etc.) - Implement 2FA (Two-Factor Authentication) - Implement WebAuthn/Passkeys - Handle login, signup, logout flows - Manage email verification - Implement password reset ## Patterns and conventions ### Authentication Philosophy Following Epic Web principles: **Least privilege** - Users should only have access to what they need, when they need it. Sessions should have minimal permissions and expire appropriately. Don't grant more access than necessary. **Design to fail fast and early** - Validate authentication and authorization as early as possible. Check session validity immediately, verify permissions before processing requests, and return clear errors quickly. **Example - Least privilege in sessions:** ```typescript // ✅ Good - Minimal session data, explicit permissions const session = await prisma.session.create({ data: { expirationDate: getSessionExpirationDate(), userId, // Only store user ID, not full user data }, }) // Session only grants access to this specific user // Permissions checked separately when needed // ❌ Avoid - Storing too much in session const session = await prisma.session.create({ data: { expirationDate: getSessionExpirationDate(), userId, userRole: 'admin', // Don't store roles in session permissions: ['all'], // Don't store permissions in session }, }) // Roles and permissions should be checked from database, not session ``` **Example - Fail fast authentication:** ```typescript // ✅ Good - Validate authentication early export async function loader({ request }: Route.LoaderArgs) { // Check authentication immediately - fail fast const userId = await requireUserId(request) // Check permissions early - fail fast await requireUserWithPermission(request, 'read:note:own') // Only proceed if authenticated and authorized const notes = await prisma.note.findMany({ where: { ownerId: userId }, }) return { notes } } // ❌ Avoid - Delayed authentication check export async function loader({ request }: Route.LoaderArgs) { // Process request first... const notes = await prisma.note.findMany() // Check authentication at the end - too late! const userId = await getUserId(request) if (!userId) { // Already processed request throw redirect('/login') } } ``` ### Cookie-based Sessions Epic Stack uses cookie-based sessions for authentication. Sessions are stored in the database and identified by signed cookies. **Session configuration:** ```typescript // app/utils/session.server.ts import { createCookieSessionStorage } from 'react-router' export const authSessionStorage = createCookieSessionStorage({ cookie: { name: 'en_session', sameSite: 'lax', // CSRF protection advised if changing to 'none' path: '/', httpOnly: true, secrets: process.env.SESSION_SECRET.split(','), secure: process.env.NODE_ENV === 'production', }, }) ``` ### Get current user **Server-side:** ```typescript import { getUserId, requireUserId } from '#app/utils/auth.server.ts' // Get userId or null if not authenticated const userId = await getUserId(request) // Require authenticated user (redirects to /login if not) const userId = await requireUserId(request) const userId = await requireUserId(request, { redirectTo: '/custom-login' }) // Require that user is NOT authenticated import { requireAnonymous } from '#app/utils/auth.server.ts' await requireAnonymous(request) // Redirects to / if authenticated ``` **Client-side:** ```typescript import { useOptionalUser, useUser } from '#app/utils/user.ts' // Get user or undefined if not authenticated const user = useOptionalUser() // Get authenticated user (throws error if not) const user = useUser() ``` ### Login with Email/Password **Validation schema:** ```typescript const LoginSchema = z.object({ username: UsernameSchema, password: z.string().min(1, 'Password is required'), redirectTo: z.string().optional(), remember: z.boolean().optional(), }) ``` **Login action (fail fast):** ```typescript import { login } from '#app/utils/auth.server.ts' import { handleNewSession } from './login.server.ts' export async function action({ request }: Route.ActionArgs) { const formData = await request.formData() // Validate input early - fail fast const submission = await parseWithZod(formData, { schema: LoginSchema, }) if (submission.status !== 'success') { return data({ result: submission.reply() }, { status: 400 }) } const { username, password, redirectTo, remember } = submission.value // Authenticate early - fail fast if invalid const session = await login({ username, password }) if (!session) { // Return error immediately - don't process further return data( { result: submission.reply({ formErrors: ['Invalid username or password'], }), }, { status: 400 }, ) } // Only create session if authentication succeeded return handleNewSession({ request, session, redirectTo, remember: remember ?? false, }) } ``` ### Signup with Email/Password **Signup action:** ```typescript import { signup } from '#app/utils/auth.server.ts' export async function action({ request }: Route.ActionArgs) { const formData = await request.formData() // Validate form... const session = await signup({ email, username, password, name, }) // Handle session and redirect... } ``` ### OAuth Providers (GitHub, Google, etc.) Epic Stack uses `remix-auth` for OAuth providers. **Configure provider (GitHub example):** ```typescript // app/utils/providers/github.server.ts import { GitHubStrategy } from 'remix-auth-github' export class GitHubProvider implements AuthProvider { getAuthStrategy() { return new GitHubStrategy( { clientID: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, callbackURL: '/auth/github/callback', }, async ({ profile }) => { // Return user profile return { id: profile.id, email: profile.emails[0].value, username: profile.displayName, name: profile.displayName, } }, ) } } ``` **Callback handler:** ```typescript // app/routes/_auth/auth.$provider/callback.ts export async function loader({ request, params }: Route.LoaderArgs) { const providerName = ProviderNameSchema.parse(params.provider) const authResult = await authenticator.authenticate(providerName, request) if (!authResult.success) { throw redirectWithToast('/login', { title: 'Auth Failed', description: `Error authenticating with ${providerName}`, type: 'error', }) } const { data: profile } = authResult // Check if connection exists const existingConnection = await prisma.connection.findUnique({ where: { providerName_providerId: { providerName, providerId: String(profile.id), }, }, }) // If exists, create session if (existingConnection) { return makeSession({ request, userId: existingConnection.userId }) } // If email exists, link account const user = await prisma.user.findUnique({ where: { email: profile.email.toLowerCase() }, }) if (user) { await prisma.connection.create({ data: { providerName, providerId: String(profile.id), userId: user.id, }, }) return makeSession({ request, userId: user.id }) } // New user, go to onboarding // ... } ``` ### WebAuthn/Passkeys Epic Stack supports authentication with passkeys using WebAuthn. **Loader to generate options:** ```typescript // app/routes/_auth/webauthn/authentication.ts import { generateAuthenticationOptions } from '@simplewebauthn/server' export async function loader({ request }: Route.LoaderArgs) { const config = getWebAuthnConfig(request) const options = await generateAuthenticationOptions({ rpID: config.rpID, userVerification: 'preferred', }) const cookieHeader = await passkeyCookie.serialize({ challenge: options.challenge, }) return Response.json( { options }, { headers: { 'Set-Cookie': cookieHeader }, }, ) } ``` **Action to verify authentication:** ```typescript import { verifyAuthenticationResponse } from '@simplewebauthn/server' export async function action({ request }: Route.ActionArgs) { const cookie = await passkeyCookie.parse(request.headers.get('Cookie')) if (!cookie?.challenge) { throw new Error('Authentication challenge not found') } const { authResponse } = await request.json() const passkey = await prisma.passkey.findUnique({ where: { id: authResponse.id }, include: { user: true }, }) const verification = await verifyAuthenticationResponse({ response: authResponse, expectedChallenge: cookie.challenge, expectedOrigin: config.origin, expectedRPID: config.rpID, credential: { id: authResponse.id, publicKey: passkey.publicKey, counter: Number(passkey.counter), }, }) if (!verification.verified) { throw new Error('Authentication verification failed') } // Actualizar counter await prisma.passkey.update({ where: { id: passkey.id }, data: { counter: BigInt(verification.authenticationInfo.newCounter) }, }) // Create sesión const session = await prisma.session.create({ data: { expirationDate: getSessionExpirationDate(), userId: passkey.userId, }, }) return handleNewSession({ request, session, remember: true }) } ``` ### Two-Factor Authentication (2FA) with TOTP Epic Stack uses TOTP (Time-based One-Time Password) para 2FA. **Check if user has 2FA:** ```typescript const verification = await prisma.verification.findUnique({ where: { target_type: { target: session.userId, type: twoFAVerificationType, }, }, }) const userHasTwoFactor = Boolean(verification) ``` **Handle session with 2FA:** ```typescript export async function handleNewSession({ request, session, redirectTo, remember, }: { request: Request session: { userId: string; id: string; expirationDate: Date } redirectTo?: string remember: boolean }) { const verification = await prisma.verification.findUnique({ where: { target_type: { target: session.userId, type: twoFAVerificationType, }, }, }) const userHasTwoFactor = Boolean(verification) if (userHasTwoFactor) { // Save unverified session const verifySession = await verifySessionStorage.getSession() verifySession.set(unverifiedSessionIdKey, session.id) verifySession.set(rememberKey, remember) // Redirect to 2FA verification const redirectUrl = getRedirectToUrl({ request, type: twoFAVerificationType, target: session.userId, redirectTo, }) return redirect(redirectUrl.toString(), { headers: { 'set-cookie': await verifySessionStorage.commitSession(verifySession), }, }) } else { // User without 2FA, create session directly const authSession = await authSessionStorage.getSession( request.headers.get('cookie'), ) authSession.set(sessionKey, session.id) return redirect(safeRedirect(redirectTo), { headers: { 'set-cookie': await authSessionStorage.commitSession(authSession, { expires: remember ? session.expirationDate : undefined, }), }, }) } } ``` **Verify 2FA code:** ```typescript import { prepareTOTP, verifyTOTP } from '@epic-web/totp' export async function action({ request }: Route.ActionArgs) { const formData = await request.formData() const submission = await parseWithZod(formData, { schema: VerifySchema, }) if (submission.status !== 'success') { return data({ result: submission.reply() }, { status: 400 }) } const { code } = submission.value const verifySession = await verifySessionStorage.getSession( request.headers.get('cookie'), ) const target = verifySession.get(targetKey) const type = verifySession.get(typeKey) if (!target || !type) { throw redirect('/login') } const verification = await prisma.verification.findUnique({ where: { target_type: { target, type } }, select: { secret: true, algorithm: true, period: true, digits: true, }, }) if (!verification) { throw redirect('/login') } const isValid = verifyTOTP({ otp: code, secret: verification.secret,
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub