From atum-stack-web
Authentication providers pattern library for modern web apps — Auth0 (enterprise SSO, Universal Login, Actions, rules), Clerk (React-first, pre-built UI components, organizations, invitations), NextAuth.js / Auth.js (open-source flexible, OAuth providers, custom credentials), Supabase Auth (Postgres + Row Level Security integration, magic links, OAuth), Lucia Auth (TypeScript-first, self-managed sessions, framework-agnostic), WebAuthn / Passkeys (passwordless FIDO2 via @simplewebauthn), and OAuth 2.0 + PKCE flows. Use when implementing authentication on a web project: choosing a provider, setting up social login (Google, GitHub, Microsoft, Apple), magic links, multi-factor authentication, protected routes, role-based access control, or migrating between auth providers. Differentiates from backend-only auth patterns by covering the full frontend + backend integration for Next.js, Remix, Nuxt, and Astro.
npx claudepluginhub arnwaldn/atum-plugins-collection --plugin atum-stack-webThis skill uses the workspace's default tool permissions.
Patterns pour l'authentification sur projets web modernes. **Ne pas réinventer l'auth** — utiliser un provider battle-tested (Auth0, Clerk, NextAuth, Supabase, Lucia).
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides agent creation for Claude Code plugins with file templates, frontmatter specs (name, description, model), triggering examples, system prompts, and best practices.
Patterns pour l'authentification sur projets web modernes. Ne pas réinventer l'auth — utiliser un provider battle-tested (Auth0, Clerk, NextAuth, Supabase, Lucia).
Client B2C avec besoin simple (email + Google/Apple) ?
├── Budget zéro + control total → NextAuth.js / Auth.js
├── React-first + UI pré-faites → Clerk
└── Postgres déjà utilisé + RLS → Supabase Auth
Client B2B enterprise (SSO, SAML, audit logs) ?
├── Budget confortable → Auth0 ou WorkOS
└── Self-hosted requis → Ory Kratos / Keycloak
Besoin passwordless / passkeys ?
├── Full custom → Lucia Auth + @simplewebauthn
└── Intégré → Clerk (passkeys natifs 2024) ou Auth0 (add-on)
Multi-tenant SaaS ?
├── Simple → Clerk (organizations natives)
└── Complexe → Auth0 ou custom avec Lucia
npm install @auth0/nextjs-auth0
// app/api/auth/[auth0]/route.ts
import { handleAuth } from '@auth0/nextjs-auth0'
export const GET = handleAuth()
// middleware.ts
import type { NextRequest } from 'next/server'
import { withMiddlewareAuthRequired } from '@auth0/nextjs-auth0/edge'
export default withMiddlewareAuthRequired()
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
}
// app/layout.tsx
import { UserProvider } from '@auth0/nextjs-auth0/client'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<UserProvider>{children}</UserProvider>
</body>
</html>
)
}
// Utilisation côté client
'use client'
import { useUser } from '@auth0/nextjs-auth0/client'
export function UserMenu() {
const { user, isLoading } = useUser()
if (isLoading) return null
if (!user) return <a href="/api/auth/login">Log in</a>
return (
<div>
<span>{user.email}</span>
<a href="/api/auth/logout">Log out</a>
</div>
)
}
AUTH0_SECRET=<32+ char random>
AUTH0_BASE_URL=https://example.com
AUTH0_ISSUER_BASE_URL=https://YOUR-TENANT.auth0.com
AUTH0_CLIENT_ID=xxx
AUTH0_CLIENT_SECRET=xxx
// Post-Login Action
exports.onExecutePostLogin = async (event, api) => {
if (!event.user.email_verified) {
api.access.deny('Please verify your email first')
}
api.idToken.setCustomClaim(
'https://example.com/roles',
event.user.app_metadata.roles ?? ['user']
)
}
npm install @clerk/nextjs
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html>
<body>{children}</body>
</html>
</ClerkProvider>
)
}
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/account(.*)'])
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) auth().protect()
})
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs'
export default function Page() {
return <SignIn />
}
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs'
export default function Page() {
return <SignUp />
}
// Layout header
import { UserButton, SignedIn, SignedOut, SignInButton } from '@clerk/nextjs'
<SignedIn>
<UserButton />
</SignedIn>
<SignedOut>
<SignInButton mode="modal" />
</SignedOut>
import { auth, currentUser } from '@clerk/nextjs/server'
export default async function Dashboard() {
const { userId } = auth()
if (!userId) return null
const user = await currentUser()
return <h1>Hello, {user?.firstName}</h1>
}
import { OrganizationSwitcher, CreateOrganization } from '@clerk/nextjs'
<OrganizationSwitcher afterCreateOrganizationUrl="/dashboard" />
npm install next-auth@beta
// auth.ts
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
import GitHub from 'next-auth/providers/github'
import Credentials from 'next-auth/providers/credentials'
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
Credentials({
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
const user = await findUserByEmail(credentials?.email as string)
if (!user) return null
const valid = await verifyPassword(user.passwordHash, credentials?.password as string)
if (!valid) return null
return { id: user.id, email: user.email, name: user.name }
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) token.role = user.role
return token
},
async session({ session, token }) {
if (token && session.user) {
session.user.role = token.role as string
}
return session
},
},
pages: {
signIn: '/sign-in',
},
})
// app/api/auth/[...nextauth]/route.ts
export { GET, POST } from '@/auth'
// Utilisation côté serveur
import { auth } from '@/auth'
export default async function Dashboard() {
const session = await auth()
if (!session?.user) return <a href="/sign-in">Log in</a>
return <h1>Hello, {session.user.email}</h1>
}
// Côté client
'use client'
import { useSession, signIn, signOut } from 'next-auth/react'
export function UserMenu() {
const { data: session } = useSession()
if (!session) return <button onClick={() => signIn()}>Sign in</button>
return <button onClick={() => signOut()}>Sign out</button>
}
npm install @supabase/supabase-js @supabase/ssr
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: any) {
cookieStore.set({ name, value, ...options })
},
remove(name: string, options: any) {
cookieStore.set({ name, value: '', ...options })
},
},
}
)
}
// Usage RSC
import { createClient } from '@/lib/supabase/server'
export default async function Dashboard() {
const supabase = createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return null
// Les queries Supabase respectent automatiquement les RLS policies
const { data: posts } = await supabase.from('posts').select()
return <div>{user.email}</div>
}
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'https://example.com/auth/callback',
},
})
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: 'https://example.com/auth/callback' },
})
npm install lucia oslo
// lib/auth.ts
import { Lucia } from 'lucia'
import { DrizzleSQLiteAdapter } from '@lucia-auth/adapter-drizzle'
import { db, sessionTable, userTable } from './db'
const adapter = new DrizzleSQLiteAdapter(db, sessionTable, userTable)
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === 'production',
},
},
getUserAttributes: (attrs) => ({
email: attrs.email,
username: attrs.username,
}),
})
declare module 'lucia' {
interface Register {
Lucia: typeof lucia
DatabaseUserAttributes: {
email: string
username: string
}
}
}
Lucia est plus low-level — utile si besoin de contrôle total sur le schéma DB, les sessions, et le flow.
npm install @simplewebauthn/server @simplewebauthn/browser
// app/api/webauthn/register/options/route.ts
import { generateRegistrationOptions } from '@simplewebauthn/server'
export async function POST(request: Request) {
const { userId, userEmail } = await request.json()
const options = await generateRegistrationOptions({
rpName: 'My App',
rpID: 'example.com',
userID: new TextEncoder().encode(userId),
userName: userEmail,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'preferred',
userVerification: 'preferred',
},
})
// Stocker options.challenge en DB pour le vérifier ensuite
await storeChallenge(userId, options.challenge)
return Response.json(options)
}
// Côté client
import { startRegistration } from '@simplewebauthn/browser'
const opts = await fetch('/api/webauthn/register/options', { method: 'POST' }).then(r => r.json())
const credential = await startRegistration(opts)
await fetch('/api/webauthn/register/verify', {
method: 'POST',
body: JSON.stringify(credential),
})
Pour les apps mobiles ou desktop sans backend, utiliser le flow OAuth 2.0 + PKCE :
code_verifier (random 43-128 chars)code_challenge = base64url(SHA256(code_verifier))/authorize?code_challenge=...&code_challenge_method=S256code_verifier contre un access token/sign-in → bruteforcestate → CSRF sur l'OAuth/sign-in, /sign-up, /forgot-passwordatum-stack-backendatum-stack-mobileshopify-expertwordpress-expert + plugins (MemberPress, Restrict Content Pro)security-reviewer