Authentication
Jumbo React Next uses Auth.js v5 (next-auth@5.0.0-beta) with a Credentials provider and JWT
sessions. The auth config lives in auth.ts at the project root and exports the four primary
utilities: auth, handlers, signIn, and signOut.
Architecture overview
auth.ts ← Auth.js config + exports
src/app/api/auth/[...nextauth]/ ← Route handler (GET + POST)
src/components/providers/AuthProvider.tsx ← SessionProvider for client components
src/lib/auth/index.ts ← Server-side helpers (getSession, isAuthenticated, hasRole)
src/proxy/auth.ts ← Proxy-level route guards
next-auth.d.ts ← Session/JWT type augmentation
auth.ts — Core configuration
// auth.ts
import type { NextAuthConfig } from 'next-auth';
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
const authConfig: NextAuthConfig = {
providers: [
Credentials({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
// Replace this block with your real database + password hash check
const demoEmail = process.env.DEMO_USER_EMAIL || 'demo@example.com';
const demoPassword = process.env.DEMO_USER_PASSWORD || 'demo123';
if (credentials.email === demoEmail && credentials.password === demoPassword) {
return { id: '1', name: 'David Tim', email: demoEmail, role: 'admin', accessToken: '' };
}
return null; // null = invalid credentials
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.role = user.role;
token.accessToken = user.accessToken;
}
return token;
},
async session({ session, token }) {
if (token && session.user) {
session.user.id = token.id as string;
session.user.role = token.role as string;
session.user.accessToken = token.accessToken as string;
}
return session;
},
},
pages: {
signIn: '/auth/login-1',
error: '/auth/error',
},
session: { strategy: 'jwt' },
};
export const { auth, handlers, signIn, signOut } = NextAuth(authConfig);Session type augmentation
next-auth.d.ts extends the default Session, User, and JWT interfaces to include the
custom fields:
// next-auth.d.ts
import type { DefaultSession, DefaultUser } from 'next-auth';
declare module 'next-auth' {
interface Session {
user: {
id: string;
role: string;
accessToken: string;
} & DefaultSession['user'];
}
interface User extends DefaultUser {
role: string;
accessToken: string;
}
}
declare module 'next-auth/jwt' {
interface JWT {
id: string;
role: string;
accessToken: string;
}
}Server-side session helpers
src/lib/auth/index.ts wraps the auth() function with named, intention-revealing helpers for
use in server components and server actions:
// src/lib/auth/index.ts
import { auth } from '@/auth';
export async function getSession() {
return await auth();
}
export async function isAuthenticated(): Promise<boolean> {
const session = await auth();
return !!session?.user;
}
export async function getCurrentUser() {
const session = await auth();
if (!session?.user) throw new Error('Not authenticated');
return session.user;
}
export async function hasRole(role: string): Promise<boolean> {
const session = await auth();
return session?.user?.role === role;
}Example usage in a server component:
// src/app/[lang]/(common)/user/profile-1/page.tsx
import { getCurrentUser } from '@/lib/auth';
export default async function Profile1Page() {
const user = await getCurrentUser(); // throws if unauthenticated
return <UserProfile1 user={user} />;
}Client-side session access
Use SessionProvider (wrapped in AuthProvider) and the useSession hook for client components:
// src/components/providers/AuthProvider.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
export function AuthProvider({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}// In any client component
'use client';
import { useSession, signOut } from 'next-auth/react';
export function UserMenu() {
const { data: session, status } = useSession();
if (status === 'loading') return <Spinner />;
if (!session) return null;
return (
<div>
<span>Welcome, {session.user.name}</span>
<button onClick={() => signOut({ callbackUrl: '/auth/login-1' })}>
Sign out
</button>
</div>
);
}Replacing the demo Credentials provider
The demo auth is for development only. To connect real authentication:
Install a database client and password hashing library
bashpnpm add @prisma/client bcryptjs pnpm add -D @types/bcryptjsUpdate the authorize function in auth.ts
Replace the demo credential check with a real database query:
typescriptasync authorize(credentials) { if (!credentials?.email || !credentials?.password) return null; const user = await db.user.findUnique({ where: { email: credentials.email as string }, }); if (!user || !user.passwordHash) return null; const isValid = await bcrypt.compare( credentials.password as string, user.passwordHash ); if (!isValid) return null; return { id: user.id, name: user.name, email: user.email, role: user.role, accessToken: '' }; },Set environment variables
Update
.env.localwith your database URL and remove the demo credential variables:bashAUTH_SECRET=<your-secret> DATABASE_URL=postgresql://user:password@localhost:5432/jumbo
The default authorize function compares credentials against plaintext environment variables.
Always replace it with a real database lookup and a proper password hashing library before
deploying to production.
Adding OAuth providers
Auth.js v5 supports Google, GitHub, and many other OAuth providers. Add them alongside the existing Credentials provider:
// auth.ts
import Google from 'next-auth/providers/google';
import GitHub from 'next-auth/providers/github';
const authConfig: NextAuthConfig = {
providers: [
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }),
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
Credentials({ /* ... */ }),
],
// ...
};