June 2, 2025
Claude Security Development

How Auth Actually Works: Auditing Auth Flows with Claude Code

You've got a production app. Users are logging in, getting tokens, refreshing sessions. But here's the thing nobody talks about: can you actually trace how it all works? Not the conceptual "OAuth flows" from a diagram. I mean: where's your actual token creation code? What enforces the guards? What happens when a token expires?

Most engineers can't answer those questions about their own systems. That's a problem—especially when you need to audit for security, onboard someone new, or fix a bug that touches auth.

Claude Code changes this. With a proper codebase search, you can map your entire authentication flow in hours instead of days. You'll find the endpoints, the middleware, the token logic, the gaps.

Let's walk through how.


Table of Contents
  1. Why Auth Audits Matter (The Hidden Cost)
  2. The Real Cost of Auth Gaps
  3. The Audit Approach: Map, Then Verify
  4. Finding Auth Entry Points
  5. Tracing the Full Token Lifecycle
  6. Token Issuance
  7. Token Validation
  8. Token Refresh
  9. Mapping Authorization Guards
  10. Building a Route Audit Table
  11. Examining Role-Based Access Control (RBAC)
  12. Permission Granularity
  13. Token Scopes and Expiry Rules
  14. Finding Gaps: The Unprotected Routes
  15. Session Management and Logout
  16. Documenting Your Findings: The Auth Flow Diagram
  17. Advanced Audit Techniques: Finding the Real Vulnerabilities
  18. Searching for Common Auth Vulnerabilities
  19. Checking for Rate Limiting
  20. Detecting Broken Token Validation
  21. Building Your Audit Report
  22. Audit Report: Auth Flow Analysis
  23. Example Finding
  24. The Audit Workflow: From Chaos to Clarity
  25. Phase 1: Reconnaissance (30 min)
  26. Phase 2: Entry Point Mapping (45 min)
  27. Phase 3: Flow Tracing (60 min)
  28. Phase 4: Guard Mapping (45 min)
  29. Phase 5: Vulnerability Scanning (30 min)
  30. Phase 6: Documentation (30 min)
  31. Practical Audit Checklist
  32. Token Issuance
  33. Token Validation
  34. Authorization
  35. Session Management
  36. Security Hardening
  37. Testing
  38. The Power of Systematic Understanding
  39. Building Your Auth Audit as a Living Document
  40. What Actually Changes When You Audit
  41. Advanced Topic: Auth in Distributed Systems
  42. Incident Response: When Auth Fails in Production
  43. Scaling Auth Audits to Larger Teams
  44. Automation: Moving from Manual to Continuous Auditing
  45. Related Topics and Further Learning

Why Auth Audits Matter (The Hidden Cost)

Before we dive into the how, let's be honest about why you need this.

Auth isn't glamorous. Nobody gets promoted for "maintained perfect authentication." But missing auth checks? Expired tokens accepted anyway? Users able to access data they shouldn't? That makes headlines.

The second hidden cost: onboarding latency. New team members trying to understand your auth system spend days reading scattered code, asking questions, finding contradictions. A documented auth flow cuts that to hours. I'm not just talking about documentation that sits in a README and nobody reads. I mean a living, verified map of where authentication actually happens in your codebase.

The third—and this one's sneaky—is debt. Every refactor, every service migration, every new endpoint becomes a question: "Did we wire up auth right?" Without a living map of your auth paths, you inherit every previous engineer's assumptions. Some of them were wrong. Some of them were compromises made under deadline pressure. Some of them were security theater—checking auth in the wrong place, or not at all.

And there's a fourth cost most teams don't measure: incident response time. When someone reports "I can see another user's data," or "My session doesn't expire," or "I got logged out from an old device but the old device can still make requests"—your team needs to trace the auth system under pressure. Without a map, you're debugging blind.

Claude Code helps you answer: Where are your auth decisions made? What actually enforces them? What could we have missed?

The Real Cost of Auth Gaps

Consider this scenario: A vulnerability researcher finds that your API accepts expired tokens for 30 seconds past expiry (because of clock skew in validation). Now you need to find every token validation point and understand the skew handling. Without a map, this becomes an archaeological dig. With one, it's a structured investigation.

Or consider onboarding: A new engineer asks "can users modify each other's profiles?" You either know the answer because you have a documented guard, or you spend an hour in grep hell discovering three different validation strategies that might or might not work together.


The Audit Approach: Map, Then Verify

Authentication auditing isn't a code-generation task—it's a code-navigation task. You're reading, not writing. That's actually Claude Code's superpower.

Here's the approach:

  1. Find the entry points (login routes, token endpoints)
  2. Trace the flow (how tokens get created, stored, validated)
  3. Map the guards (where auth is enforced, and where it's not)
  4. Document the logic (token scopes, expiry, refresh rules)
  5. Generate the diagram (visual proof of what you found)

We'll work with a realistic TypeScript example: a Node.js/Express app with JWT auth, refresh tokens, and role-based access control.


Finding Auth Entry Points

Your auth audit starts at the surface: where do authentication requests come into your system?

In a typical Express app, this means login routes. Let's search:

bash
claude search "router.post.*login|router.post.*auth" --type ts

This returns routes like:

typescript
// src/routes/auth.ts
router.post("/login", async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findByEmail(email);
 
  if (!user || !(await user.comparePassword(password))) {
    return res.status(401).json({ error: "Invalid credentials" });
  }
 
  const accessToken = jwt.sign(
    { userId: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: "15m" },
  );
 
  const refreshToken = jwt.sign(
    { userId: user.id, tokenVersion: user.tokenVersion },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: "7d" },
  );
 
  return res.json({ accessToken, refreshToken });
});

What we found: Tokens are issued here. Note the expiry times: 15m for access, 7d for refresh.

Now search for where tokens are validated:

bash
claude search "jwt.verify|verifyToken|validateJWT" --type ts

This reveals middleware:

typescript
// src/middleware/auth.ts
export const authenticateToken = (req, res, next) => {
  const token = req.headers.authorization?.split(" ")[1];
 
  if (!token) {
    return res.status(401).json({ error: "No token provided" });
  }
 
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: "Invalid token" });
  }
};

What we found: Tokens are verified on every request that uses this middleware. But here's the audit question: is this middleware applied to every protected route? That's different from just existing.


Tracing the Full Token Lifecycle

Now we know tokens are issued and verified. But what about the complete lifecycle?

Token Issuance

We already found where access tokens are created (in /login). But tokens might be issued from multiple places:

bash
claude search "jwt.sign|createToken|issueToken" --type ts

This might reveal:

  • /login endpoint (initial issuance)
  • /refresh endpoint (refresh token flow)
  • /password-reset endpoint (new token after reset)
  • Service-to-service token generation
  • Social login handlers (OAuth, OIDC flows)

Each of these needs different audit notes. A password reset that issues a new token should probably revoke all previous tokens. Does yours? This is the kind of detail that separates "looks fine" from "actually secure."

Token Validation

We found one middleware. But are there others? Search broadly:

bash
claude search "req.user|currentUser|getAuthenticatedUser" --type ts

Look for patterns like:

typescript
// Middleware 1: Standard JWT verification
export const authenticateToken = (req, res, next) => { ... };
 
// Middleware 2: Stricter verification (adds IP check)
export const authenticateTokenStrict = (req, res, next) => {
  // Same JWT check, PLUS:
  if (req.ip !== req.user.lastIp) {
    return res.status(401).json({ error: 'Token used from different IP' });
  }
  next();
};
 
// Middleware 3: Optional auth (doesn't fail if no token)
export const optionalAuth = (req, res, next) => {
  try {
    const token = req.headers.authorization?.split(' ')[1];
    if (token) req.user = jwt.verify(token, process.env.JWT_SECRET);
  } catch (err) {
    // Silently fail—we'll handle missing auth later
  }
  next();
};
 
// Middleware 4: Service account verification
export const authenticateServiceToken = (req, res, next) => {
  const token = req.headers['x-service-token'];
  if (!token || !validateServiceToken(token)) {
    return res.status(401).json({ error: 'Invalid service token' });
  }
  req.serviceId = extractServiceId(token);
  next();
};

Audit finding: You've got multiple different validation strategies. That's a red flag. Why? Because one might be weaker than intended, or different routes might use the wrong one. Maybe optionalAuth was supposed to be used on analytics endpoints but got used on a financial endpoint instead.

Token Refresh

Refresh tokens are the tricky part. Search:

bash
claude search "refreshToken|refresh.*endpoint|POST.*refresh" --type ts

Find something like:

typescript
router.post("/refresh", (req, res) => {
  const { refreshToken } = req.body;
 
  try {
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    const user = await User.findById(decoded.userId);
 
    // Check token version (invalidation tracking)
    if (decoded.tokenVersion !== user.tokenVersion) {
      return res.status(401).json({ error: "Token revoked" });
    }
 
    const newAccessToken = jwt.sign(
      { userId: user.id, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: "15m" },
    );
 
    return res.json({ accessToken: newAccessToken });
  } catch (err) {
    return res.status(401).json({ error: "Invalid refresh token" });
  }
});

Audit findings:

  • Refresh tokens are verified ✓
  • Token version is checked (allows revocation without database lookup) ✓
  • New access tokens are issued with same expiry ✓

But ask deeper questions: What invalidates the refresh token itself? Is it only the 7-day expiry? Or can it be manually revoked? Does the refresh token rotate—that is, do you get a new refresh token back every time you use it? Search:

bash
claude search "tokenVersion|incrementTokenVersion|logout" --type ts

Some systems issue a new refresh token with each access token refresh. Others reuse the same one until it expires. The first is more secure but more complex.


Mapping Authorization Guards

OK, tokens exist. They're verified. But where are they actually required?

This is where audits get real. Search for route definitions:

bash
claude search "router\.(get|post|put|delete)" --type ts --context 5

For each route, ask: Is authenticateToken middleware applied?

You're looking for patterns:

typescript
// Protected route (uses middleware)
router.get("/profile", authenticateToken, (req, res) => {
  const user = req.user; // Safe—middleware enforced auth
  res.json(user);
});
 
// Unprotected route (no middleware)
router.get("/public", (req, res) => {
  res.json({ message: "public data" });
});
 
// RISKY: Claims to be protected but doesn't verify
router.get("/data", (req, res) => {
  // Trusts req.user exists, but nothing enforces it
  if (req.user) {
    res.json({ data: req.user.data });
  } else {
    res.json({ data: [] }); // Falls back to empty data instead of rejecting
  }
});
 
// CRITICAL: No auth at all on a sensitive endpoint
router.get("/user/:id/profile", (req, res) => {
  // IDOR vulnerability! User can request any ID
  const userId = req.params.id;
  const user = await User.findById(userId);
  res.json(user); // Returns private data for ANY user
});

That third pattern? It's easy to miss and hard to spot without actually reading the routes. The fourth one is a classic IDOR (Insecure Direct Object Reference) that audits catch consistently.

Building a Route Audit Table

Create a spreadsheet or document:

RouteMethodMiddlewareAuth RequiredRolesRisk
/loginPOST-No-Low
/refreshPOST-No*-Medium**
/profileGETauthenticateTokenYesAnyLow
/admin/usersGETauthenticateToken, authorizeRole('admin')YesAdminLow
/data/exportPOSTauthenticateToken, authorizeRole('admin')YesAdminHigh***

*No auth on refresh endpoint is standard (you need it to get a first access token), but the refresh token itself must be validated.

**Medium risk because refresh token is sent in body (not header); it could be logged or intercepted. IP pinning or device tracking could help.

***High risk because it's an export endpoint; should log access, have rate limiting, and possibly require email verification for sensitive exports.


Examining Role-Based Access Control (RBAC)

Most real apps don't just check "is authenticated?" They check "is authenticated and authorized?"

Search for role checks:

bash
claude search "role|permission|authorize|canAccess" --type ts

You'll likely find middleware like:

typescript
export const authorizeRole = (...allowedRoles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }
 
    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
 
    next();
  };
};
 
// Used like:
router.get('/admin/users',
  authenticateToken,
  authorizeRole('admin'),
  (req, res) => { ... }
);

Audit questions:

  • Is role checked after token is verified? (Yes—authenticateToken comes first.) ✓
  • Are roles enforced consistently, or do some routes check manually? Search:
bash
claude search "req.user.role.*==|req.user.role.*!==" --type ts

Manual role checks are a smell:

typescript
// BAD: Manual role check without middleware
router.get("/sensitive", authenticateToken, (req, res) => {
  if (req.user.role === "admin") {
    res.json({ secret: "data" });
  } else {
    res.json({ message: "no access" }); // Doesn't fail; returns empty
  }
});
 
// GOOD: Uses middleware to enforce role
router.get(
  "/sensitive",
  authenticateToken,
  authorizeRole("admin"),
  (req, res) => {
    res.json({ secret: "data" });
  },
);

Manual role checks should use the middleware instead. It's easier to audit and harder to mess up. Plus, when you see authorizeRole('admin') in the route definition, the auth requirement is visible at a glance.

Permission Granularity

Some systems go deeper than roles. Search:

bash
claude search "permission|scope|can.*action" --type ts

You might find:

typescript
export const authorizePermission = (requiredPermission) => {
  return (req, res, next) => {
    const userPermissions = req.user.permissions || [];
    if (!userPermissions.includes(requiredPermission)) {
      return res.status(403).json({ error: 'Permission denied' });
    }
    next();
  };
};
 
// Used like:
router.delete('/users/:id',
  authenticateToken,
  authorizePermission('delete:users'),
  (req, res) => { ... }
);

Audit findings:

  • Permissions are granular (not just roles) ✓
  • They're checked before the handler runs ✓
  • But: where are permissions assigned? Search:
bash
claude search "grantPermission|addPermission|permissions.*=" --type ts

This is important because role assignment is one problem, but permission assignment is another. If you find permissions being granted in multiple places (some in a database seed, some in code, some in a permission service), that's complexity that creates bugs.


Token Scopes and Expiry Rules

Different token types should have different rules. Search for all token creation:

bash
claude search "jwt.sign|sign.*token" --type ts --context 3

Document what you find:

Token TypeSecret UsedExpiryScopeRefreshNotes
Access TokenJWT_SECRET15mAPI accessVia refresh tokenShort-lived
Refresh TokenJWT_REFRESH_SECRET7dToken refresh onlyN/ALong-lived, stored in DB
Reset TokenRESET_SECRET1hPassword reset onlyN/AOne-time use
API TokenAPI_SECRET90dService callsVia rotate callLong-lived for services

Critical audit question: Are different secrets used for different token types?

Bad:

typescript
// Using same secret everywhere—catastrophic
const token = jwt.sign(data, process.env.JWT_SECRET, { expiresIn: "15m" });
const refresh = jwt.sign(data, process.env.JWT_SECRET, { expiresIn: "7d" });
const reset = jwt.sign(data, process.env.JWT_SECRET, { expiresIn: "1h" });

If an attacker gets a reset token, they could use it as an access token if the secret is the same. Now they can impersonate a user indefinitely.

Good:

typescript
const token = jwt.sign(data, process.env.JWT_SECRET, { expiresIn: "15m" });
const refresh = jwt.sign(data, process.env.JWT_REFRESH_SECRET, {
  expiresIn: "7d",
});
const reset = jwt.sign(data, process.env.RESET_SECRET, { expiresIn: "1h" });

Finding Gaps: The Unprotected Routes

This is where auditing gets detective work. Search for routes that should be protected but aren't:

bash
claude search "router\.(get|post)" --type ts | grep -v "authenticateToken\|authorizeRole"

You'll get a list of unprotected routes. Go through manually:

typescript
router.get("/public-config", (req, res) => {
  res.json({ version: "1.0" }); // ✓ Intentionally public
});
 
router.get("/user-stats", (req, res) => {
  // ✗ MISSING AUTH—shows stats for any user
  const userId = req.query.userId;
  res.json(await UserStats.findById(userId));
});
 
router.post("/subscribe", (req, res) => {
  // ✓ Intentionally public, but rate-limited
  // (Check rate limiting middleware)
});
 
router.get("/admin/reports", (req, res) => {
  // ✗ ADMIN ENDPOINT WITH NO AUTH—critical vulnerability
  const reports = await Report.findAll();
  res.json(reports);
});

The second one is an IDOR (Insecure Direct Object Reference) vulnerability. The fourth one is worse—it's public admin data.

Session Management and Logout

How do users end a session?

bash
claude search "logout|session.*destroy|invalidate.*token" --type ts

You might find:

typescript
router.post("/logout", authenticateToken, (req, res) => {
  const user = await User.findById(req.user.userId);
  user.tokenVersion += 1; // Invalidates all refresh tokens
  await user.save();
  res.json({ message: "logged out" });
});

Audit questions:

  • Is logout required to use the app? (No—tokens just expire.)
  • But if a user logs out, does their old token immediately stop working? (Yes, if you check tokenVersion.)
  • What if someone loses their device? Is there a "revoke all sessions" option?

Search:

bash
claude search "revoke.*all|session.*all|logoutAll|logout.*everywhere" --type ts

Real-world scenario: A user's phone is stolen. They need to log out from all devices immediately. If your system doesn't support this, the thief can keep using their account for weeks until the refresh token expires.


Documenting Your Findings: The Auth Flow Diagram

After tracing, create a visual diagram. Here's a text-based version:

┌─────────────────────────────────────────────────────────────┐
│                    AUTH FLOW ARCHITECTURE                   │
└─────────────────────────────────────────────────────────────┘

1. LOGIN
   POST /login { email, password }
   ↓
   [validateCredentials] → User.comparePassword()
   ↓
   [issueTokens]
   ├─ accessToken (JWT, 15m, JWT_SECRET)
   ├─ refreshToken (JWT, 7d, JWT_REFRESH_SECRET)
   └─ Return both to client

2. REQUEST WITH TOKEN
   GET /profile (headers: Authorization: Bearer <accessToken>)
   ↓
   [authenticateToken middleware]
   ├─ Extract token from header
   ├─ jwt.verify(token, JWT_SECRET)
   ├─ Check token expiry
   ├─ If invalid/expired → 401
   └─ If valid → set req.user
   ↓
   [authorizeRole middleware] (if endpoint needs it)
   ├─ Check req.user.role
   ├─ If role not in allowedRoles → 403
   └─ If authorized → next()
   ↓
   [Handler] reads req.user (already verified)

3. TOKEN REFRESH
   POST /refresh { refreshToken }
   ↓
   [validateRefreshToken]
   ├─ jwt.verify(refreshToken, JWT_REFRESH_SECRET)
   ├─ Check tokenVersion (matches user.tokenVersion)
   ├─ If revoked (version mismatch) → 401
   └─ If valid → issue new accessToken

4. LOGOUT
   POST /logout (requires auth)
   ↓
   [authenticateToken] (verify user)
   ↓
   user.tokenVersion += 1
   ↓
   All existing refresh tokens immediately invalid
   ↓
   Client must use new credentials next login

5. EDGE CASES & ATTACKS
   - Token replay: Store `jti` (JWT ID) in database, check on each use
   - Token theft: Use HttpOnly cookies, not localStorage
   - Brute force: Rate limit /login to 5 attempts per 15min
   - Session fixation: Regenerate session ID after login

This diagram is your audit map. It shows:

  • Entry points
  • Validation steps at each stage
  • Where tokens are created and checked
  • The complete token lifecycle
  • Known edge cases and protections

Advanced Audit Techniques: Finding the Real Vulnerabilities

OK, you've mapped the happy path. But audits aren't about finding what should work—they're about finding what could break.

Searching for Common Auth Vulnerabilities

Every codebase has patterns that create security holes. Claude Code helps you find them systematically.

Vulnerability 1: Token Storage in localStorage

Search:

bash
claude search "localStorage.*token|setItem.*token" --type ts

If you find this, it's a problem. localStorage is accessible to any script on the page (including XSS attacks). Tokens should be in HttpOnly cookies instead, where JavaScript can't touch them.

What you're looking for:

typescript
// BAD: Vulnerable to XSS
localStorage.setItem("accessToken", token);
 
// BETTER: HttpOnly cookie (JavaScript can't access it)
res.cookie("accessToken", token, {
  httpOnly: true,
  secure: true, // HTTPS only
  sameSite: "strict", // Prevents CSRF
  maxAge: 15 * 60 * 1000, // 15 minutes
});

The XSS attack scenario: An attacker injects a script into your site. It reads localStorage.getItem('accessToken'), sends it to their server, and now they can impersonate the user. With HttpOnly cookies, the script can't read it.

Vulnerability 2: No Token Expiry Validation

Search for jwt.verify calls and check if they actually validate expiresIn:

bash
claude search "jwt.verify" --type ts --context 3

Every call should check the exp claim. If there's custom token handling, search:

bash
claude search "jwt.decode(?!.*verify)" --type ts

jwt.decode without verify means the signature isn't checked. That's almost always wrong. Someone could forge a token with any data.

typescript
// BAD: Forges tokens
const decoded = jwt.decode(token); // No verification
console.log(decoded.userId);
 
// GOOD: Verifies signature and expiry
const decoded = jwt.verify(token, secret);
console.log(decoded.userId); // Safe to use

Vulnerability 3: Token Stored in Query Parameters

Search:

bash
claude search "url.*token|query.*token|params.*token" --type ts

If tokens appear in URLs, they're logged everywhere (proxies, CDNs, browser history, referrer headers when clicking links to external sites). They belong in headers or cookies only.

typescript
// BAD: Token exposed in URL logs
app.get("/auth/verify", (req, res) => {
  const token = req.query.token; // Logged by proxy, in browser history
  const decoded = jwt.verify(token, secret);
});
 
// GOOD: Token in header
app.get("/api/data", (req, res) => {
  const token = req.headers.authorization?.split(" ")[1];
  const decoded = jwt.verify(token, secret);
});

Vulnerability 4: No CSRF Protection on State-Changing Operations

Search for POST/PUT/DELETE routes and check if CSRF tokens are validated:

bash
claude search "router\.(post|put|delete)" --type ts --context 2

Look for CSRF middleware being used. If you see mutation operations without CSRF protection and without SameSite cookie restrictions, flag it.

typescript
// BAD: No CSRF protection
router.post("/transfer-money", authenticateToken, (req, res) => {
  // If user visits evil.com, it can POST to here
  // User's browser automatically includes cookies
});
 
// GOOD: CSRF token validation
router.post(
  "/transfer-money",
  authenticateToken,
  csrfProtection,
  (req, res) => {
    // evil.com can't generate valid CSRF token
  },
);

Vulnerability 5: Hardcoded Secrets

Search:

bash
claude search "JWT_SECRET.*=.*['\"]|REFRESH_SECRET.*=.*['\"]" --type ts

Any secret in source code is a vulnerability. Should be in environment variables only. This search might return false positives (from error messages, examples, docs), but even one real secret is critical.

typescript
// BAD: Hardcoded secret
const secret = "super-secret-key-12345"; // Everyone can see this
 
// GOOD: Environment variable
const secret = process.env.JWT_SECRET; // Loaded from .env, not in code

Checking for Rate Limiting

Authentication endpoints are brute-force targets. Search:

bash
claude search "rateLimit|throttle|login|refresh" --type ts

Look for rate limiting middleware on /login and /refresh:

typescript
// GOOD: Rate limiting on auth endpoints
app.post(
  "/login",
  rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }),
  authHandler,
);
 
// BAD: No rate limiting
app.post("/login", authHandler);

Without it, an attacker can try thousands of password combinations per second using tools like Hydra or Medusa.

Detecting Broken Token Validation

Search for patterns where tokens are partially validated:

bash
claude search "req.user|currentUser" --type ts --context -2

This shows where req.user is used (consumption point). If you see patterns where code checks if (req.user) instead of using the middleware that ensures it exists, that's a smell.

Better pattern:

typescript
// middleware/auth.ts - ensures req.user is set or throws
export const requireAuth = (req, res, next) => {
  const token = extractToken(req);
  if (!token) return res.status(401).json({ error: "Unauthorized" });
  try {
    req.user = verifyToken(token);
    next();
  } catch (err) {
    return res.status(401).json({ error: "Invalid token" });
  }
};
 
// Then in routes, if middleware is applied, req.user is guaranteed
router.get("/profile", requireAuth, (req, res) => {
  // req.user is ALWAYS present here—no need to check
  res.json(req.user);
});

Building Your Audit Report

After running searches, organize findings into a structured audit report. Here's a template:

Audit Report: Auth Flow Analysis

Scope: Full application auth system Date: [TODAY] Auditor: [YOUR NAME]

Executive Summary

[2-3 sentences on findings: major gaps, strengths, recommendations. For example: "Auth system uses properly separated secrets and token validation middleware. However, three endpoints are missing authentication checks entirely, and there is no refresh token rotation. Recommend immediate remediation of unprotected endpoints and implementation of refresh token rotation within one sprint."]

Finding 1: [ISSUE TITLE]

Severity: High/Medium/Low Location: path/to/file.ts line 42 Issue: [What's wrong and why it matters] Evidence: [Code snippet showing the problem] Recommendation: [How to fix it]

Finding 2: [...]

Strengths

  • [What's done well]
  • [Good patterns observed]

Next Steps

  • Priority 1: [Fix critical issues]
  • Priority 2: [Improve patterns]
  • Priority 3: [Optional enhancements]

Example Finding

Finding: Missing Rate Limiting on Login Endpoint

Severity: High Location: src/routes/auth.ts line 15 Issue: The /login endpoint accepts unlimited login attempts with no rate limiting. An attacker can perform brute-force password guessing at thousands of attempts per second using automated tools. Our logs show failed login attempts from single IPs regularly exceeding 100 per hour. Evidence:

typescript
router.post("/login", async (req, res) => {
  // NO rate limiting middleware
  const { email, password } = req.body;
  // ... password check happens immediately
});

Recommendation: Apply rate limiting middleware:

typescript
router.post('/login',
  rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }),
  async (req, res) => { ... }
);

This limits each IP to 5 login attempts per 15 minutes. For more sophisticated attackers who use proxy rotation, consider adding account-based limits (e.g., max 5 failed attempts per email address per day, then require password reset).


The Audit Workflow: From Chaos to Clarity

Here's how you actually run this in practice. It's not a single command—it's a systematic process.

Phase 1: Reconnaissance (30 min)

Search broadly to understand the shape of auth:

bash
# Find all auth-related files
claude search "auth|session|token|login|jwt" --type ts | head -20
 
# Get a sense of architecture
claude search "export const|export default" --type ts | grep -i auth
 
# Look for security keywords
claude search "secret|password|encrypt|hash" --type ts

This gives you an overview without getting lost in details. You're building a mental map.

Phase 2: Entry Point Mapping (45 min)

Find where auth starts:

bash
# Login endpoints
claude search "router.post.*login" --type ts
 
# Token endpoints
claude search "router.post.*token|router.post.*refresh" --type ts
 
# Signup if applicable
claude search "router.post.*signup|register" --type ts
 
# Password reset flow
claude search "router.post.*reset|forgot.*password" --type ts

Document these routes. They're your starting points. For each one, note: What are the parameters? What does it return? What validation does it do?

Phase 3: Flow Tracing (60 min)

From each entry point, trace where the flow goes:

bash
# From /login, where do tokens get created?
claude search "jwt.sign" --type ts
 
# From token verification, what middleware exists?
claude search "jwt.verify|authenticateToken" --type ts
 
# From authorization, what role checks exist?
claude search "authorizeRole|permission|role.*===" --type ts
 
# What middleware is defined?
claude search "export const.*=.*(req|next)|middleware" --type ts

Phase 4: Guard Mapping (45 min)

Audit every route systematically:

bash
# Get all routes
claude search "router\.(get|post|put|delete)\(" --type ts --context 1
 
# Cross-reference with middleware
# Is authenticateToken applied to protected routes?
# Is authorizeRole applied to admin routes?

For each unprotected route, ask: "Should this be public?" If not, it's a vulnerability.

Phase 5: Vulnerability Scanning (30 min)

Run the vulnerability searches from the "Advanced Techniques" section. Build a checklist:

  • No tokens in localStorage
  • No hardcoded secrets
  • All protected routes use middleware
  • Rate limiting on auth endpoints
  • Different secrets for different token types
  • Proper CSRF protection
  • Logout invalidates tokens

Phase 6: Documentation (30 min)

Synthesize findings into the report template above. Include the diagram, vulnerability list, and recommendations.

Total time: ~3.5 hours for a medium-sized app. You now have a complete, documented, verifiable auth architecture.


Practical Audit Checklist

Use this to systematically audit your auth:

Token Issuance

  • Access tokens have short expiry (15-30m)
  • Refresh tokens have longer expiry (7d+)
  • Different secrets used for different token types
  • Tokens include user ID, role, and any scopes
  • Tokens do NOT include sensitive data (passwords, secrets)
  • Tokens signed with strong algorithm (RS256 or HS256, not none)

Token Validation

  • All protected routes use auth middleware
  • Middleware checks token signature
  • Middleware checks token expiry
  • Middleware extracts user info safely
  • Middleware runs before any business logic

Authorization

  • Role checks use consistent middleware
  • Manual role checks are avoided
  • Permissions are granular (not just "admin" vs "user")
  • Sensitive operations require additional checks (e.g., email confirmation)
  • Cross-tenant data is never leaked

Session Management

  • Logout invalidates tokens immediately (via tokenVersion)
  • Password reset invalidates all tokens
  • "Revoke all sessions" option exists
  • Inactive sessions can be manually terminated
  • Session data is encrypted if stored server-side

Security Hardening

  • No token replay vulnerabilities (validate jti, iat)
  • Rate limiting on /login and /refresh
  • HTTPS enforced (tokens sent only over encrypted channels)
  • HttpOnly cookies (if tokens stored in cookies, not localStorage)
  • CSRF protection (if cookies used)
  • CORS properly configured (not * with credentials)

Testing

  • Expired tokens rejected
  • Invalid signatures rejected
  • Missing auth rejected with 401
  • Insufficient permissions rejected with 403
  • Refresh tokens work as intended
  • Revoked tokens rejected
  • Token tampering detected

The Power of Systematic Understanding

Here's what changes when you audit your auth with Claude Code:

Before: "How does auth work?" gets vague answers like "we use JWTs" or "there's middleware somewhere." New engineers spend a week asking questions. When a bug happens, debugging takes forever because nobody understands the flow.

After: You have a diagram. You know every token creation point. You know which routes are protected and which aren't. You can answer "what if someone steals a refresh token?" with specifics. A new engineer is productive in a day. When a bug happens, you trace it to the source in minutes.

This isn't just documentation—it's confidence. When the security team asks for an auth audit, you don't scramble. When a new engineer asks how to add auth to a new endpoint, you have a checklist. When a bug is found in the wild, you already understand the system well enough to fix it without breaking other parts.


Building Your Auth Audit as a Living Document

Here's what separates an audit report that sits in a Confluence page from one that actually influences your system: treating it as a living document that gets reviewed and updated quarterly.

Quarter 1 Audit: You map your auth system as it exists. You find vulnerabilities, document them, prioritize fixes.

Month 2-3: Fix the critical vulnerabilities. Update the audit document with remediation details (what was changed, why, when it ships).

Quarter 2 Audit: Run the audit again. Some items from Q1 are fixed. New code has been added—does it follow the auth patterns you documented? New vulnerabilities emerge (new ones, not old regressions).

Quarter 3: Deeper audit. You're not just checking for vulnerabilities anymore; you're optimizing. Are you still using separate secrets for different token types? Have any additional vulnerabilities been discovered in the field? Do your patterns match current OWASP guidance?

Quarter 4: Comprehensive review. You update the diagram, review with the team, use it for onboarding new engineers, plan next year's priorities.

This cycle means your auth audit is never "done." It evolves with your system. New engineers understand auth by reading the living document, not by asking random questions. When security advisories come out (new auth vulnerabilities discovered), you immediately check the document to see if you're affected. When bugs happen, they go into the document as lessons learned.

What Actually Changes When You Audit

This is the real payoff. Here's what becomes possible:

Before Audit:

  • "I wonder if we validate tokens?" (You don't know for sure)
  • New engineer spends a week learning auth (asks 50 questions)
  • Security audit takes weeks of investigation
  • Bug in auth takes days to debug and fix
  • New endpoint added without auth check (OWASP Top 10 #1: broken access control)

After Audit:

  • "We validate tokens in the authenticateToken middleware, applied to routes X, Y, Z"
  • New engineer reads the audit document and is productive on day 2
  • Security audit is 2 days of verification against a known system
  • Auth bugs are understood immediately because you know the flow
  • New endpoints go through a checklist ensuring auth is applied

The audit itself doesn't change your security posture. But the understanding it creates—the shared knowledge about how auth actually works in your system—changes everything.

Advanced Topic: Auth in Distributed Systems

The audit processes so far assume a monolithic application. But what if you're distributed? What if auth is handled in one service, but verified in another? What if you have multiple auth services?

This is where audits get complex. You need to trace tokens across service boundaries:

Client → API Gateway → Service A → Service B → Database

Auth validated at: API Gateway (token checked)
Auth re-validated at: Service A (token in header)
Auth trusted in: Service B (assumes Service A did its job)

In this pattern, you're distributing trust. Service B trusts that Service A validated the token correctly. This creates risks:

  • What if Service A is compromised? Service B still accepts forged tokens from it.
  • What if Service A and Service B disagree on token validation rules?
  • What if a token is revoked after Service A validates it but before Service B uses it?

The audit question becomes: Is trust distributed appropriately?

Better patterns:

All requests → API Gateway (single point of auth validation)
                     ↓
Services (assume tokens are already validated, don't re-validate)

Or:

All requests → Service A → Service B (B validates token signature again)

The audit maps which pattern you're using and whether it's secure.

Incident Response: When Auth Fails in Production

Auth vulnerabilities are different from other bugs. They affect system security guarantees. When one is discovered (whether through audit, bug reports, or security research), response is critical.

Hour 0-1 (Acknowledge)

  • Someone reports "I can see another user's data" or "I got logged out but my old token still works"
  • Confirm the vulnerability exists
  • Open incident war room

Hour 1-4 (Investigate)

  • If you have a living audit document: pull it up, trace the flow, identify the vulnerable component
  • If you don't: spend 3 hours reading code, asking questions, feeling panicked
  • Run the audit queries to understand scope (what users affected, how many, when did it start)

Hour 4-8 (Contain & Fix)

  • Immediately patch the vulnerability
  • Deploy the fix with high priority
  • Begin notifying affected users (what happened, what we're doing about it, what they should do)

Hour 8+ (Aftermath)

  • Update the audit to document the vulnerability and fix
  • Add tests to prevent regression
  • Analyze: how did this vulnerability exist? Why didn't the audit catch it?
  • Update your audit process to catch similar issues in the future

The difference between a contained incident (1 hour downtime, 10,000 users affected briefly) and a disaster (3 days of investigation, user data leaked) often comes down to understanding your system. A living audit makes that understanding immediate.

Scaling Auth Audits to Larger Teams

As your team grows, auditing becomes harder (more code) and more important (more people touching auth code). Here's how to scale:

Team ≤ 5 people: One person (security-focused) owns the auth audit. They understand everything and can answer all questions. Audit quarterly.

Team 5-20 people: Pair model—the security engineer and a core platform engineer together audit and maintain the document. They own auth standards and review PRs adding new auth features. Audit quarterly.

Team 20+ people: Distributed ownership but centralized standards. Each service has an owner responsible for auth in that service. They audit quarterly. A central security team coordinates, maintains best practices, and does spot checks. Quarterly team audit (all service owners together) ensures consistency.

Team 100+ people: Formal auth architecture governance. Dedicated security team. Auth pattern library (approved ways to do auth across the company). Quarterly audits per service plus annual comprehensive audit. Violations of established patterns get flagged automatically in code review.

The key is that audit scales with team size. You can't expect a 100-person team to all understand each other's auth code. You need distributed understanding but centralized standards.

Automation: Moving from Manual to Continuous Auditing

Right now, we've framed audits as quarterly events. But increasingly, auth auditing is becoming continuous.

Continuous Auth Auditing:

yaml
# CI/CD integration
on: [pull_request]
 
jobs:
  auth-audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      # Check for common auth vulnerabilities in the PR
      - name: Scan for Auth Vulnerabilities
        run: |
          # Look for dangerous patterns in this PR
          grep -r "localStorage.*token\|Math.random.*auth\|hardcode.*secret" . \
            --include="*.ts" --include="*.js" && exit 1 || true
 
      # Ensure middleware is applied to new routes
      - name: Check Route Protection
        run: |
          # Extract new routes added in this PR
          git diff HEAD~1 | grep "^+.*router\." | grep -v "authenticateToken" && \
            echo "⚠️ New routes without auth middleware detected" || true
 
      # Verify token validation hasn't been weakened
      - name: Check Token Validation
        run: |
          # Ensure jwt.verify is always followed by expiry check
          # (This is simplified; real checks would be more sophisticated)
          grep -A3 "jwt.verify" . --include="*.ts" | grep -v "exp" && \
            echo "⚠️ Possible incomplete token validation" || true

This runs on every PR, checking for common auth mistakes. It's not perfect (false positives, false negatives), but it's better than nothing and catches the obvious issues before they merge.

More sophisticated: machine learning models trained on past vulnerabilities and security practices can detect subtle auth issues. As your codebase grows, this becomes invaluable.

  • Rate Limiting & Auth: Protect your login endpoint from brute force attacks
  • Token Rotation: Advanced strategy for handling compromised tokens
  • Passwordless Auth: Moving beyond passwords (WebAuthn, magic links, FIDO2)
  • Distributed Sessions: Auth in microservices and federated systems
  • Security Logging: Detecting and responding to auth anomalies in real time
  • Multi-Factor Authentication: Adding additional verification layers
  • OAuth 2.0 & OpenID Connect: Third-party integration patterns
  • SAML and Enterprise Auth: Single sign-on at scale
  • JWT Best Practices: Token structure, claims, validation
  • Session vs Token Auth: When to use each approach

The field of authentication is constantly evolving. New vulnerabilities are discovered, new standards emerge, new best practices form. Your audit process should evolve with it. Use your quarterly audits not just to check current state, but to learn about emerging threats and adapt your approach.


-iNet

Need help implementing this?

We build automation systems like this for clients every day.

Discuss Your Project