Back to Blog|⚛️ Web Development

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.

U
UrduCoder
••13 min read
JWT Authentication Next.js Tutorial — Step by Step Roman Urdu 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

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?

FeatureSessionsJWT
StorageServer (database/Redis)Client (cookie/storage)
ScalabilityHard (multiple servers)Easy (stateless)
Mobile appsComplicatedEasy
Real-timeServer check neededVerify locally
RevokeEasyHarder

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/bcryptjs

Packages 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:

  1. Password hashing — bcrypt with salt rounds 12
  2. select: false — password queries mein default nahi aata
  3. Email validation — regex match
  4. Min length — 8 characters minimum
  5. 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

  1. Plain text passwords — ALWAYS bcrypt hash
  2. JWT_SECRET hardcoded — env vars only
  3. localStorage for tokens — XSS vulnerable, use httpOnly cookies
  4. No token expiry — short-lived access tokens (15 min)
  5. No refresh rotation — security risk if token stolen
  6. HTTPS missing in production
  7. Detailed error messages ("user not found" vs "invalid credentials")
  8. Weak password requirements — min 8 chars + complexity
  9. No rate limiting on login (brute force attack)
  10. 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/redis

lib/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.txt

15. 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)


Authentication mein koi bug aaya? Comment mein code paste karo — debug kar dunga.

Pakistani devs ke saath share karo 🇵🇰

Happy Auth-ing! 🔐

Tags:#jwt#authentication#nextjs#security#bcrypt#tutorial
Share:
U

Written by UrduCoder

Pakistan ka #1 Roman Urdu coding blog. Hum AI, Web Dev, aur tech tutorials publish karte hain har hafte.