Skip to main content
Technology & EngineeringAuth Services322 lines

Nextauth

Build with NextAuth.js (Auth.js) for authentication in Next.js and other frameworks.

Quick Summary34 lines
You are an auth specialist who integrates NextAuth.js (Auth.js v5) into projects.
Auth.js is an open-source, self-hosted authentication library for Next.js and other
frameworks, supporting OAuth, credentials, magic links, and database-backed sessions.

## Key Points

- Use JWT strategy for serverless (no database session lookups)
- Use database strategy when you need to revoke sessions instantly
- Extend the session via callbacks to include user ID and roles
- Use middleware for route protection — cleaner than checking in every page
- Always set `AUTH_SECRET` — required for token encryption
- Use the Credentials provider only with proper password hashing (bcrypt/argon2)
- Type-augment the Session and JWT interfaces for type safety
- Storing sensitive data in the JWT — it's readable (base64), not encrypted
- Using Credentials provider without rate limiting — vulnerable to brute force
- Not setting custom pages — the default pages are functional but unstyled
- Calling `useSession()` without `<SessionProvider>` — returns undefined
- Not handling the loading state — `status` starts as `"loading"`

## Quick Example

```bash
npm install next-auth@beta
```

```env
AUTH_SECRET=<random-32-byte-string>
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret
AUTH_GITHUB_ID=your-github-client-id
AUTH_GITHUB_SECRET=your-github-client-secret
```
skilldb get auth-services-skills/NextauthFull skill: 322 lines
Paste into your CLAUDE.md or agent config

NextAuth.js (Auth.js) Integration

You are an auth specialist who integrates NextAuth.js (Auth.js v5) into projects. Auth.js is an open-source, self-hosted authentication library for Next.js and other frameworks, supporting OAuth, credentials, magic links, and database-backed sessions.

Core Philosophy

Self-hosted, zero vendor lock-in

Auth.js runs in your application — no external auth service, no per-user pricing. You own the user data, the session storage, and the auth flow. Switch providers or databases without changing your auth code.

Provider-agnostic

Auth.js supports 80+ OAuth providers (Google, GitHub, Discord, etc.), email/magic link, and custom credentials. Adding a provider is a few lines of config. Removing one is deleting those lines.

Adapter pattern

Auth.js uses adapters to persist users and sessions to any database — Prisma, Drizzle, MongoDB, Supabase, DynamoDB. The adapter interface is simple: implement a few functions and your database is wired in.

Setup

Install (Auth.js v5)

npm install next-auth@beta

Environment variables

AUTH_SECRET=<random-32-byte-string>
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret
AUTH_GITHUB_ID=your-github-client-id
AUTH_GITHUB_SECRET=your-github-client-secret

Configuration

// 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';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from './lib/prisma';
import bcrypt from 'bcryptjs';

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    Google,
    GitHub,
    Credentials({
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      authorize: async (credentials) => {
        const user = await prisma.user.findUnique({
          where: { email: credentials.email as string },
        });
        if (!user?.hashedPassword) return null;

        const valid = await bcrypt.compare(
          credentials.password as string,
          user.hashedPassword
        );
        return valid ? user : null;
      },
    }),
  ],
  session: { strategy: 'jwt' },
  pages: {
    signIn: '/login',
    error: '/login',
  },
});

Route handler

// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';

export const { GET, POST } = handlers;

Middleware

// middleware.ts
import { auth } from './auth';

export default auth((req) => {
  if (!req.auth && req.nextUrl.pathname !== '/login') {
    const newUrl = new URL('/login', req.nextUrl.origin);
    return Response.redirect(newUrl);
  }
});

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};

Key Techniques

Server-side session

import { auth } from '@/auth';

// In Server Components
export default async function DashboardPage() {
  const session = await auth();
  if (!session?.user) redirect('/login');

  return <div>Welcome {session.user.name}</div>;
}

// In API routes
export async function GET() {
  const session = await auth();
  if (!session?.user) {
    return new Response('Unauthorized', { status: 401 });
  }

  const data = await prisma.post.findMany({
    where: { authorId: session.user.id },
  });

  return Response.json(data);
}

Client-side session

'use client';
import { useSession, signIn, signOut } from 'next-auth/react';

function AuthButton() {
  const { data: session, status } = useSession();

  if (status === 'loading') return <div>Loading...</div>;

  if (session) {
    return (
      <div>
        <p>{session.user?.email}</p>
        <button onClick={() => signOut()}>Sign out</button>
      </div>
    );
  }

  return (
    <div>
      <button onClick={() => signIn('google')}>Sign in with Google</button>
      <button onClick={() => signIn('github')}>Sign in with GitHub</button>
    </div>
  );
}

Session provider (layout)

// app/layout.tsx
import { SessionProvider } from 'next-auth/react';
import { auth } from '@/auth';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const session = await auth();

  return (
    <html>
      <body>
        <SessionProvider session={session}>
          {children}
        </SessionProvider>
      </body>
    </html>
  );
}

Callbacks (customize tokens and sessions)

export const { handlers, auth, signIn, signOut } = NextAuth({
  // ...providers, adapter
  callbacks: {
    // Add user ID and role to JWT
    jwt({ token, user }) {
      if (user) {
        token.id = user.id;
        token.role = user.role;
      }
      return token;
    },
    // Add user ID and role to session
    session({ session, token }) {
      session.user.id = token.id as string;
      session.user.role = token.role as string;
      return session;
    },
    // Control who can sign in
    signIn({ user, account }) {
      if (account?.provider === 'credentials' && !user) return false;
      return true;
    },
    // Control redirects
    redirect({ url, baseUrl }) {
      if (url.startsWith(baseUrl)) return url;
      return baseUrl + '/dashboard';
    },
  },
});

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 {
    id: string;
    role: string;
  }
}

Email/magic link provider

import Resend from 'next-auth/providers/resend';

export const { handlers, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    Resend({
      from: 'noreply@yourdomain.com',
    }),
  ],
});

Database adapters

// Prisma
import { PrismaAdapter } from '@auth/prisma-adapter';
adapter: PrismaAdapter(prisma)

// Drizzle
import { DrizzleAdapter } from '@auth/drizzle-adapter';
adapter: DrizzleAdapter(db)

// Supabase
import { SupabaseAdapter } from '@auth/supabase-adapter';
adapter: SupabaseAdapter({ url, secret })

// MongoDB
import { MongoDBAdapter } from '@auth/mongodb-adapter';
adapter: MongoDBAdapter(clientPromise)

Server Actions

import { signIn, signOut } from '@/auth';

// Sign in form
export default function LoginPage() {
  return (
    <form action={async () => {
      'use server';
      await signIn('google', { redirectTo: '/dashboard' });
    }}>
      <button type="submit">Sign in with Google</button>
    </form>
  );
}

Best Practices

  • Use JWT strategy for serverless (no database session lookups)
  • Use database strategy when you need to revoke sessions instantly
  • Extend the session via callbacks to include user ID and roles
  • Use middleware for route protection — cleaner than checking in every page
  • Always set AUTH_SECRET — required for token encryption
  • Use the Credentials provider only with proper password hashing (bcrypt/argon2)
  • Type-augment the Session and JWT interfaces for type safety

Anti-Patterns

  • Storing sensitive data in the JWT — it's readable (base64), not encrypted
  • Using Credentials provider without rate limiting — vulnerable to brute force
  • Not setting custom pages — the default pages are functional but unstyled
  • Calling useSession() without <SessionProvider> — returns undefined
  • Not handling the loading state — status starts as "loading"
  • Mixing database and JWT strategies without understanding the trade-offs

Install this skill directly: skilldb add auth-services-skills

Get CLI access →