JWT Authentication Next.js Tutorial — Step by Step Roman Urdu Guide
JWT authentication kaise implement karein Next.js mein. Bcrypt password hashing, login/signup endpoints, protected routes, httpOnly cookies — complete production-ready guide.
JWT Authentication Next.js Tutorial — Roman Urdu Guide
Authentication = har production app ki backbone. Lekin 80% Pakistani devs is mein mistakes karte hain — passwords plain text mein store, tokens insecure, no refresh logic.
Aaj complete production-ready JWT auth banayenge Next.js 16 mein. Yahi exact pattern Vercel, Notion, Linear jaisi companies use karti hain.
ℹ️ 🎯 Is Tutorial Mein Kya Banayenge
- ✅ User registration with bcrypt password hashing
- ✅ Login endpoint with JWT token generation
- ✅ Protected API routes with middleware
- ✅ httpOnly cookies for secure storage
- ✅ Refresh token rotation
- ✅ Logout functionality
- ✅ MongoDB integration (using earlier guide)
- ✅ TypeScript for type safety
ℹ️ 📚 Prerequisites
- ✅ Next.js 16 basics (read guide)
- ✅ MongoDB Atlas setup (read guide)
- ✅ TypeScript basics (read guide)
1. JWT Kya Hai?
JWT (JSON Web Token) = encrypted string jo server user ke pass dekar bolta hai "ye banda authenticated hai".
JWT Structure:
header.payload.signature
Example:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMifQ.aBcDeFgHi...
↑ ↑ ↑
header payload signature
(algorithm) (user data) (verify integrity)
Why JWT Over Sessions?
| Feature | Sessions | JWT |
|---|---|---|
| Storage | Server (database/Redis) | Client (cookie/storage) |
| Scalability | Hard (multiple servers) | Easy (stateless) |
| Mobile apps | Complicated | Easy |
| Real-time | Server check needed | Verify locally |
| Revoke | Easy | Harder |
Modern apps: JWT prefer karte hain stateless architecture ke liye.
2. Project Setup
Step 1: Dependencies Install
npm install jsonwebtoken bcryptjs mongoose
npm install --save-dev @types/jsonwebtoken @types/bcryptjsPackages explained:
- jsonwebtoken — JWT sign + verify
- bcryptjs — password hashing (industry standard)
- mongoose — MongoDB ORM
Step 2: Environment Variables
.env.local mein add karo:
MONGODB_URI=mongodb+srv://...
JWT_SECRET=your-super-secret-key-min-32-chars-long
JWT_REFRESH_SECRET=another-super-secret-key-for-refresh🚫 ⚠️ Security Critical
JWT_SECRET kabhi public mein nahi share karna. Use strong random string (32+ characters). Production mein environment variables se hi load karo, hardcoded nahi.
Generate secret command:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"3. User Model (MongoDB)
models/User.ts:
import mongoose, { Schema, Document } from 'mongoose';
import bcrypt from 'bcryptjs';
export interface IUser extends Document {
email: string;
password: string;
name: string;
role: 'user' | 'admin';
refreshToken?: string;
comparePassword(candidatePassword: string): Promise<boolean>;
}
const UserSchema = new Schema<IUser>({
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email']
},
password: {
type: String,
required: true,
minlength: 8,
select: false // Default queries mein exclude
},
name: {
type: String,
required: true,
trim: true
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
refreshToken: {
type: String,
select: false
}
}, {
timestamps: true
});
// Hash password before saving
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Compare password method
UserSchema.methods.comparePassword = async function(candidatePassword: string) {
return bcrypt.compare(candidatePassword, this.password);
};
export default mongoose.models.User || mongoose.model<IUser>('User', UserSchema);Key Security Features:
- Password hashing — bcrypt with salt rounds 12
select: false— password queries mein default nahi aata- Email validation — regex match
- Min length — 8 characters minimum
- Pre-save hook — automatic hashing
4. JWT Helper Functions
lib/jwt.ts:
import jwt, { JwtPayload } from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;
const ACCESS_TOKEN_EXPIRY = '15m'; // Short-lived
const REFRESH_TOKEN_EXPIRY = '7d'; // Long-lived
export interface TokenPayload {
userId: string;
email: string;
role: string;
}
// Sign access token
export function signAccessToken(payload: TokenPayload): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY });
}
// Sign refresh token
export function signRefreshToken(payload: TokenPayload): string {
return jwt.sign(payload, JWT_REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY });
}
// Verify access token
export function verifyAccessToken(token: string): TokenPayload | null {
try {
return jwt.verify(token, JWT_SECRET) as TokenPayload;
} catch (error) {
return null;
}
}
// Verify refresh token
export function verifyRefreshToken(token: string): TokenPayload | null {
try {
return jwt.verify(token, JWT_REFRESH_SECRET) as TokenPayload;
} catch (error) {
return null;
}
}5. Registration Endpoint
app/api/auth/register/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { connectDB } from '@/lib/db';
import User from '@/models/User';
import { signAccessToken, signRefreshToken } from '@/lib/jwt';
import { cookies } from 'next/headers';
export async function POST(req: NextRequest) {
try {
await connectDB();
const { email, password, name } = await req.json();
// Validation
if (!email || !password || !name) {
return NextResponse.json(
{ error: 'Sare fields required hain' },
{ status: 400 }
);
}
// Check existing user
const existingUser = await User.findOne({ email });
if (existingUser) {
return NextResponse.json(
{ error: 'Email already registered' },
{ status: 409 }
);
}
// Create user (password auto-hashed via pre-save hook)
const user = await User.create({ email, password, name });
// Generate tokens
const tokenPayload = {
userId: user._id.toString(),
email: user.email,
role: user.role
};
const accessToken = signAccessToken(tokenPayload);
const refreshToken = signRefreshToken(tokenPayload);
// Save refresh token in DB
user.refreshToken = refreshToken;
await user.save();
// Set httpOnly cookies
const cookieStore = await cookies();
cookieStore.set('accessToken', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 15 * 60 // 15 minutes
});
cookieStore.set('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 // 7 days
});
return NextResponse.json({
success: true,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role
}
}, { status: 201 });
} catch (error: any) {
console.error('Registration error:', error);
return NextResponse.json(
{ error: 'Registration failed' },
{ status: 500 }
);
}
}Test with curl:
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "babar@example.com",
"password": "Strong@Pass123",
"name": "Babar Burhan"
}'6. Login Endpoint
app/api/auth/login/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { connectDB } from '@/lib/db';
import User from '@/models/User';
import { signAccessToken, signRefreshToken } from '@/lib/jwt';
import { cookies } from 'next/headers';
export async function POST(req: NextRequest) {
try {
await connectDB();
const { email, password } = await req.json();
if (!email || !password) {
return NextResponse.json(
{ error: 'Email aur password required hain' },
{ status: 400 }
);
}
// Find user with password (override default exclusion)
const user = await User.findOne({ email }).select('+password');
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Compare password
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Generate tokens
const tokenPayload = {
userId: user._id.toString(),
email: user.email,
role: user.role
};
const accessToken = signAccessToken(tokenPayload);
const refreshToken = signRefreshToken(tokenPayload);
// Update refresh token in DB
user.refreshToken = refreshToken;
await user.save();
// Set cookies
const cookieStore = await cookies();
cookieStore.set('accessToken', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 15 * 60
});
cookieStore.set('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60
});
return NextResponse.json({
success: true,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role
}
});
} catch (error: any) {
console.error('Login error:', error);
return NextResponse.json(
{ error: 'Login failed' },
{ status: 500 }
);
}
}⚠️ ⚠️ Security: Generic Error Messages
"Invalid credentials" use karo — never "Email exists" or "Wrong password". Specifying which is wrong = enumeration attack possibility.
7. Protected API Route Middleware
middleware.ts (root of project):
import { NextRequest, NextResponse } from 'next/server';
import { verifyAccessToken } from '@/lib/jwt';
export async function middleware(req: NextRequest) {
const protectedPaths = ['/api/dashboard', '/api/user', '/api/admin'];
const path = req.nextUrl.pathname;
if (!protectedPaths.some(p => path.startsWith(p))) {
return NextResponse.next();
}
const accessToken = req.cookies.get('accessToken')?.value;
if (!accessToken) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
);
}
const payload = verifyAccessToken(accessToken);
if (!payload) {
return NextResponse.json(
{ error: 'Invalid or expired token' },
{ status: 401 }
);
}
// Admin-only routes
if (path.startsWith('/api/admin') && payload.role !== 'admin') {
return NextResponse.json(
{ error: 'Admin access required' },
{ status: 403 }
);
}
// Pass user info to route handler
const requestHeaders = new Headers(req.headers);
requestHeaders.set('x-user-id', payload.userId);
requestHeaders.set('x-user-email', payload.email);
requestHeaders.set('x-user-role', payload.role);
return NextResponse.next({ request: { headers: requestHeaders } });
}
export const config = {
matcher: ['/api/dashboard/:path*', '/api/user/:path*', '/api/admin/:path*']
};Use in Protected Route:
app/api/dashboard/route.ts:
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
// Middleware ne already verify kiya aur headers add kiye
const userId = req.headers.get('x-user-id');
const email = req.headers.get('x-user-email');
return NextResponse.json({
message: 'Welcome to dashboard',
userId,
email
});
}8. Refresh Token Endpoint
app/api/auth/refresh/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { connectDB } from '@/lib/db';
import User from '@/models/User';
import { verifyRefreshToken, signAccessToken, signRefreshToken } from '@/lib/jwt';
import { cookies } from 'next/headers';
export async function POST(req: NextRequest) {
try {
const cookieStore = await cookies();
const refreshToken = cookieStore.get('refreshToken')?.value;
if (!refreshToken) {
return NextResponse.json({ error: 'No refresh token' }, { status: 401 });
}
// Verify refresh token
const payload = verifyRefreshToken(refreshToken);
if (!payload) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
}
await connectDB();
// Verify token matches DB (rotation security)
const user = await User.findById(payload.userId).select('+refreshToken');
if (!user || user.refreshToken !== refreshToken) {
return NextResponse.json({ error: 'Token mismatch' }, { status: 401 });
}
// Generate new tokens
const newTokenPayload = {
userId: user._id.toString(),
email: user.email,
role: user.role
};
const newAccessToken = signAccessToken(newTokenPayload);
const newRefreshToken = signRefreshToken(newTokenPayload);
// Rotate refresh token in DB
user.refreshToken = newRefreshToken;
await user.save();
// Update cookies
cookieStore.set('accessToken', newAccessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 15 * 60
});
cookieStore.set('refreshToken', newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60
});
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: 'Refresh failed' }, { status: 500 });
}
}9. Logout Endpoint
app/api/auth/logout/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import { connectDB } from '@/lib/db';
import User from '@/models/User';
import { verifyAccessToken } from '@/lib/jwt';
import { cookies } from 'next/headers';
export async function POST(req: NextRequest) {
try {
const cookieStore = await cookies();
const accessToken = cookieStore.get('accessToken')?.value;
if (accessToken) {
const payload = verifyAccessToken(accessToken);
if (payload) {
await connectDB();
// Clear refresh token from DB
await User.findByIdAndUpdate(payload.userId, {
$unset: { refreshToken: '' }
});
}
}
// Clear cookies
cookieStore.delete('accessToken');
cookieStore.delete('refreshToken');
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: 'Logout failed' }, { status: 500 });
}
}10. Frontend Auth Components
Login Form:
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (!res.ok) {
setError(data.error || 'Login failed');
return;
}
router.push('/dashboard');
} catch (err) {
setError('Network error');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}11. Common Security Mistakes
🚫 ❌ Critical Mistakes to Avoid
- Plain text passwords — ALWAYS bcrypt hash
- JWT_SECRET hardcoded — env vars only
- localStorage for tokens — XSS vulnerable, use httpOnly cookies
- No token expiry — short-lived access tokens (15 min)
- No refresh rotation — security risk if token stolen
- HTTPS missing in production
- Detailed error messages ("user not found" vs "invalid credentials")
- Weak password requirements — min 8 chars + complexity
- No rate limiting on login (brute force attack)
- Saving sensitive data in JWT — payload is base64, NOT encrypted
12. Production Checklist
✅ ✅ Pre-Launch Checklist
- HTTPS configured (Vercel auto)
- Strong JWT_SECRET (32+ chars)
- Different secrets for access + refresh
- httpOnly cookies in production
- sameSite: 'strict' set
- Rate limiting on login/register
- Email verification flow
- Password reset flow
- Logout from all devices feature
- Failed login attempts tracked
- 2FA support (optional but recommended)
13. Rate Limiting (Important!)
Install Vercel rate limit:
npm install @upstash/ratelimit @upstash/redislib/rate-limit.ts:
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
export const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '1 m'), // 5 requests per minute
});Use in login:
const { success } = await ratelimit.limit(req.headers.get('x-forwarded-for') ?? '');
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}14. Testing Your Auth
Manual Testing Flow:
# 1. Register
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"Test@1234","name":"Test"}'
# 2. Login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"Test@1234"}' \
-c cookies.txt
# 3. Access protected
curl http://localhost:3000/api/dashboard -b cookies.txt
# 4. Refresh token
curl -X POST http://localhost:3000/api/auth/refresh -b cookies.txt
# 5. Logout
curl -X POST http://localhost:3000/api/auth/logout -b cookies.txt15. Real-World Improvements
Add Email Verification:
// On registration, send verification email
const verificationToken = jwt.sign(
{ userId: user._id, type: 'email-verify' },
JWT_SECRET,
{ expiresIn: '24h' }
);
// Send email with link
await sendEmail(
user.email,
'Verify your email',
`Click: ${process.env.NEXT_PUBLIC_SITE_URL}/verify?token=${verificationToken}`
);Add Password Reset:
Similar pattern — generate short-lived token, send email link, verify on reset.
Add Social Login:
Use NextAuth.js for OAuth (Google, GitHub, etc.):
npm install next-auth🎯 What You've Built
After this tutorial, you have:
✅ Production-ready JWT authentication
✅ Bcrypt password hashing (industry standard)
✅ Token refresh rotation
✅ Protected API routes via middleware
✅ httpOnly cookies (XSS-safe storage)
✅ Logout with token invalidation
✅ MongoDB integration
✅ TypeScript type safety
Time invested: 2-4 hours Value created: Production-ready auth (would cost $500-2000 from freelancer)
🎓 Related Posts
- Next.js 16 Complete Beginner Guide
- TypeScript Basics for JS Devs
- MongoDB Atlas Free Setup
- React Hooks Complete Guide
- Cursor AI Complete Guide
Authentication mein koi bug aaya? Comment mein code paste karo — debug kar dunga.
Pakistani devs ke saath share karo 🇵🇰
Happy Auth-ing! 🔐
Written by UrduCoder
Pakistan ka #1 Roman Urdu coding blog. Hum AI, Web Dev, aur tech tutorials publish karte hain har hafte.
Related Posts
Next.js 16 Complete Beginner Guide — Roman Urdu Mein Seekho
Next.js 16 ka complete beginner guide Roman Urdu mein. Turbopack, App Router, Server Components, React 19, async params — sab kuch step-by-step examples ke saath.
TypeScript Basics — JavaScript Developers Ke Liye Roman Urdu Guide
TypeScript ka complete beginner guide JavaScript developers ke liye. Types, interfaces, generics, React mein use — sab kuch examples ke saath Roman Urdu mein.
Next.js 14 Complete Course - Roman Urdu Mein Seekho
Next.js 14 ka A-to-Z course Roman Urdu mein. App Router, Server Components, aur modern web development ki har cheez.