Skip to main content
Esprit CLI detects authentication vulnerabilities across JWT/OIDC flows, session management, and token verification. Authentication failures often enable token forgery, token confusion, and durable account takeover.
Do not trust headers, claims, or token opacity without strict validation bound to issuer, audience, key, and context.

Attack Surface Coverage

Authentication Methods

  • JWT - JWS (signed) and JWE (encrypted) tokens
  • OIDC/OAuth2 - Access tokens, ID tokens, refresh tokens
  • Session - Cookie-based sessions, session fixation
  • API Keys - Bearer tokens, API key management

Flow Types Analyzed

  • Authorization code flow
  • PKCE (Proof Key for Code Exchange)
  • Device/Backchannel flows
  • Refresh token rotation
  • Impersonation/delegation

JWT Vulnerabilities

Esprit detects critical JWT security issues:

Signature Verification Bypass

// Vulnerable: Algorithm confusion
const jwt = require('jsonwebtoken');

app.use((req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  // VULNERABLE: Accepts any algorithm
  const decoded = jwt.verify(token, publicKey);
  req.user = decoded;
  next();
});
Attack: RS256 → HS256 Confusion
// Attacker changes alg to HS256 and signs with public key as HMAC secret
const forged = jwt.sign(
  { sub: 'admin', role: 'admin' },
  publicKey,  // Using RSA public key as HMAC secret
  { algorithm: 'HS256' }
);
Esprit detection: Tests algorithm downgrade attacks and validates algorithm pinning

”none” Algorithm Acceptance

// Attacker JWT header
{
  "alg": "none",
  "typ": "JWT"
}

// Payload (no signature)
{
  "sub": "admin",
  "role": "admin",
  "exp": 9999999999
}
// Vulnerable verification
if (jwt.decode(token)) {  // decode() doesn't verify!
  req.user = jwt.decode(token);
}
Never use jwt.decode() for authentication. Always use jwt.verify() with algorithm pinning.

Header Manipulation

Esprit tests header injection vulnerabilities:

kid (Key ID) Injection

// Path traversal in kid
{
  "alg": "HS256",
  "kid": "../../../../etc/passwd"
}

// SQL injection in kid lookup
{
  "alg": "HS256",
  "kid": "1' OR '1'='1"
}

jku (JWK Set URL) Abuse

// Attacker-controlled JWKS endpoint
{
  "alg": "RS256",
  "jku": "https://attacker.com/jwks.json",
  "kid": "attacker-key"
}

jwk Inline Key Injection

// Embed attacker's public key
{
  "alg": "RS256",
  "jwk": {
    "kty": "RSA",
    "n": "attacker-public-key...",
    "e": "AQAB"
  }
}

Claims Validation Failures

// Vulnerable: Missing claims validation
function authenticate(req, res, next) {
  const token = extractToken(req);
  const decoded = jwt.verify(token, secret);
  // VULNERABLE: No iss/aud/exp checks!
  req.user = decoded;
  next();
}
Missing validations Esprit checks:
  • iss (issuer) - Accept tokens from any issuer
  • aud (audience) - Cross-service token reuse
  • exp (expiration) - Accept expired tokens
  • nbf (not before) - Accept future-dated tokens
  • azp (authorized party) - Token confusion between clients

Example Vulnerabilities

Scenario 1: Algorithm Confusion

// Vulnerable code: src/middleware/auth.js:15
const publicKey = fs.readFileSync('public.pem');

function verifyToken(token) {
  try {
    // VULNERABLE: No algorithm specified
    return jwt.verify(token, publicKey);
  } catch (err) {
    return null;
  }
}
Exploitation:
// Attacker crafts token with HS256
const forgedToken = jwt.sign(
  { sub: '[email protected]', role: 'admin' },
  publicKey,
  { algorithm: 'HS256' }
);

// Token accepted because HS256 uses publicKey as HMAC secret
Impact: Complete authentication bypass, privilege escalation

Scenario 2: Missing Audience Validation

// Vulnerable code: src/services/api-gateway.js:45
app.use((req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  const decoded = jwt.verify(token, secret, { algorithms: ['RS256'] });
  // VULNERABLE: No audience check
  req.user = decoded;
  next();
});
Exploitation:
# Use token issued for Service A against Service B
curl -H "Authorization: Bearer <service-a-token>" \
  https://service-b.com/api/admin/users
Impact: Cross-service token reuse, unauthorized access

Scenario 3: JKU/X5U URL Injection

// Vulnerable code: src/lib/jwt.js:67
async function verifyWithJku(token) {
  const header = jwt.decode(token, { complete: true }).header;
  
  if (header.jku) {
    // VULNERABLE: Fetches from attacker-controlled URL
    const jwks = await fetch(header.jku).then(r => r.json());
    const key = jwks.keys.find(k => k.kid === header.kid);
    return jwt.verify(token, key);
  }
}
Exploitation:
  1. Attacker hosts JWKS at https://attacker.com/jwks.json
  2. Creates JWT with jku pointing to attacker URL
  3. Token verified with attacker’s keys
Impact: Token forgery, authentication bypass

OIDC/OAuth2 Vulnerabilities

Esprit detects OIDC-specific issues:

Token Type Confusion

// Vulnerable: Accepts ID token instead of access token
app.get('/api/users', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  const decoded = jwt.verify(token, secret);
  // VULNERABLE: Should check token type (typ claim)
  // Attacker can use ID token meant for client, not API
  const users = getUsersByRole(decoded.role);
  res.json(users);
});

PKCE Downgrade

// Vulnerable: Optional PKCE
app.post('/oauth/token', (req, res) => {
  const { code, code_verifier } = req.body;
  
  const authCode = codes.get(code);
  
  // VULNERABLE: Missing PKCE check if no verifier provided
  if (code_verifier && authCode.code_challenge) {
    // Verify PKCE
  }
  
  // Issue token without PKCE verification
  const token = issueToken(authCode.client_id);
  res.json({ access_token: token });
});

State/Nonce Missing or Weak

// Vulnerable: Predictable state parameter
app.get('/oauth/authorize', (req, res) => {
  const state = req.session.userId;  // VULNERABLE: Predictable
  const authUrl = `${IDP_URL}?client_id=${CLIENT_ID}&state=${state}`;
  res.redirect(authUrl);
});

Refresh Token Issues

Esprit validates refresh token security:

No Rotation Enforcement

// Vulnerable: Refresh tokens can be reused
app.post('/oauth/token', (req, res) => {
  const { grant_type, refresh_token } = req.body;
  
  if (grant_type === 'refresh_token') {
    const tokenData = validateRefreshToken(refresh_token);
    // VULNERABLE: Same refresh token still valid
    const newAccess = issueAccessToken(tokenData.userId);
    res.json({ access_token: newAccess });
  }
});
Impact: Stolen refresh tokens grant indefinite access

Session Vulnerabilities

Esprit detects session management issues:

Session Fixation

// Vulnerable: Session ID not regenerated on login
app.post('/login', (req, res) => {
  const user = authenticateUser(req.body);
  if (user) {
    // VULNERABLE: Keeps existing session ID
    req.session.userId = user.id;
    res.json({ success: true });
  }
});
// Vulnerable: Missing security flags
app.use(session({
  secret: 'secret',
  cookie: {
    // VULNERABLE: Missing Secure, HttpOnly, SameSite
    maxAge: 86400000
  }
}));
Esprit validates that session cookies use Secure, HttpOnly, and SameSite=Strict flags.

Remediation

Esprit recommends:

Secure JWT Verification

// SAFE: Proper JWT verification
const publicKey = fs.readFileSync('public.pem');

function verifyToken(token) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256'],  // Pin algorithm
    issuer: 'https://auth.example.com',
    audience: 'api.example.com',
    clockTolerance: 30  // Allow 30s clock skew
  });
}

Validate All Claims

// SAFE: Complete claims validation
function authenticate(req, res, next) {
  try {
    const token = extractToken(req);
    const decoded = jwt.verify(token, secret, {
      algorithms: ['RS256'],
      issuer: EXPECTED_ISSUER,
      audience: EXPECTED_AUDIENCE
    });
    
    // Additional checks
    if (decoded.typ !== 'access_token') {
      throw new Error('Invalid token type');
    }
    
    req.user = decoded;
    next();
  } catch (err) {
    res.status(401).json({ error: 'Unauthorized' });
  }
}

Secure JWKS Fetching

// SAFE: Whitelist JWKS URLs
const ALLOWED_JWKS_URLS = [
  'https://auth.example.com/.well-known/jwks.json'
];

async function verifyWithJku(token) {
  const header = jwt.decode(token, { complete: true }).header;
  
  if (header.jku && ALLOWED_JWKS_URLS.includes(header.jku)) {
    const jwks = await fetch(header.jku).then(r => r.json());
    const key = jwks.keys.find(k => k.kid === header.kid);
    return jwt.verify(token, key, { algorithms: ['RS256'] });
  }
  
  throw new Error('Invalid jku');
}

Implement Refresh Token Rotation

// SAFE: Refresh token rotation
app.post('/oauth/token', async (req, res) => {
  const { grant_type, refresh_token } = req.body;
  
  if (grant_type === 'refresh_token') {
    const tokenData = await validateRefreshToken(refresh_token);
    
    // Invalidate old refresh token
    await revokeRefreshToken(refresh_token);
    
    // Issue new tokens
    const newAccess = issueAccessToken(tokenData.userId);
    const newRefresh = issueRefreshToken(tokenData.userId);
    
    res.json({ 
      access_token: newAccess,
      refresh_token: newRefresh 
    });
  }
});

Secure Session Management

// SAFE: Secure session configuration
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,        // HTTPS only
    httpOnly: true,      // No JavaScript access
    sameSite: 'strict',  // CSRF protection
    maxAge: 3600000      // 1 hour
  },
  store: new RedisStore({ client: redisClient })
}));

// Regenerate session ID on login
app.post('/login', (req, res) => {
  const user = authenticateUser(req.body);
  if (user) {
    req.session.regenerate((err) => {
      req.session.userId = user.id;
      res.json({ success: true });
    });
  }
});

Detection Output

Esprit provides detailed authentication findings:
[CRITICAL] JWT Algorithm Confusion
Location: src/middleware/auth.js:15
Vulnerability: RS256 → HS256 downgrade

Vulnerable Code:
  jwt.verify(token, publicKey)  // No algorithm pinning

Proof:
  1. Extract RS256 public key from /.well-known/jwks.json
  2. Create JWT with alg: HS256, signed with public key
  3. Token accepted by verification
  
Forged Token:
  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
  Claims: {"sub":"admin","role":"admin"}

Impact:
  - Complete authentication bypass
  - Privilege escalation to admin
  - No secret material required

Remediation:
  jwt.verify(token, publicKey, {
    algorithms: ['RS256']  // Pin expected algorithm
  });

Next Steps

Access Control

Detect authorization bypass vulnerabilities

SSRF Detection

Find server-side request forgery issues

Build docs developers (and LLMs) love