We use cookies to enhance your experience on the site
CodeWorlds

Authentication and Authorization in Next.js

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 basics

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:

  1. Custom approach - building an authentication system from scratch
  2. Libraries and SDKs - using ready-made solutions like NextAuth.js/Auth.js
  3. External services - integration with services like Firebase Auth, Auth0, Supabase Auth

NextAuth.js / Auth.js

NextAuth.js (now also known as Auth.js) is a popular, comprehensive library for authentication in Next.js applications. Its biggest advantages are:

  • Support for multiple authentication providers (Google, GitHub, Facebook, Twitter, etc.)
  • Built-in support for sessions and JWT tokens
  • Compatibility with API Routes and App Router
  • Easy configuration and minimal boilerplate code

Basic Auth.js configuration in App Router

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 };

Authentication strategies

Auth.js supports three main authentication strategies:

  1. OAuth - authentication via external providers (Google, Facebook, GitHub, etc.)
  2. Credentials - authentication via your own login/password system
  3. Email - authentication using links sent by email (passwordless)

Using client components for session management

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}

Session Provider in the main layout

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}

Middleware for protecting routes

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};

User roles and permissions

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}

Application security

When implementing authentication, keep in mind several key security aspects:

  1. Secure password storage - always use hashing functions like bcrypt, never store passwords in plain text
  2. HTTPS - all authentication requests should use HTTPS
  3. CSRF (Cross-Site Request Forgery) - Auth.js automatically protects against CSRF attacks
  4. Login attempt limits - implement limits on the number of failed login attempts
  5. Secure tokens - ensure JWT tokens are properly signed and have a reasonable validity period
  6. Secure environment variables - store secret keys and passwords in environment variables

Example implementation of login attempt limits

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}

Summary

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:

  1. Project requirements - what login methods are needed?
  2. Target users - do they prefer social or traditional login?
  3. Application scale - is a complex role and permission system needed?
  4. Security - what security measures are necessary for your application?

A proper approach to authentication and authorization not only protects your application but also improves the user experience, building trust in your product.

Go to CodeWorlds