/*
File: contexts/AuthContext.tsx
Description: React context for authentication using JWT stored in HTTP-only cookies.
*/
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
// --- Types ---
export interface User {
id: string;
email: string;
name?: string;
}
export interface AuthContextType {
user: User | null;
loading: boolean;
signIn: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
}
// --- Context ---
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// --- Provider ---
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// fetch current user on mount
useEffect(() => {
fetch('/api/auth/session')
.then(res => res.ok ? res.json() : Promise.reject())
.then((data: { user: User }) => setUser(data.user))
.catch(() => setUser(null))
.finally(() => setLoading(false));
}, []);
const signIn = async (email: string, password: string) => {
setLoading(true);
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
setLoading(false);
throw new Error('Invalid credentials');
}
const { user: logged } = await res.json();
setUser(logged);
setLoading(false);
};
const signOut = async () => {
setLoading(true);
await fetch('/api/auth/logout', { method: 'POST' });
setUser(null);
setLoading(false);
};
return (
<AuthContext.Provider value={{ user, loading, signIn, signOut }}>
{children}
</AuthContext.Provider>
);
};
// --- Hook ---
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
/*
File: middleware.ts
Description: Protect routes based on presence of JWT in cookies.
*/
import { NextResponse, NextRequest } from 'next/server';
import { jwtVerify } from 'jose';
const PUBLIC_FILE = /\.(.*)$/;
const AUTH_TOKEN_NAME = 'token';
const LOGIN_PATH = '/login';
export async function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
// skip public files and API routes and login page
if (
pathname.startsWith('/api') ||
pathname === LOGIN_PATH ||
PUBLIC_FILE.test(pathname)
) {
return NextResponse.next();
}
const token = req.cookies.get(AUTH_TOKEN_NAME)?.value;
if (!token) {
const url = req.nextUrl.clone();
url.pathname = LOGIN_PATH;
return NextResponse.redirect(url);
}
try {
// verify JWT (HS256 secret from env)
await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET));
return NextResponse.next();
} catch {
const url = req.nextUrl.clone();
url.pathname = LOGIN_PATH;
return NextResponse.redirect(url);
}
}
export const config = {
matcher: ['/', '/protected/:path*'],
};
/*
File: next-env.d.ts (for jose types)
*/
/// <reference types="next" />
/// <reference types="next/types/global" />
Created
June 29, 2025 18:48
-
-
Save yeasin2002/d40d86b12ba782c000d01d965834fa63 to your computer and use it in GitHub Desktop.
minimal-auth
A lightweight, fully type-safe JWT authentication setup for Next.js (13+), using HTTP-only cookies and the jose library. This boilerplate provides:
- React Context (
AuthContext) for client-side auth state management - API routes for login, logout, and session retrieval
- Next.js Middleware to protect routes and redirect unauthenticated users
- TypeScript interfaces for strong typing
- Minimal & Opinionated: Zero bloat, focused solely on JWT-based auth.
- Secure: Stores JWT in HTTP-only cookies; verifies tokens in middleware.
- Type-Safe: All interfaces and hooks written in TypeScript.
- Easy to Extend: Plug in your own login logic, add refresh tokens, or integrate with a database.
- Node.js ≥ 18
- Next.js ≥ 13
- TypeScript
-
Clone the repo
git clone https://github.com/your-org/nextjs-auth-minimal.git cd nextjs-auth-minimal -
Install dependencies
npm install # or yarn install -
Set up environment variables
Create a
.env.localfile at the project root:# JWT secret for signing/verifying tokens JWT_SECRET=your_super_secret_key # Cookie settings NEXTAUTH_URL=http://localhost:3000
nextjs-auth-minimal/
├── contexts/
│ └── AuthContext.tsx # React AuthProvider & useAuth hook
├── pages/
│ ├── api/
│ │ ├── auth/
│ │ │ ├── login.ts # POST: { email, password } → sets HTTP-only JWT cookie
│ │ │ ├── logout.ts # POST: clears cookie
│ │ │ └── session.ts # GET: returns { user } if JWT valid
│ └── protected-page.tsx # Example protected page
├── middleware.ts # Protects routes via jwtVerify
├── next-env.d.ts # Type references for Next.js + jose
└── README.md # This file
In app/layout.tsx or pages/_app.tsx:
import { AuthProvider } from '../contexts/AuthContext';
export default function App({ Component, pageProps }) {
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
);
}import { useAuth } from '../contexts/AuthContext';
export default function ProfilePage() {
const { user, loading, signOut } = useAuth();
if (loading) return <p>Loading...</p>;
if (!user) return <p>Not signed in</p>;
return (
<div>
<h1>Welcome, {user.name ?? user.email}!</h1>
<button onClick={signOut}>Sign Out</button>
</div>
);
}-
POST
/api/auth/login- Expects
{ email, password } - Validates credentials, returns
{ user }, sets JWT cookie
- Expects
-
POST
/api/auth/logout- Clears cookie
-
GET
/api/auth/session- Returns
{ user }if JWT valid, otherwise 401
- Returns
Customize these routes to connect to your database or external auth service.
The middleware.ts file uses Next.js Middleware to protect routes:
- Skips
/api,/login, and static files - Verifies
tokencookie viajose/ HS256 - Redirects unauthenticated requests to
/login - Apply to paths in
config.matcher(default:/and/protected/:path*)
Modify matcher as needed for your app’s routing.
- Refresh Tokens: Add a second cookie and rotate tokens in middleware.
- RBAC / Permissions: Enhance JWT payload with roles, and check in middleware.
- OAuth: Swap login route logic to integrate with OAuth providers.
MIT © Your Name
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment