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!