diff --git a/auth.config.ts b/auth.config.ts index a8e2b4c..f570814 100644 --- a/auth.config.ts +++ b/auth.config.ts @@ -1,6 +1,22 @@ import type { NextAuthConfig } from "next-auth" export const authConfig = { + session: { + strategy: "jwt", + maxAge: 30 * 24 * 60 * 60, // 30 days in seconds + updateAge: 24 * 60 * 60, // Update session every 24 hours + }, + cookies: { + sessionToken: { + name: `${process.env.NODE_ENV === 'production' ? '__Secure-' : ''}next-auth.session-token`, + options: { + httpOnly: true, // Prevent JavaScript access to cookies (XSS protection) + sameSite: 'lax', // CSRF protection - prevents cookie from being sent in cross-site requests + path: '/', + secure: process.env.NODE_ENV === 'production', // Only send cookie over HTTPS in production + }, + }, + }, pages: { signIn: "/auth/login", signOut: "/auth/logout", @@ -9,20 +25,48 @@ export const authConfig = { callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user - const isOnAuthPage = nextUrl.pathname.startsWith("/auth") - // Allow access to auth pages when not logged in - if (isOnAuthPage) { + // Define protected routes + const isProtectedRoute = + nextUrl.pathname === "/" || + nextUrl.pathname.startsWith("/list") || + nextUrl.pathname.startsWith("/profile") || + nextUrl.pathname.startsWith("/invitations") + + // Define auth pages (login, register, etc.) + const isAuthPage = nextUrl.pathname.startsWith("/auth") + + // Allow access to auth pages regardless of authentication status + if (isAuthPage) { + // If already logged in and trying to access login/register, redirect to home + if (isLoggedIn && (nextUrl.pathname === "/auth/login" || nextUrl.pathname === "/auth/register")) { + return Response.redirect(new URL("/", nextUrl)) + } return true } - // Redirect to login if not authenticated - if (!isLoggedIn) { - return false + // Redirect to login if accessing protected route without authentication + if (isProtectedRoute && !isLoggedIn) { + return false // This will redirect to signIn page } + // Allow access to public routes return true }, + async jwt({ token, user }) { + // Add user ID to token on sign in + if (user) { + token.id = user.id + } + return token + }, + async session({ session, token }) { + // Add user ID to session from token + if (session.user && token.id) { + session.user.id = token.id as string + } + return session + }, }, providers: [], // Providers will be added in auth.ts } satisfies NextAuthConfig diff --git a/auth.ts b/auth.ts index 161a484..c6daa4c 100644 --- a/auth.ts +++ b/auth.ts @@ -1,5 +1,6 @@ import NextAuth from "next-auth" import Credentials from "next-auth/providers/credentials" +import { authConfig } from "@/auth.config" import { prisma } from "@/lib/db" import bcrypt from "bcryptjs" import { z } from "zod" @@ -11,22 +12,7 @@ const loginSchema = z.object({ }) export const { handlers, signIn, signOut, auth } = NextAuth({ - session: { - strategy: "jwt", - maxAge: 30 * 24 * 60 * 60, // 30 days in seconds - updateAge: 24 * 60 * 60, // Update session every 24 hours - }, - cookies: { - sessionToken: { - name: `${process.env.NODE_ENV === 'production' ? '__Secure-' : ''}next-auth.session-token`, - options: { - httpOnly: true, // Prevent JavaScript access to cookies (XSS protection) - sameSite: 'lax', // CSRF protection - prevents cookie from being sent in cross-site requests - path: '/', - secure: process.env.NODE_ENV === 'production', // Only send cookie over HTTPS in production - }, - }, - }, + ...authConfig, providers: [ Credentials({ name: "credentials", @@ -69,34 +55,4 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ }, }), ], - callbacks: { - async jwt({ token, user, trigger }) { - // Add user ID to token on sign in - if (user) { - token.id = user.id - } - - // Handle session expiration - // Token will be automatically invalidated by NextAuth when it expires - // based on the maxAge setting in session config - - return token - }, - async session({ session, token }) { - // Add user ID to session from token - if (session.user && token.id) { - session.user.id = token.id as string - } - - // Session validation happens automatically via JWT expiration - // If token is expired, this callback won't be called and user will be redirected - - return session - }, - }, - pages: { - signIn: "/auth/login", - signOut: "/auth/logout", - error: "/auth/error", - }, }) diff --git a/middleware.ts b/middleware.ts index 1b9d4b7..116c0fc 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,6 +1,5 @@ -import { auth } from "@/auth" -import { NextResponse } from "next/server" -import type { NextRequest } from "next/server" +import NextAuth from "next-auth" +import { authConfig } from "@/auth.config" /** * Middleware for session validation, route protection, and security headers @@ -14,59 +13,27 @@ import type { NextRequest } from "next/server" * * Requirements: 3.3, 3.5, 9.4 (CSRF protection), 9.5 (security headers) */ +const { auth } = NextAuth(authConfig) + export default auth((req) => { - const { nextUrl } = req - const isLoggedIn = !!req.auth?.user + // The authorization logic is handled in auth.config.ts authorized callback + // Here we just add security headers - // Define protected routes - const isProtectedRoute = - nextUrl.pathname === "/" || - nextUrl.pathname.startsWith("/list") || - nextUrl.pathname.startsWith("/profile") || - nextUrl.pathname.startsWith("/invitations") - - // Define auth pages (login, register, etc.) - const isAuthPage = nextUrl.pathname.startsWith("/auth") - - // Create response - let response: NextResponse - - // Allow access to auth pages regardless of authentication status - if (isAuthPage) { - // If already logged in and trying to access login/register, redirect to home - if (isLoggedIn && (nextUrl.pathname === "/auth/login" || nextUrl.pathname === "/auth/register")) { - response = NextResponse.redirect(new URL("/", req.url)) - } else { - response = NextResponse.next() - } - } - // Redirect to login if accessing protected route without authentication - else if (isProtectedRoute && !isLoggedIn) { - const loginUrl = new URL("/auth/login", req.url) - // Add callback URL to redirect back after login - loginUrl.searchParams.set("callbackUrl", nextUrl.pathname) - response = NextResponse.redirect(loginUrl) - } - // Allow access to public routes - else { - response = NextResponse.next() - } + const requestHeaders = new Headers(req.headers) // Add security headers - Requirement 9.4, 9.5 - const headers = response.headers - // CSRF Protection - SameSite cookie attribute is set in auth.ts // This header provides additional CSRF protection - headers.set('X-Content-Type-Options', 'nosniff') + requestHeaders.set('X-Content-Type-Options', 'nosniff') // XSS Protection - headers.set('X-XSS-Protection', '1; mode=block') + requestHeaders.set('X-XSS-Protection', '1; mode=block') // Prevent clickjacking attacks - headers.set('X-Frame-Options', 'DENY') + requestHeaders.set('X-Frame-Options', 'DENY') // Referrer Policy - don't leak referrer information - headers.set('Referrer-Policy', 'strict-origin-when-cross-origin') + requestHeaders.set('Referrer-Policy', 'strict-origin-when-cross-origin') // Content Security Policy - helps prevent XSS attacks // Note: This is a basic CSP. You may need to adjust based on your needs @@ -79,12 +46,12 @@ export default auth((req) => { "connect-src 'self'", "frame-ancestors 'none'", ].join('; ') - headers.set('Content-Security-Policy', csp) + requestHeaders.set('Content-Security-Policy', csp) // Permissions Policy - restrict access to browser features - headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()') + requestHeaders.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()') - return response + return undefined // Let NextAuth handle the response }) /**