In a world where personal data and secure access to resources are invaluable, authentication and authorization are fundamental aspects of any modern web application. Next.js, as a framework oriented toward production use, offers extensive tools and integrations that enable the implementation of secure authentication systems.
Authentication is the process of verifying a user's identity - it answers the question "Who are you?". Authorization, on the other hand, determines what resources an authenticated user can access - it answers the question "What can you do?".
In Next.js applications, we can implement authentication in different ways:
NextAuth.js (now also known as Auth.js) is a popular, comprehensive library for authentication in Next.js applications. Its biggest advantages are:
Here is a simple Auth.js configuration example in an App Router application:
1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from "next-auth";
3import GitHubProvider from "next-auth/providers/github";
4import CredentialsProvider from "next-auth/providers/credentials";
5
6const handler = NextAuth({
7 providers: [
8 GitHubProvider({
9 clientId: process.env.GITHUB_ID as string,
10 clientSecret: process.env.GITHUB_SECRET as string,
11 }),
12 CredentialsProvider({
13 name: 'Credentials',
14 credentials: {
15 username: { label: "Email", type: "email" },
16 password: { label: "Password", type: "password" }
17 },
18 async authorize(credentials) {
19 // Here you should implement your own credentials verification logic
20 // e.g. checking in the database
21 if (credentials?.username === "admin@example.com" && credentials?.password === "password") {
22 return { id: "1", name: "Admin", email: "admin@example.com" };
23 }
24 return null;
25 }
26 }),
27 ],
28 pages: {
29 signIn: '/auth/signin',
30 signOut: '/auth/signout',
31 error: '/auth/error',
32 },
33 callbacks: {
34 async session({ session, token }) {
35 // Here we can add additional data to the session
36 return session;
37 },
38 async jwt({ token, user }) {
39 // Here we can add additional data to the JWT token
40 if (user) {
41 token.role = user.role;
42 }
43 return token;
44 }
45 }
46});
47
48export { handler as GET, handler as POST };Auth.js supports three main authentication strategies:
In client components we can use hooks provided by Auth.js:
1// app/components/login-button.tsx
2'use client';
3
4import { signIn, signOut, useSession } from "next-auth/react";
5
6export function LoginButton() {
7 const { data: session, status } = useSession();
8
9 if (status === "loading") {
10 return <div>Loading...</div>;
11 }
12
13 if (session) {
14 return (
15 <div>
16 <p>Logged in as: {session.user?.name}</p>
17 <button onClick={() => signOut()}>Log out</button>
18 </div>
19 );
20 }
21
22 return <button onClick={() => signIn()}>Log in</button>;
23}To use useSession() in client components, we need to add SessionProvider to the main application layout:
1// app/providers.tsx
2'use client';
3
4import { SessionProvider } from "next-auth/react";
5
6export function Providers({ children }: { children: React.ReactNode }) {
7 return <SessionProvider>{children}</SessionProvider>;
8}
9
10// app/layout.tsx
11import { Providers } from "./providers";
12
13export default function RootLayout({ children }: { children: React.ReactNode }) {
14 return (
15 <html lang="pl">
16 <body>
17 <Providers>{children}</Providers>
18 </body>
19 </html>
20 );
21}Next.js allows using middleware to secure routes against unauthorized access. Here is an example middleware that checks the user's session and redirects to the login page if no session exists:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import { getToken } from 'next-auth/jwt';
4import type { NextRequest } from 'next/server';
5
6export async function middleware(request: NextRequest) {
7 const token = await getToken({ req: request });
8
9 // Paths that should be protected
10 const protectedPaths = ['/dashboard', '/profile', '/settings'];
11
12 // Check if the current path is protected
13 const isProtectedPath = protectedPaths.some(path =>
14 request.nextUrl.pathname.startsWith(path)
15 );
16
17 if (isProtectedPath && !token) {
18 // If the user tries to access a protected path without being logged in,
19 // we redirect them to the login page
20 const loginUrl = new URL('/auth/signin', request.url);
21 loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname);
22 return NextResponse.redirect(loginUrl);
23 }
24
25 return NextResponse.next();
26}
27
28export const config = {
29 matcher: ['/dashboard/:path*', '/profile/:path*', '/settings/:path*'],
30};In more complex applications, we often need to implement a role and permission system. We can extend the default Auth.js mechanisms to support roles:
1// We extend the types for Auth.js
2declare module "next-auth" {
3 interface User {
4 role?: string;
5 }
6
7 interface Session {
8 user?: {
9 id?: string;
10 name?: string;
11 email?: string;
12 role?: string;
13 };
14 }
15}
16
17// In the Auth.js callbacks
18callbacks: {
19 async jwt({ token, user }) {
20 if (user) {
21 token.role = user.role;
22 }
23 return token;
24 },
25 async session({ session, token }) {
26 if (session.user) {
27 session.user.role = token.role as string;
28 }
29 return session;
30 }
31}Then we can create conditional components based on roles:
1// app/components/role-guard.tsx
2'use client';
3
4import { useSession } from "next-auth/react";
5import { redirect } from "next/navigation";
6
7interface RolesGuardProps {
8 children: React.ReactNode;
9 allowedRoles: string[];
10}
11
12export function RolesGuard({ children, allowedRoles }: RolesGuardProps) {
13 const { data: session, status } = useSession();
14
15 if (status === "loading") {
16 return <div>Checking permissions...</div>;
17 }
18
19 if (!session || !session.user?.role || !allowedRoles.includes(session.user.role)) {
20 return <div>Access denied. Required permissions: {allowedRoles.join(', ')}</div>;
21 }
22
23 return <>{children}</>;
24}When implementing authentication, keep in mind several key security aspects:
1import { NextRequest, NextResponse } from 'next/server';
2import { getToken } from 'next-auth/jwt';
3
4// Simple in-memory implementation (in production you would use Redis or another store)
5const loginAttempts = new Map<string, { count: number, lastAttempt: number }>();
6
7export async function middleware(request: NextRequest) {
8 if (request.nextUrl.pathname === '/api/auth/callback/credentials') {
9 const ip = request.ip || 'unknown';
10 const currentAttempts = loginAttempts.get(ip) || { count: 0, lastAttempt: Date.now() };
11
12 // Reset the counter after 30 minutes
13 if (Date.now() - currentAttempts.lastAttempt > 30 * 60 * 1000) {
14 currentAttempts.count = 0;
15 }
16
17 // If the attempt limit has been exceeded
18 if (currentAttempts.count >= 5) {
19 return new NextResponse(
20 JSON.stringify({ error: 'Too many login attempts. Try again in 30 minutes.' }),
21 { status: 429 }
22 );
23 }
24
25 // Update the counter
26 loginAttempts.set(ip, {
27 count: currentAttempts.count + 1,
28 lastAttempt: Date.now()
29 });
30 }
31
32 return NextResponse.next();
33}Authentication and authorization are key elements of every web application. Next.js together with Auth.js offers powerful tools for implementing secure authentication systems, from simple solutions to complex systems with roles and permissions.
When choosing an authentication solution for your Next.js application, it's worth considering:
A proper approach to authentication and authorization not only protects your application but also improves the user experience, building trust in your product.