コンテンツにスキップ

Authentication Implementation Guide

Complete guide to implementing authentication in Next.js applications using NextAuth.js (Auth.js v5).


Overview

This tutorial covers: - NextAuth.js v5 setup with App Router - OAuth providers (Google, GitHub) - Credentials provider (custom login) - Session management - Protected routes - Integration with FastAPI backend


Why NextAuth.js?

Feature Benefit
App Router Support Full RSC and Server Actions integration
Multiple Providers OAuth, Email, Credentials
Session Management Automatic JWT or database sessions
Type-Safe Full TypeScript support
Edge Compatible Works with Edge Runtime

Installation

npm install next-auth@beta
npm install @auth/prisma-adapter  # If using database

Basic Setup

1. Environment Variables

.env.local:

# NextAuth
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="generate-with-openssl-rand-base64-32"

# OAuth Providers
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"

GITHUB_ID="your-github-client-id"
GITHUB_SECRET="your-github-client-secret"

Generate secret:

openssl rand -base64 32


Configuration

2. Auth Route Handler

app/api/auth/[...nextauth]/route.ts:

import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import GitHubProvider from 'next-auth/providers/github';
import CredentialsProvider from 'next-auth/providers/credentials';

const handler = NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        // Call your backend API
        const res = await fetch(`${process.env.API_URL}/auth/login`, {
          method: 'POST',
          body: JSON.stringify({
            email: credentials?.email,
            password: credentials?.password,
          }),
          headers: { 'Content-Type': 'application/json' },
        });

        const user = await res.json();

        if (res.ok && user) {
          return user;
        }

        return null;
      },
    }),
  ],

  callbacks: {
    async jwt({ token, user, account }) {
      // Persist the OAuth access_token and user ID to the token
      if (account) {
        token.accessToken = account.access_token;
      }
      if (user) {
        token.id = user.id;
      }
      return token;
    },

    async session({ session, token }) {
      // Send properties to the client
      session.user.id = token.id as string;
      session.accessToken = token.accessToken as string;
      return session;
    },
  },

  pages: {
    signIn: '/auth/signin',
    signOut: '/auth/signout',
    error: '/auth/error',
  },

  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
});

export { handler as GET, handler as POST };

3. Type Definitions

types/next-auth.d.ts:

import 'next-auth';

declare module 'next-auth' {
  interface Session {
    user: {
      id: string;
      name?: string | null;
      email?: string | null;
      image?: string | null;
    };
    accessToken?: string;
  }

  interface User {
    id: string;
  }
}

declare module 'next-auth/jwt' {
  interface JWT {
    id: string;
    accessToken?: string;
  }
}


Usage in Server Components

Get Session

import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';

export default async function ProtectedPage() {
  const session = await getServerSession();

  if (!session) {
    redirect('/auth/signin');
  }

  return (
    <div>
      <h1>Welcome, {session.user?.name}!</h1>
      <p>Email: {session.user?.email}</p>
    </div>
  );
}

Protected Route Pattern

app/dashboard/page.tsx:

import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';

async function getData(userId: string) {
  const res = await fetch(`https://api.example.com/user/${userId}`);
  return res.json();
}

export default async function DashboardPage() {
  const session = await getServerSession();

  if (!session?.user) {
    redirect('/auth/signin');
  }

  const data = await getData(session.user.id);

  return (
    <div>
      <h1>Dashboard</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}


Usage in Client Components

Session Provider Setup

app/providers.tsx:

'use client';

import { SessionProvider } from 'next-auth/react';

export function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>;
}

app/layout.tsx:

import { Providers } from './providers';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Using useSession Hook

components/UserMenu.tsx:

'use client';

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

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

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

  if (!session) {
    return (
      <button onClick={() => signIn()}>
        Sign In
      </button>
    );
  }

  return (
    <div>
      <span>Hello, {session.user?.name}</span>
      <button onClick={() => signOut()}>
        Sign Out
      </button>
    </div>
  );
}


Custom Sign-In Page

app/auth/signin/page.tsx:

'use client';

import { signIn } from 'next-auth/react';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function SignInPage() {
  const router = useRouter();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');

    const result = await signIn('credentials', {
      email,
      password,
      redirect: false,
    });

    if (result?.error) {
      setError('Invalid credentials');
    } else {
      router.push('/dashboard');
    }
  };

  return (
    <div className="max-w-md mx-auto mt-16 p-8">
      <h1 className="text-3xl font-bold mb-8">Sign In</h1>

      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label className="block mb-2">Email</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className="w-full border px-4 py-2 rounded"
            required
          />
        </div>

        <div>
          <label className="block mb-2">Password</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            className="w-full border px-4 py-2 rounded"
            required
          />
        </div>

        {error && (
          <div className="bg-red-100 text-red-800 p-3 rounded">
            {error}
          </div>
        )}

        <button
          type="submit"
          className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600"
        >
          Sign In
        </button>
      </form>

      <div className="mt-8">
        <p className="text-center text-gray-600 mb-4">Or continue with</p>

        <div className="space-y-2">
          <button
            onClick={() => signIn('google', { callbackUrl: '/dashboard' })}
            className="w-full border border-gray-300 py-2 rounded hover:bg-gray-50"
          >
            Sign in with Google
          </button>

          <button
            onClick={() => signIn('github', { callbackUrl: '/dashboard' })}
            className="w-full border border-gray-300 py-2 rounded hover:bg-gray-50"
          >
            Sign in with GitHub
          </button>
        </div>
      </div>
    </div>
  );
}


Server Actions with Authentication

lib/actions.ts:

'use server';

import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const session = await getServerSession();

  if (!session?.user) {
    redirect('/auth/signin');
  }

  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // Call your API with the user's session
  const res = await fetch('https://api.example.com/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${session.accessToken}`,
    },
    body: JSON.stringify({
      title,
      content,
      userId: session.user.id,
    }),
  });

  if (!res.ok) {
    throw new Error('Failed to create post');
  }

  revalidatePath('/posts');
  return { success: true };
}

export async function deletePost(postId: string) {
  const session = await getServerSession();

  if (!session?.user) {
    redirect('/auth/signin');
  }

  // Verify ownership or admin role
  const post = await fetch(`https://api.example.com/posts/${postId}`).then(r => r.json());

  if (post.userId !== session.user.id) {
    throw new Error('Unauthorized');
  }

  await fetch(`https://api.example.com/posts/${postId}`, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${session.accessToken}`,
    },
  });

  revalidatePath('/posts');
}


Middleware for Route Protection

middleware.ts:

export { default } from 'next-auth/middleware';

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

Or with custom logic:

import { withAuth } from 'next-auth/middleware';
import { NextResponse } from 'next/server';

export default withAuth(
  function middleware(req) {
    const token = req.nextauth.token;
    const isAuth = !!token;
    const isAuthPage = req.nextUrl.pathname.startsWith('/auth');

    if (isAuthPage) {
      if (isAuth) {
        return NextResponse.redirect(new URL('/dashboard', req.url));
      }
      return null;
    }

    if (!isAuth) {
      let from = req.nextUrl.pathname;
      if (req.nextUrl.search) {
        from += req.nextUrl.search;
      }

      return NextResponse.redirect(
        new URL(`/auth/signin?from=${encodeURIComponent(from)}`, req.url)
      );
    }
  },
  {
    callbacks: {
      authorized: ({ token }) => !!token,
    },
  }
);

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


Integration with FastAPI Backend

NextAuth Configuration for FastAPI

app/api/auth/[...nextauth]/route.ts:

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';

const handler = NextAuth({
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        try {
          // Call FastAPI backend
          const res = await fetch('https://corporate-api.px-wing.com/api/auth/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              email: credentials?.email,
              password: credentials?.password,
            }),
          });

          if (!res.ok) {
            return null;
          }

          const data = await res.json();

          // Return user object with JWT token from FastAPI
          return {
            id: data.user.id,
            email: data.user.email,
            name: data.user.name,
            accessToken: data.access_token,
          };
        } catch (error) {
          console.error('Auth error:', error);
          return null;
        }
      },
    }),
  ],

  callbacks: {
    async jwt({ token, user }) {
      // Store FastAPI JWT in NextAuth session
      if (user) {
        token.id = user.id;
        token.accessToken = user.accessToken;
      }
      return token;
    },

    async session({ session, token }) {
      session.user.id = token.id as string;
      session.accessToken = token.accessToken as string;
      return session;
    },
  },

  session: {
    strategy: 'jwt',
  },
});

export { handler as GET, handler as POST };

FastAPI JWT Verification

Python FastAPI side:

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from pydantic import BaseModel

app = FastAPI()
security = HTTPBearer()

SECRET_KEY = "your-nextauth-secret"
ALGORITHM = "HS256"

class User(BaseModel):
    id: int
    email: str
    name: str

def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> User:
    token = credentials.credentials
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user_id: str = payload.get("id")
        if user_id is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid authentication credentials",
            )
        return User(id=user_id, email=payload.get("email"), name=payload.get("name"))
    except JWTError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials",
        )

@app.get("/api/protected")
async def protected_route(user: User = Depends(verify_token)):
    return {"message": f"Hello {user.name}"}


Database Integration with Prisma

Setup Prisma Adapter

npm install @auth/prisma-adapter
npm install @prisma/client

prisma/schema.prisma:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Account {
  id                 String  @id @default(cuid())
  userId             String
  type               String
  provider           String
  providerAccountId  String
  refresh_token      String?
  access_token       String?
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String?
  session_state      String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

Update NextAuth config:

import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';

const handler = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    // ... your providers
  ],
  session: {
    strategy: 'database', // Use database sessions
  },
});


Role-Based Access Control

types/next-auth.d.ts:

declare module 'next-auth' {
  interface Session {
    user: {
      id: string;
      role: 'admin' | 'user';
      email?: string | null;
      name?: string | null;
    };
  }

  interface User {
    role: 'admin' | 'user';
  }
}

Protect admin routes:

import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';

export default async function AdminPage() {
  const session = await getServerSession();

  if (!session?.user || session.user.role !== 'admin') {
    redirect('/unauthorized');
  }

  return <div>Admin Content</div>;
}


Testing Authentication

import { render, screen } from '@testing-library/react';
import { SessionProvider } from 'next-auth/react';

const mockSession = {
  user: {
    id: '1',
    name: 'Test User',
    email: 'test@example.com',
  },
  expires: '2024-12-31',
};

test('shows user menu when authenticated', () => {
  render(
    <SessionProvider session={mockSession}>
      <UserMenu />
    </SessionProvider>
  );

  expect(screen.getByText('Test User')).toBeInTheDocument();
});

Security Best Practices

1. Secure Environment Variables

# Use strong secrets
NEXTAUTH_SECRET=$(openssl rand -base64 32)

# Never commit .env files
echo ".env*.local" >> .gitignore

2. HTTPS in Production

// next-auth.config.ts
export const config = {
  useSecureCookies: process.env.NODE_ENV === 'production',
};

3. CSRF Protection

NextAuth.js automatically handles CSRF protection.

4. Rate Limiting

// middleware.ts
import { Ratelimit } from '@upstash/ratelimit';

const ratelimit = new Ratelimit({
  redis: redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'),
});

export async function middleware(request: Request) {
  const ip = request.headers.get('x-forwarded-for') ?? 'anonymous';
  const { success } = await ratelimit.limit(ip);

  if (!success) {
    return new Response('Too many requests', { status: 429 });
  }
}

Common Patterns

Redirect After Login

const handleSignIn = async () => {
  await signIn('google', {
    callbackUrl: '/dashboard',
  });
};

Conditional Rendering

'use client';

import { useSession } from 'next-auth/react';

export function ConditionalContent() {
  const { data: session } = useSession();

  if (!session) {
    return <PublicContent />;
  }

  return <PrivateContent user={session.user} />;
}

Resources