JWT & Session Management

Secure user sessions with JSON Web Tokens

🎫 What is JWT?

JSON Web Tokens (JWT) are secure tokens for authentication. They store user information in an encrypted format, allowing stateless authentication without server-side session storage for scalable applications.


// JWT structure: header.payload.signature
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
                                    

Key JWT Concepts

🔐

Stateless Auth

No server-side session storage needed

Scalable Fast Efficient
📦

Self-Contained

Token contains all user information

User Data Permissions Expiry
🛡️

Secure

Cryptographically signed and verified

Encrypted Tamper-Proof Verified

Expiration

Automatic token expiry for security

Time-Limited Refresh Safe

🔹 Creating JWT Tokens

Generate JWT tokens using the jose library in Next.js. Tokens contain user data and are signed with a secret key to ensure authenticity and prevent tampering.

# Install jose library
npm install jose

🔸 Sign JWT Token

// lib/jwt.js
import { SignJWT } from 'jose'

export async function createToken(payload) {
  const secret = new TextEncoder().encode(
    process.env.JWT_SECRET
  )

  const token = await new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('24h')
    .sign(secret)

  return token
}

Result:

✅ Secure JWT token created

✅ Expires in 24 hours

✅ Contains user payload

🔹 Verifying JWT Tokens

Verify and decode JWT tokens to authenticate requests. This ensures the token is valid, not expired, and hasn't been tampered with before granting access to protected resources.

// lib/jwt.js
import { jwtVerify } from 'jose'

export async function verifyToken(token) {
  try {
    const secret = new TextEncoder().encode(
      process.env.JWT_SECRET
    )

    const { payload } = await jwtVerify(token, secret)
    return payload
  } catch (error) {
    return null
  }
}

Verification checks:

  • Token signature is valid
  • Token hasn't expired
  • Token hasn't been tampered with
  • Secret key matches

🔹 Login API Route

Create an API route that authenticates users and returns a JWT token. This endpoint validates credentials and generates a token that clients can use for subsequent authenticated requests.

// app/api/login/route.js
import { createToken } from '@/lib/jwt'
import { NextResponse } from 'next/server'

export async function POST(request) {
  const { email, password } = await request.json()

  // Validate credentials (check database)
  const user = await validateUser(email, password)

  if (!user) {
    return NextResponse.json(
      { error: 'Invalid credentials' },
      { status: 401 }
    )
  }

  // Create JWT token
  const token = await createToken({
    userId: user.id,
    email: user.email,
  })

  return NextResponse.json({ token })
}

Result:

✅ User authenticated

✅ JWT token generated

✅ Token returned to client

🔹 Middleware Authentication

Use middleware to protect routes by verifying JWT tokens before allowing access. This centralizes authentication logic and automatically protects multiple routes without repeating code.

🔸 Create Middleware

// middleware.js
import { NextResponse } from 'next/server'
import { verifyToken } from '@/lib/jwt'

export async function middleware(request) {
  const token = request.cookies.get('token')?.value

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  const payload = await verifyToken(token)

  if (!payload) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*'],
}

🔸 Protected Routes

  • /dashboard/* - Requires authentication
  • /profile/* - Requires authentication
  • Other routes - Public access

🔹 Storing Tokens

Store JWT tokens securely in HTTP-only cookies to prevent XSS attacks. Cookies are automatically sent with requests and can't be accessed by JavaScript, providing better security than localStorage.

🔸 Set Cookie

// app/api/login/route.js
import { cookies } from 'next/headers'

export async function POST(request) {
  // ... authenticate user ...

  const token = await createToken(payload)

  cookies().set('token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 60 * 60 * 24, // 24 hours
  })

  return NextResponse.json({ success: true })
}

🔸 Clear Cookie (Logout)

// app/api/logout/route.js
import { cookies } from 'next/headers'

export async function POST() {
  cookies().delete('token')
  return NextResponse.json({ success: true })
}

🔹 Refresh Tokens

Implement refresh tokens for long-lived sessions. Access tokens expire quickly for security, while refresh tokens allow users to get new access tokens without re-authenticating frequently.

// app/api/refresh/route.js
import { verifyToken, createToken } from '@/lib/jwt'
import { cookies } from 'next/headers'

export async function POST() {
  const refreshToken = cookies().get('refreshToken')?.value

  if (!refreshToken) {
    return NextResponse.json(
      { error: 'No refresh token' },
      { status: 401 }
    )
  }

  const payload = await verifyToken(refreshToken)

  if (!payload) {
    return NextResponse.json(
      { error: 'Invalid refresh token' },
      { status: 401 }
    )
  }

  // Create new access token
  const newToken = await createToken({
    userId: payload.userId,
    email: payload.email,
  })

  cookies().set('token', newToken, {
    httpOnly: true,
    secure: true,
    maxAge: 60 * 15, // 15 minutes
  })

  return NextResponse.json({ success: true })
}

🔹 Client-Side Usage

Access protected API routes from the client using the stored JWT token. The token is automatically included in requests via cookies, making authenticated API calls simple and secure.

'use client'
import { useState } from 'react'

export default function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleLogin = async (e) => {
    e.preventDefault()

    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    })

    if (response.ok) {
      window.location.href = '/dashboard'
    }
  }

  return (
    <form onSubmit={handleLogin}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  )
}

🧠 Test Your Knowledge

Where should JWT tokens be stored for best security?