agentleFS
PMDevSolutionsPMDevSolutions/Aurelius/.claude/skills/auth-flows/SKILL.md

auth-flows

Skill9 starsChanged 19 days 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. Keywords: authentication, auth, login, sign in, sign up, session, protected route, authorization, OAuth, clerk, supabase auth, nextauth

  • Reads credentials
  • Installs packages

2 other projects use a version of this, 1 of them edited.

Adopted by

Most recently taken up by

The file

Files
SKILL.md

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
ProviderBest ForHostingUI Components
Auth.js v5Full control, self-hosted, Next.jsSelf-hostedCustom (build your own)
ClerkFast setup, managed users, any frameworkManaged SaaSBuilt-in (SignIn, SignUp, UserButton)
Supabase AuthSupabase stack, row-level securitySupabase cloud / self-hostedHeadless (build your own)

1. Auth.js v5 (NextAuth) — Next.js

Installation

pnpm add next-auth@beta @auth/prisma-adapter

Configuration

// 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-react

Environment 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-up

Middleware

// 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/ssr

Browser 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:

RuleReason
Never store tokens in localStorageXSS attacks can steal them. Use httpOnly cookies.
Always use httpOnly cookies for sessionsCannot be accessed by client-side JavaScript.
Validate sessions server-sideNever trust client-side session data for authorization decisions.
Implement CSRF protectionAuth.js and Next.js include CSRF tokens automatically. Verify for custom APIs.
Rate limit auth endpointsPrevent 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 tokensInvalidate old tokens when issuing new ones to limit exposure.
Validate redirect URLsOnly allow redirects to trusted origins. Never redirect to user-supplied URLs without validation.
Use secure and sameSite cookie flagssecure: true in production, sameSite: "lax" minimum.
Log authentication eventsRecord 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;
}

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.