React & Next.js Topic

Next.js Authentication & Authorization

1. NextAuth.js Setup & Configuration

Basic NextAuth.js Setup

jsx

// pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import GoogleProvider from 'next-auth/providers/google';
import GitHubProvider from 'next-auth/providers/github';

export default NextAuth({
  // Configure one or more authentication providers
  providers: [
    // Email/Password authentication
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' }
      },
      async authorize(credentials) {
        try {
          // Validate credentials against your database
          const user = await validateUser(
            credentials.email, 
            credentials.password
          );
          
          if (user) {
            return {
              id: user.id,
              email: user.email,
              name: user.name,
              role: user.role,
              image: user.avatar
            };
          }
          return null;
        } catch (error) {
          throw new Error('Invalid credentials');
        }
      }
    }),

    // Google OAuth
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.name,
          email: profile.email,
          image: profile.picture,
          role: 'user' // Default role
        };
      }
    }),

    // GitHub OAuth
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET
    })
  ],

  // Custom pages
  pages: {
    signIn: '/auth/signin',
    signUp: '/auth/signup',
    error: '/auth/error'
  },

  // Callbacks for controlling JWT and session
  callbacks: {
    async jwt({ token, user, account }) {
      // Persist user role and id to token
      if (user) {
        token.role = user.role;
        token.id = user.id;
      }
      
      // Add access token for OAuth providers
      if (account) {
        token.accessToken = account.access_token;
      }
      
      return token;
    },

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

    async redirect({ url, baseUrl }) {
      // Redirect to intended page after login
      if (url.startsWith('/')) return `${baseUrl}${url}`;
      else if (new URL(url).origin === baseUrl) return url;
      return baseUrl;
    }
  },

  // Session configuration
  session: {
    strategy: 'jwt', // Use JWT for session
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },

  // JWT configuration
  jwt: {
    secret: process.env.NEXTAUTH_SECRET,
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },

  // Debug in development
  debug: process.env.NODE_ENV === 'development',
});

Environment Variables

env

# .env.local
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key-here

# OAuth Providers
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

# Database
DATABASE_URL=your-database-connection-string

2. Authentication Components & Pages

Sign In Page

jsx

// pages/auth/signin.js
import { useState } from 'react';
import { signIn, getSession, getCsrfToken } from 'next-auth/react';
import { useRouter } from 'next/router';

export default function SignIn({ csrfToken }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const router = useRouter();
  const { callbackUrl } = router.query;

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      const result = await signIn('credentials', {
        email,
        password,
        redirect: false,
        callbackUrl: callbackUrl || '/dashboard'
      });

      if (result?.error) {
        setError(result.error);
      } else {
        router.push(result?.url || '/dashboard');
      }
    } catch (error) {
      setError('An error occurred during sign in');
    } finally {
      setLoading(false);
    }
  };

  const handleOAuthSignIn = (provider) => {
    signIn(provider, {
      callbackUrl: callbackUrl || '/dashboard'
    });
  };

  return (
    <div className="auth-container">
      <div className="auth-card">
        <h1>Sign In</h1>
        
        {error && (
          <div className="error-message">
            {error}
          </div>
        )}

        <form onSubmit={handleSubmit}>
          <input name="csrfToken" type="hidden" defaultValue={csrfToken} />
          
          <div className="form-group">
            <label htmlFor="email">Email</label>
            <input
              id="email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
            />
          </div>

          <div className="form-group">
            <label htmlFor="password">Password</label>
            <input
              id="password"
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
            />
          </div>

          <button 
            type="submit" 
            disabled={loading}
            className="submit-button"
          >
            {loading ? 'Signing In...' : 'Sign In'}
          </button>
        </form>

        <div className="oauth-providers">
          <button 
            onClick={() => handleOAuthSignIn('google')}
            className="oauth-button google"
          >
            Sign in with Google
          </button>
          
          <button 
            onClick={() => handleOAuthSignIn('github')}
            className="oauth-button github"
          >
            Sign in with GitHub
          </button>
        </div>

        <div className="auth-links">
          <a href="/auth/signup">Don't have an account? Sign up</a>
          <a href="/auth/forgot-password">Forgot your password?</a>
        </div>
      </div>
    </div>
  );
}

export async function getServerSideProps(context) {
  const session = await getSession(context);

  // Redirect if already authenticated
  if (session) {
    return {
      redirect: {
        destination: '/dashboard',
        permanent: false,
      },
    };
  }

  return {
    props: {
      csrfToken: await getCsrfToken(context),
    },
  };
}

Sign Up Page

jsx

// pages/auth/signup.js
import { useState } from 'react';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/router';

export default function SignUp() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const router = useRouter();

  const handleChange = (e) => {
    setFormData(prev => ({
      ...prev,
      [e.target.name]: e.target.value
    }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    if (formData.password !== formData.confirmPassword) {
      setError('Passwords do not match');
      setLoading(false);
      return;
    }

    try {
      const response = await fetch('/api/auth/signup', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          name: formData.name,
          email: formData.email,
          password: formData.password,
        }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.error || 'Something went wrong');
      }

      // Auto sign in after successful registration
      await signIn('credentials', {
        email: formData.email,
        password: formData.password,
        redirect: false,
      });

      router.push('/dashboard');
    } catch (error) {
      setError(error.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="auth-container">
      <div className="auth-card">
        <h1>Create Account</h1>
        
        {error && <div className="error-message">{error}</div>}

        <form onSubmit={handleSubmit}>
          <div className="form-group">
            <label htmlFor="name">Full Name</label>
            <input
              id="name"
              name="name"
              type="text"
              value={formData.name}
              onChange={handleChange}
              required
            />
          </div>

          <div className="form-group">
            <label htmlFor="email">Email</label>
            <input
              id="email"
              name="email"
              type="email"
              value={formData.email}
              onChange={handleChange}
              required
            />
          </div>

          <div className="form-group">
            <label htmlFor="password">Password</label>
            <input
              id="password"
              name="password"
              type="password"
              value={formData.password}
              onChange={handleChange}
              required
              minLength="6"
            />
          </div>

          <div className="form-group">
            <label htmlFor="confirmPassword">Confirm Password</label>
            <input
              id="confirmPassword"
              name="confirmPassword"
              type="password"
              value={formData.confirmPassword}
              onChange={handleChange}
              required
            />
          </div>

          <button type="submit" disabled={loading}>
            {loading ? 'Creating Account...' : 'Sign Up'}
          </button>
        </form>

        <div className="auth-links">
          <a href="/auth/signin">Already have an account? Sign in</a>
        </div>
      </div>
    </div>
  );
}

API Route for User Registration

jsx

// pages/api/auth/signup.js
import { hash } from 'bcryptjs';

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { name, email, password } = req.body;

  // Validation
  if (!name || !email || !password) {
    return res.status(400).json({ error: 'Missing required fields' });
  }

  if (password.length < 6) {
    return res.status(400).json({ error: 'Password must be at least 6 characters' });
  }

  try {
    // Check if user already exists
    const existingUser = await prisma.user.findUnique({
      where: { email }
    });

    if (existingUser) {
      return res.status(400).json({ error: 'User already exists' });
    }

    // Hash password
    const hashedPassword = await hash(password, 12);

    // Create user
    const user = await prisma.user.create({
      data: {
        name,
        email,
        password: hashedPassword,
        role: 'user' // Default role
      }
    });

    // Return user without password
    const { password: _, ...userWithoutPassword } = user;
    
    res.status(201).json({ 
      user: userWithoutPassword,
      message: 'User created successfully' 
    });
  } catch (error) {
    console.error('Signup error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
}

3. Middleware for Protected Routes

Advanced Middleware Configuration

jsx

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

export default withAuth(
  function middleware(req) {
    const token = req.nextauth.token;
    const { pathname } = req.nextUrl;

    // Role-based access control
    if (pathname.startsWith('/admin') && token?.role !== 'admin') {
      return NextResponse.redirect(new URL('/unauthorized', req.url));
    }

    // Redirect to intended page after login
    if (pathname === '/auth/signin' && token) {
      return NextResponse.redirect(new URL('/dashboard', req.url));
    }

    // API route protection
    if (pathname.startsWith('/api/admin') && token?.role !== 'admin') {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    return NextResponse.next();
  },
  {
    callbacks: {
      authorized: ({ token, req }) => {
        // Protect all routes except public ones
        const publicPaths = [
          '/',
          '/auth/signin',
          '/auth/signup',
          '/auth/forgot-password',
          '/api/public'
        ];

        const isPublicPath = publicPaths.some(path => 
          req.nextUrl.pathname.startsWith(path)
        );

        return !!token || isPublicPath;
      },
    },
  }
);

// Configure which routes to protect
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * - public folder
     */
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

Route-specific Middleware

jsx

// middleware/auth-middleware.js
export function requireAuth(middleware) {
  return async (req, event) => {
    const session = await getServerSession(req, res, authOptions);
    
    if (!session) {
      return {
        redirect: {
          destination: '/auth/signin',
          permanent: false,
        },
      };
    }

    return middleware ? middleware(req, event, session) : { props: { session } };
  };
}

// Usage in page
export const getServerSideProps = requireAuth(async (context, session) => {
  // Your protected page logic here
  return {
    props: {
      session,
      protectedData: await fetchProtectedData(session.user.id)
    }
  };
});

4. JWT & Session Handling

Custom Session Management

jsx

// lib/auth.js
import { jwtVerify, SignJWT } from 'jose';

const secret = new TextEncoder().encode(process.env.NEXTAUTH_SECRET);

export async function createJWT(payload) {
  return await new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('30d')
    .sign(secret);
}

export async function verifyJWT(token) {
  try {
    const { payload } = await jwtVerify(token, secret);
    return payload;
  } catch (error) {
    return null;
  }
}

// Custom session hook
export function useSession() {
  const { data: session, status } = useSession();
  const isLoading = status === 'loading';
  const isAuthenticated = !!session;

  return {
    session,
    isLoading,
    isAuthenticated,
    user: session?.user,
    role: session?.user?.role,
    hasRole: (requiredRole) => {
      if (!session?.user?.role) return false;
      return session.user.role === requiredRole;
    },
    hasAnyRole: (requiredRoles) => {
      if (!session?.user?.role) return false;
      return requiredRoles.includes(session.user.role);
    }
  };
}

Advanced Session Component

jsx

// components/SessionProvider.js
import { SessionProvider } from 'next-auth/react';
import { useRouter } from 'next/router';

export default function AuthProvider({ children, session }) {
  const router = useRouter();

  return (
    <SessionProvider 
      session={session}
      refetchInterval={5 * 60} // Refetch session every 5 minutes
      refetchOnWindowFocus={true}
    >
      <AuthListener>
        {children}
      </AuthListener>
    </SessionProvider>
  );
}

// Component to handle auth state changes
function AuthListener({ children }) {
  const { data: session, status } = useSession();
  const router = useRouter();

  useEffect(() => {
    if (status === 'loading') return;

    const publicPaths = ['/auth/signin', '/auth/signup', '/'];
    const isPublicPath = publicPaths.includes(router.pathname);

    if (!session && !isPublicPath) {
      router.replace('/auth/signin');
    }

    if (session && isPublicPath) {
      router.replace('/dashboard');
    }
  }, [session, status, router]);

  return children;
}

Protected Route Components

jsx

// components/ProtectedRoute.js
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';

export default function ProtectedRoute({ 
  children, 
  requiredRole, 
  fallback = null 
}) {
  const { data: session, status } = useSession();
  const router = useRouter();

  useEffect(() => {
    if (status === 'loading') return;

    if (!session) {
      router.replace(`/auth/signin?callbackUrl=${router.asPath}`);
      return;
    }

    if (requiredRole && session.user.role !== requiredRole) {
      router.replace('/unauthorized');
    }
  }, [session, status, router, requiredRole]);

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

  if (!session) {
    return fallback || <div>Redirecting...</div>;
  }

  if (requiredRole && session.user.role !== requiredRole) {
    return fallback || <div>Access Denied</div>;
  }

  return children;
}

// Usage examples
function DashboardPage() {
  return (
    <ProtectedRoute>
      <Dashboard />
    </ProtectedRoute>
  );
}

function AdminPage() {
  return (
    <ProtectedRoute requiredRole="admin">
      <AdminPanel />
    </ProtectedRoute>
  );
}

Role-Based Access Components

jsx

// components/RoleBasedAccess.js
import { useSession } from '../lib/auth';

export function WithRole({ role, children, fallback = null }) {
  const { hasRole } = useSession();

  if (!hasRole(role)) {
    return fallback;
  }

  return children;
}

export function WithAnyRole({ roles, children, fallback = null }) {
  const { hasAnyRole } = useSession();

  if (!hasAnyRole(roles)) {
    return fallback;
  }

  return children;
}

// Usage
function AdminSettings() {
  return (
    <WithRole role="admin">
      <AdminSettingsPanel />
    </WithRole>
  );
}

function ModeratorContent() {
  return (
    <WithAnyRole roles={['admin', 'moderator']}>
      <ModeratorTools />
    </WithAnyRole>
  );
}

5. API Route Protection

Protected API Routes

jsx

// pages/api/protected/route.js
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../auth/[...nextauth]';

export default async function handler(req, res) {
  const session = await getServerSession(req, res, authOptions);

  if (!session) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // Role-based API access
  if (req.method === 'POST' && session.user.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden' });
  }

  try {
    switch (req.method) {
      case 'GET':
        const data = await getProtectedData(session.user.id);
        return res.status(200).json(data);
      
      case 'POST':
        if (session.user.role !== 'admin') {
          return res.status(403).json({ error: 'Forbidden' });
        }
        const result = await createProtectedData(req.body);
        return res.status(201).json(result);
      
      default:
        return res.status(405).json({ error: 'Method not allowed' });
    }
  } catch (error) {
    console.error('API error:', error);
    return res.status(500).json({ error: 'Internal server error' });
  }
}

API Route Helper

jsx

// lib/api-auth.js
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../pages/api/auth/[...nextauth]';

export async function requireAuth(req, res) {
  const session = await getServerSession(req, res, authOptions);
  
  if (!session) {
    res.status(401).json({ error: 'Unauthorized' });
    return null;
  }
  
  return session;
}

export async function requireRole(req, res, requiredRole) {
  const session = await requireAuth(req, res);
  
  if (!session) return null;
  
  if (session.user.role !== requiredRole) {
    res.status(403).json({ error: 'Forbidden' });
    return null;
  }
  
  return session;
}

// Usage in API routes
export default async function adminHandler(req, res) {
  const session = await requireRole(req, res, 'admin');
  if (!session) return;

  // Admin-only logic here
  res.json({ message: 'Admin access granted' });
}

This comprehensive authentication and authorization setup provides:

  • Multiple login methods (credentials, OAuth)

  • Role-based access control

  • Protected routes and API endpoints

  • Session management with JWT

  • Middleware for route protection

  • Secure password handling

The system is scalable and can be extended with additional providers, roles, and security features as needed.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?