- Auth Security Expert
- ⚠️ CRITICAL: OAuth 2.1 becomes MANDATORY Q2 2026
- OAuth 2.1 consolidates a decade of security best practices into a single specification (draft-ietf-oauth-v2-1). Google, Microsoft, and Okta have already deprecated legacy OAuth 2.0 flows with enforcement deadlines in Q2 2026.
- Required Changes from OAuth 2.0
- 1. PKCE is REQUIRED for ALL Clients
- Previously optional, now MANDATORY for public AND confidential clients
- Prevents authorization code interception and injection attacks
- Code verifier: 43-128 cryptographically random URL-safe characters
- Code challenge: BASE64URL(SHA256(code_verifier))
- Code challenge method: MUST be 'S256' (SHA-256), not 'plain'
- // Correct PKCE implementation
- async
- function
- generatePKCE
- (
- )
- {
- const
- array
- =
- new
- Uint8Array
- (
- 32
- )
- ;
- // 256 bits
- crypto
- .
- getRandomValues
- (
- array
- )
- ;
- // Cryptographically secure random
- const
- verifier
- =
- base64UrlEncode
- (
- array
- )
- ;
- const
- encoder
- =
- new
- TextEncoder
- (
- )
- ;
- const
- hash
- =
- await
- crypto
- .
- subtle
- .
- digest
- (
- 'SHA-256'
- ,
- encoder
- .
- encode
- (
- verifier
- )
- )
- ;
- const
- challenge
- =
- base64UrlEncode
- (
- new
- Uint8Array
- (
- hash
- )
- )
- ;
- return
- {
- verifier
- ,
- challenge
- }
- ;
- }
- // Helper: Base64 URL encoding
- function
- base64UrlEncode
- (
- buffer
- )
- {
- return
- btoa
- (
- String
- .
- fromCharCode
- (
- ...
- new
- Uint8Array
- (
- buffer
- )
- )
- )
- .
- replace
- (
- /
- +
- /
- g
- ,
- '-'
- )
- .
- replace
- (
- /
- \/
- /
- g
- ,
- '_'
- )
- .
- replace
- (
- /
- =
- +
- $
- /
- ,
- ''
- )
- ;
- }
- 2. Implicit Flow REMOVED
- ❌
- response_type=token
- or
- response_type=id_token token
- - FORBIDDEN
- Tokens exposed in URL fragments leak via:
- Browser history
- Referrer headers to third-party scripts
- Server logs (if fragment accidentally logged)
- Browser extensions
- Migration
-
- Use Authorization Code Flow + PKCE for ALL SPAs
- 3. Resource Owner Password Credentials (ROPC) REMOVED
- ❌
- grant_type=password
- - FORBIDDEN
- Violates delegated authorization principle
- Increases phishing and credential theft risk
- Forces users to trust client with credentials
- Migration
-
- Authorization Code Flow for users, Client Credentials for services
- 4. Bearer Tokens in URI Query Parameters FORBIDDEN
- ❌
- GET /api/resource?access_token=xyz
- - FORBIDDEN
- Tokens leak via:
- Server access logs
- Proxy logs
- Browser history
- Referrer headers
- ✅ Use Authorization header:
- Authorization: Bearer
- ✅ Or secure POST body parameter
- 5. Exact Redirect URI Matching REQUIRED
- No wildcards:
- https://*.example.com
- - FORBIDDEN
- No partial matches or subdomain wildcards
- MUST perform exact string comparison
- Prevents open redirect vulnerabilities
- Implementation
- Register each redirect URI explicitly
// Server-side redirect URI validation
function
validateRedirectUri
(
requestedUri
,
registeredUris
)
{
// EXACT match required - no wildcards, no normalization
return
registeredUris
.
includes
(
requestedUri
)
;
}
6. Refresh Token Protection REQUIRED
MUST implement ONE of:
Sender-constrained tokens
(mTLS, DPoP - Demonstrating Proof-of-Possession)
Refresh token rotation
with reuse detection (recommended for most apps)
PKCE Downgrade Attack Prevention
The Attack:
Attacker intercepts authorization request and strips
code_challenge
parameters. If authorization server allows backward compatibility with OAuth 2.0 (non-PKCE), it proceeds without PKCE protection. Attacker steals authorization code and exchanges it without needing the
code_verifier
.
Prevention (Server-Side):
// Authorization endpoint - REJECT requests without PKCE
app
.
get
(
'/authorize'
,
(
req
,
res
)
=>
{
const
{
code_challenge
,
code_challenge_method
}
=
req
.
query
;
// OAuth 2.1: PKCE is MANDATORY
if
(
!
code_challenge
||
!
code_challenge_method
)
{
return
res
.
status
(
400
)
.
json
(
{
error
:
'invalid_request'
,
error_description
:
'code_challenge required (OAuth 2.1)'
,
}
)
;
}
if
(
code_challenge_method
!==
'S256'
)
{
return
res
.
status
(
400
)
.
json
(
{
error
:
'invalid_request'
,
error_description
:
'code_challenge_method must be S256'
,
}
)
;
}
// Continue authorization flow...
}
)
;
// Token endpoint - VERIFY code_verifier
app
.
post
(
'/token'
,
async
(
req
,
res
)
=>
{
const
{
code
,
code_verifier
}
=
req
.
body
;
const
authCode
=
await
db
.
authorizationCodes
.
findOne
(
{
code
}
)
;
if
(
!
authCode
.
code_challenge
)
{
return
res
.
status
(
400
)
.
json
(
{
error
:
'invalid_grant'
,
error_description
:
'Authorization code was not issued with PKCE'
,
}
)
;
}
// Verify code_verifier matches code_challenge
const
hash
=
crypto
.
createHash
(
'sha256'
)
.
update
(
code_verifier
)
.
digest
(
)
;
const
challenge
=
base64UrlEncode
(
hash
)
;
if
(
challenge
!==
authCode
.
code_challenge
)
{
return
res
.
status
(
400
)
.
json
(
{
error
:
'invalid_grant'
,
error_description
:
'code_verifier does not match code_challenge'
,
}
)
;
}
// Issue tokens...
}
)
;
Authorization Code Flow with PKCE (Step-by-Step)
Client-Side Implementation:
// Step 1: Generate PKCE parameters
const
{
verifier
,
challenge
}
=
await
generatePKCE
(
)
;
sessionStorage
.
setItem
(
'pkce_verifier'
,
verifier
)
;
// Temporary only
sessionStorage
.
setItem
(
'oauth_state'
,
generateRandomState
(
)
)
;
// CSRF protection
// Step 2: Redirect to authorization endpoint
const
authUrl
=
new
URL
(
'https://auth.example.com/authorize'
)
;
authUrl
.
searchParams
.
set
(
'response_type'
,
'code'
)
;
authUrl
.
searchParams
.
set
(
'client_id'
,
CLIENT_ID
)
;
authUrl
.
searchParams
.
set
(
'redirect_uri'
,
REDIRECT_URI
)
;
// MUST match exactly
authUrl
.
searchParams
.
set
(
'scope'
,
'openid profile email'
)
;
authUrl
.
searchParams
.
set
(
'state'
,
sessionStorage
.
getItem
(
'oauth_state'
)
)
;
authUrl
.
searchParams
.
set
(
'code_challenge'
,
challenge
)
;
authUrl
.
searchParams
.
set
(
'code_challenge_method'
,
'S256'
)
;
window
.
location
.
href
=
authUrl
.
toString
(
)
;
// Step 3: Handle callback (after user authorizes)
// URL: https://yourapp.com/callback?code=xyz&state=abc
const
urlParams
=
new
URLSearchParams
(
window
.
location
.
search
)
;
const
code
=
urlParams
.
get
(
'code'
)
;
const
state
=
urlParams
.
get
(
'state'
)
;
// Validate state (CSRF protection)
if
(
state
!==
sessionStorage
.
getItem
(
'oauth_state'
)
)
{
throw
new
Error
(
'State mismatch - possible CSRF attack'
)
;
}
// Step 4: Exchange code for tokens
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
:
code
,
redirect_uri
:
REDIRECT_URI
,
// MUST match authorization request
client_id
:
CLIENT_ID
,
code_verifier
:
sessionStorage
.
getItem
(
'pkce_verifier'
)
,
// Prove possession
}
)
,
}
)
;
const
tokens
=
await
response
.
json
(
)
;
// Clear PKCE parameters immediately
sessionStorage
.
removeItem
(
'pkce_verifier'
)
;
sessionStorage
.
removeItem
(
'oauth_state'
)
;
// Server should set tokens as HttpOnly cookies (see Token Storage section)
OAuth 2.1 Security Checklist
Before Production Deployment:
PKCE enabled for ALL clients (public AND confidential)
PKCE downgrade prevention (reject requests without code_challenge)
Implicit Flow completely disabled/removed
Password Credentials Flow disabled/removed
Exact redirect URI matching enforced (no wildcards)
Tokens NEVER in URL query parameters
Authorization server rejects 'code_challenge_method=plain'
State parameter validated (CSRF protection)
All communication over HTTPS only
Token endpoint requires client authentication (for confidential clients)
JWT Security (RFC 8725 - Best Practices)
⚠️ CRITICAL: JWT vulnerabilities are in OWASP Top 10 (Broken Authentication)
Token Lifecycle Best Practices
Access Tokens:
Lifetime:
≤15 minutes maximum
(recommended: 5-15 minutes)
Short-lived to limit damage from token theft
Stateless validation (no database lookup needed)
Include minimal claims (user ID, permissions, expiry)
Refresh Tokens:
Lifetime: Days to weeks (7-30 days typical)
MUST implement rotation (issue new, invalidate old)
Stored securely server-side (hashed, like passwords)
Revocable (require database lookup)
ID Tokens (OpenID Connect):
Short-lived (5-60 minutes)
Contains user profile information
MUST validate signature and claims
Never use for API authorization (use access tokens)
JWT Signature Algorithms (RFC 8725)
✅ RECOMMENDED Algorithms:
RS256 (RSA with SHA-256)
Asymmetric signing (private key signs, public key verifies)
Best for distributed systems (API gateway can verify without private key)
Key size: 2048-bit minimum (4096-bit for high security)
const
jwt
=
require
(
'jsonwebtoken'
)
;
const
fs
=
require
(
'fs'
)
;
// Sign with private key
const
privateKey
=
fs
.
readFileSync
(
'private.pem'
)
;
const
token
=
jwt
.
sign
(
payload
,
privateKey
,
{
algorithm
:
'RS256'
,
expiresIn
:
'15m'
,
issuer
:
'https://auth.example.com'
,
audience
:
'api.example.com'
,
keyid
:
'key-2024-01'
,
// Key rotation tracking
}
)
;
// Verify with public key
const
publicKey
=
fs
.
readFileSync
(
'public.pem'
)
;
const
decoded
=
jwt
.
verify
(
token
,
publicKey
,
{
algorithms
:
[
'RS256'
]
,
// Whitelist ONLY expected algorithm
issuer
:
'https://auth.example.com'
,
audience
:
'api.example.com'
,
}
)
;
ES256 (ECDSA with SHA-256)
Asymmetric signing (smaller keys than RSA, same security)
Faster signing/verification than RSA
Key size: 256-bit (equivalent to 3072-bit RSA)
// Generate ES256 key pair (one-time setup)
const
{
generateKeyPairSync
}
=
require
(
'crypto'
)
;
const
{
privateKey
,
publicKey
}
=
generateKeyPairSync
(
'ec'
,
{
namedCurve
:
'prime256v1'
,
// P-256 curve
}
)
;
const
token
=
jwt
.
sign
(
payload
,
privateKey
,
{
algorithm
:
'ES256'
,
expiresIn
:
'15m'
,
}
)
;
⚠️ USE WITH CAUTION:
HS256 (HMAC with SHA-256)
Symmetric signing (same secret for sign and verify)
ONLY for single-server systems (secret must be shared to verify)
NEVER expose secret to clients
NEVER use if API gateway/microservices need to verify tokens
// Only use HS256 if ALL verification happens on same server
const
secret
=
process
.
env
.
JWT_SECRET
;
// 256-bit minimum
const
token
=
jwt
.
sign
(
payload
,
secret
,
{
algorithm
:
'HS256'
,
expiresIn
:
'15m'
,
}
)
;
const
decoded
=
jwt
.
verify
(
token
,
secret
,
{
algorithms
:
[
'HS256'
]
,
// STILL whitelist algorithm
}
)
;
❌ FORBIDDEN Algorithms:
none (No Signature)
// NEVER accept unsigned tokens
const
decoded
=
jwt
.
verify
(
token
,
null
,
{
algorithms
:
[
'none'
]
,
// ❌ CRITICAL VULNERABILITY
}
)
;
// Attacker can create token: {"alg":"none","typ":"JWT"}.{"sub":"admin"}
Prevention:
// ALWAYS whitelist allowed algorithms, NEVER allow 'none'
jwt
.
verify
(
token
,
publicKey
,
{
algorithms
:
[
'RS256'
,
'ES256'
]
,
// Whitelist only
}
)
;
JWT Validation (Complete Checklist)
async
function
validateAccessToken
(
token
)
{
try
{
// 1. Parse without verification first (to check 'alg')
const
unverified
=
jwt
.
decode
(
token
,
{
complete
:
true
}
)
;
// 2. Reject 'none' algorithm
if
(
!
unverified
||
unverified
.
header
.
alg
===
'none'
)
{
throw
new
Error
(
'Unsigned JWT not allowed'
)
;
}
// 3. Verify signature with public key
const
publicKey
=
await
getPublicKey
(
unverified
.
header
.
kid
)
;
// Key ID
const
decoded
=
jwt
.
verify
(
token
,
publicKey
,
{
algorithms
:
[
'RS256'
,
'ES256'
]
,
// Whitelist expected algorithms
issuer
:
'https://auth.example.com'
,
// Expected issuer
audience
:
'api.example.com'
,
// This API's identifier
clockTolerance
:
30
,
// Allow 30s clock skew
complete
:
false
,
// Return payload only
}
)
;
// 4. Validate required claims
if
(
!
decoded
.
sub
)
throw
new
Error
(
'Missing subject (sub) claim'
)
;
if
(
!
decoded
.
exp
)
throw
new
Error
(
'Missing expiry (exp) claim'
)
;
if
(
!
decoded
.
iat
)
throw
new
Error
(
'Missing issued-at (iat) claim'
)
;
if
(
!
decoded
.
jti
)
throw
new
Error
(
'Missing JWT ID (jti) claim'
)
;
// 5. Validate token lifetime (belt-and-suspenders with jwt.verify)
const
now
=
Math
.
floor
(
Date
.
now
(
)
/
1000
)
;
if
(
decoded
.
exp
<=
now
)
throw
new
Error
(
'Token expired'
)
;
if
(
decoded
.
nbf
&&
decoded
.
nbf
now ) throw new Error ( 'Token not yet valid' ) ; // 6. Check token revocation (if implementing revocation list) if ( await isTokenRevoked ( decoded . jti ) ) { throw new Error ( 'Token has been revoked' ) ; } // 7. Validate custom claims if ( decoded . scope && ! decoded . scope . includes ( 'read:resource' ) ) { throw new Error ( 'Insufficient permissions' ) ; } return decoded ; } catch ( error ) { // NEVER use the token if ANY validation fails console . error ( 'JWT validation failed:' , error . message ) ; throw new Error ( 'Invalid token' ) ; } } JWT Claims (RFC 7519) Registered Claims (Standard): iss (issuer): Authorization server URL - VALIDATE sub (subject): User ID (unique, immutable) - REQUIRED aud (audience): API/service identifier - VALIDATE exp (expiration): Unix timestamp - REQUIRED, ≤15 min for access tokens iat (issued at): Unix timestamp - REQUIRED nbf (not before): Unix timestamp - OPTIONAL jti (JWT ID): Unique token ID - REQUIRED for revocation Custom Claims (Application-Specific): const payload = { // Standard claims iss : 'https://auth.example.com' , sub : 'user_12345' , aud : 'api.example.com' , exp : Math . floor ( Date . now ( ) / 1000 ) + 15 * 60 , // 15 minutes iat : Math . floor ( Date . now ( ) / 1000 ) , jti : crypto . randomUUID ( ) , // Custom claims scope : 'read:profile write:profile admin:users' , role : 'admin' , tenant_id : 'tenant_789' , email : 'user@example.com' , // OK for access token, not sensitive // NEVER include: password, SSN, credit card, etc. } ; ⚠️ NEVER Store Sensitive Data in JWT: JWTs are base64-encoded, NOT encrypted (anyone can decode) Assume all JWT contents are public Use encrypted JWE (JSON Web Encryption) if you must include sensitive data Token Storage Security ✅ CORRECT: HttpOnly Cookies (Server-Side) // Server sets tokens as HttpOnly cookies after OAuth callback app . post ( '/auth/callback' , async ( req , res ) => { const { access_token , refresh_token } = await exchangeCodeForTokens ( req . body . code ) ; // Access token cookie res . cookie ( 'access_token' , access_token , { httpOnly : true , // Cannot be accessed by JavaScript (XSS protection) secure : true , // HTTPS only sameSite : 'strict' , // CSRF protection (blocks cross-site requests) maxAge : 15 * 60 * 1000 , // 15 minutes path : '/' , domain : '.example.com' , // Allow subdomains } ) ; // Refresh token cookie (more restricted) res . cookie ( 'refresh_token' , refresh_token , { httpOnly : true , secure : true , sameSite : 'strict' , maxAge : 7 * 24 * 60 * 60 * 1000 , // 7 days path : '/auth/refresh' , // ONLY accessible by refresh endpoint domain : '.example.com' , } ) ; res . json ( { success : true } ) ; } ) ; // Client makes authenticated requests (browser sends cookie automatically) fetch ( 'https://api.example.com/user/profile' , { credentials : 'include' , // Include cookies in request } ) ; ❌ WRONG: localStorage/sessionStorage // ❌ VULNERABLE TO XSS ATTACKS localStorage . setItem ( 'access_token' , token ) ; sessionStorage . setItem ( 'access_token' , token ) ; // Any XSS vulnerability (even third-party script) can steal tokens: // Why HttpOnly Cookies Prevent XSS Theft: httpOnly: true makes cookie inaccessible to JavaScript (document.cookie returns empty) Even if XSS exists, attacker cannot read the token Browser automatically includes cookie in requests (no JavaScript needed) Refresh Token Rotation with Reuse Detection The Attack: Refresh Token Theft If attacker steals refresh token, they can generate unlimited access tokens until refresh token expires (days/weeks). The Defense: Rotation + Reuse Detection Every refresh generates new refresh token and invalidates old one. If old token is used again, ALL tokens for that user are revoked (signals possible theft). Server-Side Implementation: app . post ( '/auth/refresh' , async ( req , res ) => { const oldRefreshToken = req . cookies . refresh_token ; try { // 1. Validate refresh token (check signature, expiry) const decoded = jwt . verify ( oldRefreshToken , publicKey , { algorithms : [ 'RS256' ] , issuer : 'https://auth.example.com' , } ) ; // 2. Look up token in database (we store hashed refresh tokens) const tokenHash = crypto . createHash ( 'sha256' ) . update ( oldRefreshToken ) . digest ( 'hex' ) ; const tokenRecord = await db . refreshTokens . findOne ( { tokenHash , userId : decoded . sub , } ) ; if ( ! tokenRecord ) { throw new Error ( 'Refresh token not found' ) ; } // 3. CRITICAL: Detect token reuse (possible theft) if ( tokenRecord . isUsed ) { // Token was already used - this is a REUSE ATTACK await db . refreshTokens . deleteMany ( { userId : decoded . sub } ) ; // Revoke ALL tokens await logSecurityEvent ( 'REFRESH_TOKEN_REUSE_DETECTED' , { userId : decoded . sub , tokenId : decoded . jti , ip : req . ip , userAgent : req . headers [ 'user-agent' ] , } ) ; // Send alert to user's email await sendSecurityAlert ( decoded . sub , 'Token theft detected - all sessions terminated' ) ; return res . status ( 401 ) . json ( { error : 'token_reuse' , error_description : 'Refresh token reuse detected - all sessions revoked' , } ) ; } // 4. Mark old token as used (ATOMIC operation before issuing new tokens) await db . refreshTokens . updateOne ( { tokenHash } , { $set : { isUsed : true , lastUsedAt : new Date ( ) } , } ) ; // 5. Generate new tokens const newAccessToken = jwt . sign ( { sub : decoded . sub , scope : decoded . scope , exp : Math . floor ( Date . now ( ) / 1000 ) + 15 * 60 , } , privateKey , { algorithm : 'RS256' } ) ; const newRefreshToken = jwt . sign ( { sub : decoded . sub , scope : decoded . scope , exp : Math . floor ( Date . now ( ) / 1000 ) + 7 * 24 * 60 * 60 , // 7 days jti : crypto . randomUUID ( ) , } , privateKey , { algorithm : 'RS256' } ) ; // 6. Store new refresh token (hashed) const newTokenHash = crypto . createHash ( 'sha256' ) . update ( newRefreshToken ) . digest ( 'hex' ) ; await db . refreshTokens . create ( { userId : decoded . sub , tokenHash : newTokenHash , isUsed : false , expiresAt : new Date ( Date . now ( ) + 7 * 24 * 60 * 60 * 1000 ) , createdAt : new Date ( ) , userAgent : req . headers [ 'user-agent' ] , ipAddress : req . ip , } ) ; // 7. Set new tokens as cookies res . cookie ( 'access_token' , newAccessToken , { httpOnly : true , secure : true , sameSite : 'strict' , maxAge : 15 * 60 * 1000 , } ) ; res . cookie ( 'refresh_token' , newRefreshToken , { httpOnly : true , secure : true , sameSite : 'strict' , maxAge : 7 * 24 * 60 * 60 * 1000 , path : '/auth/refresh' , } ) ; res . json ( { success : true } ) ; } catch ( error ) { // Clear invalid cookies res . clearCookie ( 'refresh_token' ) ; res . status ( 401 ) . json ( { error : 'invalid_token' } ) ; } } ) ; Database Schema (Refresh Tokens): { userId : 'user_12345' , tokenHash : 'sha256_hash_of_refresh_token' , // NEVER store plaintext isUsed : false , // Set to true when token is used for refresh expiresAt : ISODate ( '2026-02-01T00:00:00Z' ) , createdAt : ISODate ( '2026-01-25T00:00:00Z' ) , lastUsedAt : null , // Updated when isUsed set to true userAgent : 'Mozilla/5.0...' , ipAddress : '192.168.1.1' , jti : 'uuid-v4' , // Matches JWT 'jti' claim } Password Hashing (2026 Best Practices) Recommended: Argon2id Winner of Password Hashing Competition (2015) Resistant to both GPU cracking and side-channel attacks Configurable memory, time, and parallelism parameters // Argon2id example (Node.js) import argon2 from 'argon2' ; // Hash password const hash = await argon2 . hash ( password , { type : argon2 . argon2id , memoryCost : 19456 , // 19 MiB timeCost : 2 , parallelism : 1 , } ) ; // Verify password const isValid = await argon2 . verify ( hash , password ) ; Acceptable Alternative: bcrypt Still secure but slower than Argon2id for same security level Work factor: minimum 12 (recommended 14+ in 2026) // bcrypt example import bcrypt from 'bcryptjs' ; const hash = await bcrypt . hash ( password , 14 ) ; // Cost factor 14 const isValid = await bcrypt . compare ( password , hash ) ; NEVER use: MD5, SHA-1, SHA-256 alone (not designed for passwords) Plain text storage Reversible encryption Multi-Factor Authentication (MFA) Types of MFA: TOTP (Time-based One-Time Passwords) Apps: Google Authenticator, Authy, 1Password 6-digit codes that rotate every 30 seconds Offline-capable WebAuthn/FIDO2 (Passkeys) Most secure option (phishing-resistant) Hardware tokens (YubiKey) or platform authenticators (Face ID, Touch ID) Public key cryptography, no shared secrets SMS-based (Legacy - NOT recommended) Vulnerable to SIM swapping attacks Use only as fallback, never as primary MFA Backup Codes Provide one-time recovery codes during MFA enrollment Store securely (hashed in database) Implementation Best Practices: Allow multiple MFA methods per user Enforce MFA for admin/privileged accounts Provide clear enrollment and recovery flows Never bypass MFA without proper verification Passkeys / WebAuthn Why Passkeys: Phishing-resistant (cryptographic binding to origin) No shared secrets to leak or intercept Passwordless authentication Synced across devices (Apple, Google, Microsoft ecosystems) Implementation: Use @simplewebauthn/server (Node.js) or similar libraries Support both platform authenticators (biometrics) and roaming authenticators (security keys) Provide fallback authentication method during transition WebAuthn Registration Flow: Server generates challenge Client creates credential with authenticator Client sends public key to server Server stores public key associated with user account WebAuthn Authentication Flow: Server generates challenge Client signs challenge with private key (stored in authenticator) Server verifies signature with stored public key Session Management Secure Session Practices: Use secure, HTTP-only cookies for session tokens Set-Cookie: session=...; Secure; HttpOnly; SameSite=Strict Implement absolute timeout (e.g., 24 hours) Implement idle timeout (e.g., 30 minutes of inactivity) Regenerate session ID after login (prevent session fixation) Provide "logout all devices" functionality Session Storage: Server-side session store (Redis, database) Don't store sensitive data in client-side storage Implement session revocation on password change Security Headers Essential HTTP Security Headers: Strict-Transport-Security: max-age=31536000; includeSubDomains (HSTS) X-Content-Type-Options: nosniff X-Frame-Options: DENY or SAMEORIGIN Content-Security-Policy: default-src 'self' X-XSS-Protection: 1; mode=block (legacy support) Common Vulnerabilities to Prevent Injection Attacks: Use parameterized queries (SQL injection prevention) Validate and sanitize all user input Use ORMs with built-in protection Cross-Site Scripting (XSS): Escape output in templates Use Content Security Policy headers Never use eval() or innerHTML with user input Cross-Site Request Forgery (CSRF): Use CSRF tokens for state-changing operations Verify origin/referer headers Use SameSite cookie attribute Broken Authentication: Enforce strong password policies Implement account lockout after failed attempts Use MFA for sensitive operations Never expose user enumeration (same error for "user not found" and "invalid password") Consolidated Skills This expert skill consolidates 1 individual skills: auth-security-expert
auth-security-expert
安装
npx skills add https://github.com/oimiragieo/agent-studio --skill auth-security-expert