正在加载,请稍候…

JWT Authentication Security: Common Vulnerabilities and Secure Implementation

Implement secure JWT authentication in Node.js. Learn algorithm confusion attacks, token storage, refresh token patterns, blacklisting, and JWT security best practices.

JWT Authentication Security

Secure JWT Implementation

import jwt from 'jsonwebtoken'
import crypto from 'crypto'

const JWT_SECRET = process.env.JWT_SECRET // At least 256 bits
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET
const ACCESS_TOKEN_TTL = '15m'
const REFRESH_TOKEN_TTL = '7d'

function generateTokens(payload) {
  const accessToken = jwt.sign(
    { ...payload, type: 'access' },
    JWT_SECRET,
    {
      algorithm: 'HS256',
      expiresIn: ACCESS_TOKEN_TTL,
      issuer: 'myapp.com',
      audience: 'myapp-users',
    }
  )

  const refreshToken = jwt.sign(
    { userId: payload.userId, type: 'refresh', jti: crypto.randomUUID() },
    JWT_REFRESH_SECRET,
    { expiresIn: REFRESH_TOKEN_TTL }
  )

  return { accessToken, refreshToken }
}

function verifyAccessToken(token) {
  return jwt.verify(token, JWT_SECRET, {
    algorithms: ['HS256'],  // ALWAYS specify algorithms - prevents alg:none attack
    issuer: 'myapp.com',
    audience: 'myapp-users',
  })
}

Common JWT Vulnerabilities

// VULNERABILITY 1: Algorithm confusion (alg:none)
// BAD: Not specifying algorithm
jwt.verify(token, secret) // Vulnerable to alg:none

// GOOD: Always specify algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] })

// VULNERABILITY 2: Weak secret
// BAD:
const secret = 'secret123' // Too weak

// GOOD: Cryptographically random
const secret = crypto.randomBytes(32).toString('hex') // 256 bits

// VULNERABILITY 3: Storing JWT in localStorage
// BAD: Vulnerable to XSS
localStorage.setItem('token', jwt)

// GOOD: httpOnly cookie - XSS resistant
res.cookie('access_token', token, {
  httpOnly: true,   // Not accessible via JS
  secure: true,     // HTTPS only
  sameSite: 'strict',
  maxAge: 15 * 60 * 1000,
})

Refresh Token Rotation with Redis

import { createClient } from 'redis'

const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()

async function saveRefreshToken(jti, userId, expiresIn) {
  await redis.setEx(
    `refresh_token:${jti}`,
    expiresIn,
    JSON.stringify({ userId, used: false })
  )
}

async function useRefreshToken(refreshToken) {
  const payload = jwt.verify(refreshToken, JWT_REFRESH_SECRET, {
    algorithms: ['HS256']
  })

  const stored = await redis.get(`refresh_token:${payload.jti}`)
  if (!stored) throw new Error('Invalid refresh token')

  const data = JSON.parse(stored)

  // Detect reuse attack
  if (data.used) {
    // Token reuse - revoke all user tokens
    await revokeAllUserTokens(data.userId)
    throw new Error('Refresh token reuse detected - all sessions revoked')
  }

  // Mark as used
  await redis.set(`refresh_token:${payload.jti}`,
    JSON.stringify({ ...data, used: true }),
    { keepTTL: true }
  )

  // Issue new tokens
  const { accessToken, refreshToken: newRefreshToken } = generateTokens({
    userId: data.userId
  })

  // Save new refresh token
  const newPayload = jwt.decode(newRefreshToken)
  await saveRefreshToken(newPayload.jti, data.userId, 7 * 24 * 3600)

  return { accessToken, refreshToken: newRefreshToken }
}

JWT Blacklisting for Logout

async function logout(accessToken, refreshToken) {
  // Blacklist access token until expiry
  const decoded = jwt.decode(accessToken)
  const ttl = decoded.exp - Math.floor(Date.now() / 1000)

  if (ttl > 0) {
    await redis.setEx(`blacklist:${decoded.jti}`, ttl, '1')
  }

  // Invalidate refresh token
  const refreshDecoded = jwt.decode(refreshToken)
  await redis.del(`refresh_token:${refreshDecoded.jti}`)
}

// Middleware to check blacklist
async function authMiddleware(req, res, next) {
  const token = req.cookies.access_token || req.headers.authorization?.split(' ')[1]
  if (!token) return res.status(401).json({ error: 'No token' })

  try {
    const payload = verifyAccessToken(token)

    // Check blacklist
    const blacklisted = await redis.get(`blacklist:${payload.jti}`)
    if (blacklisted) return res.status(401).json({ error: 'Token revoked' })

    req.user = payload
    next()
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' })
  }
}

Asymmetric JWT with RS256

import { readFileSync } from 'fs'

// Generate keys: openssl genrsa -out private.pem 2048
//                openssl rsa -in private.pem -pubout -out public.pem
const privateKey = readFileSync('private.pem')
const publicKey = readFileSync('public.pem')

function signToken(payload) {
  return jwt.sign(payload, privateKey, {
    algorithm: 'RS256',
    expiresIn: '15m',
    keyid: 'key-v1', // For key rotation
  })
}

function verifyToken(token) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256'],  // Only RS256 - never HS256
  })
}
// RS256 benefit: Only auth service has private key, 
// any service can verify with public key

Security Summary

Vulnerability Prevention
Algorithm confusion Specify algorithms: ['HS256']
Token theft httpOnly cookies, short expiry
Weak secret 256-bit random secret
Missing validation Verify issuer, audience
No revocation Redis blacklist + rotation