Skip to main content

epic-auth

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

Informations de source

Dépôt
epicweb-dev/epic-stack
Dernière activité de la source
30 janvier 2026 à 00:59
Langue détectée de SKILL.md
anglais
Étoiles
5 538
Forks
463

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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,
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub