auth-flows
Skill1 starsChanged 8 months agoMITView on GitHub
Authentication patterns for React apps — Auth.js (NextAuth), Clerk, Supabase Auth. Covers session management, protected routes, role-based access, and OAuth flows.
- Reads credentials
- Installs packages
2 other projects use a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- PMDevSolutions/Aureliusedited, first seen Oct 24, 2026
- LucSena/how-to-build-softwaretheir own version, first seen Oct 11, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 1
- PMDevSolutions/Aurelius★ 9100% same
Wrote their own version · 1
- LucSena/how-to-build-software★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository9 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- animation-motionSkill
- form-handlingSkill
- react-accessibilitySkill
- react-component-developmentSkill
- react-performance-optimizationSkill
- react-testing-workflowsSkill
- seo-metadataSkill
- state-managementSkill
The file
Files
Authentication Flows
Decision Tree — Choosing an Auth Provider
Which framework?
├── Next.js
│ ├── Need full control / self-hosted? → Auth.js v5
│ ├── Want managed UI + user management? → Clerk
│ └── Already using Supabase DB? → Supabase Auth
├── Vite / Remix / SPA
│ ├── Want drop-in components? → Clerk
│ └── Using Supabase backend? → Supabase Auth| Provider | Best For | Hosting | UI Components |
| Auth.js v5 | Full control, self-hosted, Next.js | Self-hosted | Custom (build your own) |
| Clerk | Fast setup, managed users, any framework | Managed SaaS | Built-in (SignIn, SignUp, UserButton) |
| Supabase Auth | Supabase stack, row-level security | Supabase cloud / self-hosted | Headless (build your own) |
1. Auth.js v5 (NextAuth) — Next.js
Installation
pnpm add next-auth@beta @auth/prisma-adapterConfiguration
// auth.ts (project root)
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import { z } from "zod";
import bcrypt from "bcryptjs";
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const parsed = loginSchema.safeParse(credentials);
if (!parsed.success) return null;
const user = await prisma.user.findUnique({
where: { email: parsed.data.email },
});
if (!user?.hashedPassword) return null;
const valid = await bcrypt.compare(
parsed.data.password,
user.hashedPassword
);
if (!valid) return null;
return { id: user.id, email: user.email, name: user.name, role: user.role };
},
}),
],
callbacks: {
async jwt({ token, user }) {
// Attach role to JWT on sign-in
if (user) {
token.role = user.role;
}
return token;
},
async session({ session, token }) {
// Expose role in client session
if (session.user) {
session.user.id = token.sub!;
session.user.role = token.role as string;
}
return session;
},
},
pages: {
signIn: "/login",
error: "/auth/error",
newUser: "/onboarding",
},
session: {
strategy: "jwt",
},
});Type Augmentation
// types/next-auth.d.ts
import { type DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
role: string;
} & DefaultSession["user"];
}
interface User {
role: string;
}
}
declare module "next-auth/jwt" {
interface JWT {
role: string;
}
}Route Handler
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;Middleware for Protected Routes
// middleware.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
const protectedPaths = ["/dashboard", "/settings", "/admin"];
const adminPaths = ["/admin"];
export default auth((req) => {
const { pathname } = req.nextUrl;
const isProtected = protectedPaths.some((p) => pathname.startsWith(p));
const isAdmin = adminPaths.some((p) => pathname.startsWith(p));
if (isProtected && !req.auth) {
const loginUrl = new URL("/login", req.url);
loginUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(loginUrl);
}
if (isAdmin && req.auth?.user?.role !== "admin") {
return NextResponse.redirect(new URL("/unauthorized", req.url));
}
return NextResponse.next();
});
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*", "/admin/:path*"],
};Server Component Access
// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {session.user.name}</p>
<p>Role: {session.user.role}</p>
</div>
);
}Client Component Access
// components/UserNav.tsx
"use client";
import { useSession, signIn, signOut } from "next-auth/react";
export function UserNav() {
const { data: session, status } = useSession();
if (status === "loading") {
return <div aria-busy="true">Loading...</div>;
}
if (!session) {
return (
<button onClick={() => signIn()}>Sign In</button>
);
}
return (
<div className="flex items-center gap-4">
<span>{session.user.name}</span>
<button onClick={() => signOut({ callbackUrl: "/" })}>
Sign Out
</button>
</div>
);
}Session Provider (Layout)
// app/layout.tsx
import { SessionProvider } from "next-auth/react";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<SessionProvider>{children}</SessionProvider>
</body>
</html>
);
}2. Clerk — Any Framework
Installation
pnpm add @clerk/nextjs
# or for Vite:
# pnpm add @clerk/clerk-reactEnvironment Variables
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-upMiddleware
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isPublicRoute = createRouteMatcher([
"/",
"/sign-in(.*)",
"/sign-up(.*)",
"/api/webhooks(.*)",
]);
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect();
}
});
export const config = {
matcher: [
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
"/(api|trpc)(.*)",
],
};Layout Provider
// app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}Built-in Components
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from "@clerk/nextjs";
export default function SignInPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<SignIn />
</div>
);
}// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from "@clerk/nextjs";
export default function SignUpPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<SignUp />
</div>
);
}// components/Header.tsx
import { SignedIn, SignedOut, UserButton, SignInButton } from "@clerk/nextjs";
export function Header() {
return (
<header className="flex items-center justify-between p-4">
<h1>My App</h1>
<nav>
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
<SignedOut>
<SignInButton mode="modal" />
</SignedOut>
</nav>
</header>
);
}Server-Side Auth
// app/dashboard/page.tsx
import { auth, currentUser } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const { userId } = await auth();
if (!userId) {
redirect("/sign-in");
}
const user = await currentUser();
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user?.firstName}</p>
</div>
);
}3. Supabase Auth
Installation
pnpm add @supabase/supabase-js @supabase/ssrBrowser Client
// lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}Server Client (Next.js App Router)
// lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// Ignored in Server Components (read-only)
}
},
},
}
);
}Middleware
// middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
);
supabaseResponse = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
},
},
}
);
const {
data: { user },
} = await supabase.auth.getUser();
// Redirect unauthenticated users from protected routes
if (
!user &&
!request.nextUrl.pathname.startsWith("/login") &&
!request.nextUrl.pathname.startsWith("/auth") &&
request.nextUrl.pathname.startsWith("/dashboard")
) {
const url = request.nextUrl.clone();
url.pathname = "/login";
return NextResponse.redirect(url);
}
return supabaseResponse;
}Sign In / Sign Up
// app/login/page.tsx
"use client";
import { createClient } from "@/lib/supabase/client";
import { useState } from "react";
export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const supabase = createClient();
async function handleSignIn(e: React.FormEvent) {
e.preventDefault();
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
setError(error.message);
} else {
window.location.href = "/dashboard";
}
}
async function handleOAuth(provider: "github" | "google") {
await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
}
return (
<form onSubmit={handleSignIn}>
{error && <p role="alert" className="text-red-600">{error}</p>}
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">Sign In</button>
<div className="flex gap-2 mt-4">
<button type="button" onClick={() => handleOAuth("github")}>
Continue with GitHub
</button>
<button type="button" onClick={() => handleOAuth("google")}>
Continue with Google
</button>
</div>
</form>
);
}4. Role-Based Access Control (RBAC)
A framework-agnostic RBAC pattern that works with any auth provider.
// lib/auth/permissions.ts
export const roles = {
admin: {
permissions: [
"users:read",
"users:write",
"users:delete",
"posts:read",
"posts:write",
"posts:delete",
"settings:read",
"settings:write",
],
},
editor: {
permissions: [
"posts:read",
"posts:write",
"posts:delete",
"users:read",
],
},
viewer: {
permissions: [
"posts:read",
"users:read",
],
},
} as const;
export type Role = keyof typeof roles;
export type Permission =
(typeof roles)[Role]["permissions"][number];
export function hasPermission(
userRole: Role,
permission: Permission
): boolean {
const role = roles[userRole];
return (role.permissions as readonly string[]).includes(permission);
}
export function requirePermission(
userRole: Role,
permission: Permission
): void {
if (!hasPermission(userRole, permission)) {
throw new Error(
`Role "${userRole}" does not have permission "${permission}"`
);
}
}RequirePermission Component
// components/RequirePermission.tsx
"use client";
import { type ReactNode } from "react";
import { hasPermission, type Permission, type Role } from "@/lib/auth/permissions";
interface RequirePermissionProps {
role: Role;
permission: Permission;
children: ReactNode;
fallback?: ReactNode;
}
export function RequirePermission({
role,
permission,
children,
fallback = null,
}: RequirePermissionProps) {
if (!hasPermission(role, permission)) {
return <>{fallback}</>;
}
return <>{children}</>;
}Usage:
<RequirePermission
role={session.user.role}
permission="users:delete"
fallback={<p>You do not have permission to manage users.</p>}
>
<UserManagementPanel />
</RequirePermission>Server-Side Permission Check
// app/admin/users/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import { requirePermission, type Role } from "@/lib/auth/permissions";
export default async function AdminUsersPage() {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
try {
requirePermission(session.user.role as Role, "users:write");
} catch {
redirect("/unauthorized");
}
return <UserManagementPanel />;
}5. Security Rules
These rules are NON-NEGOTIABLE for any auth implementation:
| Rule | Reason |
Never store tokens in localStorage | XSS attacks can steal them. Use httpOnly cookies. |
Always use httpOnly cookies for sessions | Cannot be accessed by client-side JavaScript. |
| Validate sessions server-side | Never trust client-side session data for authorization decisions. |
| Implement CSRF protection | Auth.js and Next.js include CSRF tokens automatically. Verify for custom APIs. |
| Rate limit auth endpoints | Prevent brute force attacks. Use middleware or API route rate limiting. |
| Hash passwords with bcrypt (cost 12+) | Never store plaintext or weakly-hashed passwords. |
| Rotate refresh tokens | Invalidate old tokens when issuing new ones to limit exposure. |
| Validate redirect URLs | Only allow redirects to trusted origins. Never redirect to user-supplied URLs without validation. |
Use secure and sameSite cookie flags | secure: true in production, sameSite: "lax" minimum. |
| Log authentication events | Record sign-ins, failures, and password resets for audit trails. |
Redirect URL Validation
// lib/auth/validate-redirect.ts
const ALLOWED_HOSTS = [
process.env.NEXT_PUBLIC_APP_URL,
"localhost:3000",
];
export function isValidRedirectUrl(url: string): boolean {
try {
const parsed = new URL(url, process.env.NEXT_PUBLIC_APP_URL);
return ALLOWED_HOSTS.some(
(host) => parsed.origin === new URL(`https://${host}`).origin
);
} catch {
return false;
}
}
export function getSafeRedirect(url: string | null, fallback = "/"): string {
if (!url) return fallback;
return isValidRedirectUrl(url) ? url : fallback;
}Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

