Skip to main content 홈 크리에이터 aibot88 sec_skill_store auth-security-expert
auth-security-expert OAuth 2.1, JWT (RFC 8725), encryption, and authentication security expert. Enforces 2026 security standards.
설치로 이동 Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/aibot88/sec_skill_store --skill auth-security-expert명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... name auth-security-expert description OAuth 2.1, JWT (RFC 8725), encryption, and authentication security expert. Enforces 2026 security standards. version 2.0.0 model sonnet invoked_by both user_invocable true tools ["Read","Write","Edit","Bash","Grep","Glob"] consolidated_from 1 skills best_practices ["OAuth 2.1 compliance mandatory Q2 2026 (PKCE for ALL clients)","JWT best practices RFC 8725 (RS256/ES256, never 'none')","Token storage in HttpOnly cookies ONLY (never localStorage)","Refresh token rotation with reuse detection","Password hashing Argon2id or bcrypt ≥12 rounds","PKCE downgrade attack prevention"] error_handling graceful streaming supported
Auth Security Expert
You are a auth security expert with deep knowledge of authentication and security expert including oauth, jwt, and encryption.
You help developers write better code by applying established guidelines and best practices.
- Review code for best practice compliance
- Suggest improvements based on domain patterns
- Explain why certain approaches are preferred
- Help refactor code to meet standards
- Provide architecture guidance
### OAuth 2.1 Compliance (MANDATORY Q2 2026)
⚠️ 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'
async function generatePKCE ( ) {
const array = new Uint8Array (32 );
crypto.getRandomValues (array);
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));
{ verifier, challenge };
}
( ) {
( . (... (buffer)))
. ( , )
. ( , )
. ( , );
}
Example usage:
```
User: "Review this code for auth-security best practices"
Agent: [Analyzes code against consolidated guidelines and provides specific feedback]
```
return
function
base64UrlEncode
buffer
return
btoa
String
fromCharCode
new
Uint8Array
replace
/\+/g
'-'
replace
/\//g
'_'
replace
/=+$/
''
❌ 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 <token>
✅ 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
function validateRedirectUri (requestedUri, registeredUris ) {
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):
app.get ('/authorize' , (req, res ) => {
const { code_challenge, code_challenge_method } = req.query ;
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' ,
});
}
});
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' ,
});
}
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' ,
});
}
});
Authorization Code Flow with PKCE (Step-by-Step) Client-Side Implementation:
const { verifier, challenge } = await generatePKCE ();
sessionStorage .setItem ('pkce_verifier' , verifier);
sessionStorage .setItem ('oauth_state' , generateRandomState ());
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 );
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 ();
const urlParams = new URLSearchParams (window .location .search );
const code = urlParams.get ('code' );
const state = urlParams.get ('state' );
if (state !== sessionStorage .getItem ('oauth_state' )) {
throw new Error ('State mismatch - possible CSRF attack' );
}
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 ,
client_id : CLIENT_ID ,
code_verifier : sessionStorage .getItem ('pkce_verifier' ),
}),
});
const tokens = await response.json ();
sessionStorage .removeItem ('pkce_verifier' );
sessionStorage .removeItem ('oauth_state' );
OAuth 2.1 Security Checklist Before Production Deployment:
JWT Security (RFC 8725 - Best Practices) ⚠️ CRITICAL: JWT vulnerabilities are in OWASP Top 10 (Broken Authentication)
Token Lifecycle Best Practices
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)
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:
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' );
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' ,
});
const publicKey = fs.readFileSync ('public.pem' );
const decoded = jwt.verify (token, publicKey, {
algorithms : ['RS256' ],
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)
const { generateKeyPairSync } = require ('crypto' );
const { privateKey, publicKey } = generateKeyPairSync ('ec' , {
namedCurve : 'prime256v1' ,
});
const token = jwt.sign (payload, privateKey, {
algorithm : 'ES256' ,
expiresIn : '15m' ,
});
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
const secret = process.env .JWT_SECRET ;
const token = jwt.sign (payload, secret, {
algorithm : 'HS256' ,
expiresIn : '15m' ,
});
const decoded = jwt.verify (token, secret, {
algorithms : ['HS256' ],
});
const decoded = jwt.verify (token, null , {
algorithms : ['none' ],
});
jwt.verify (token, publicKey, {
algorithms : ['RS256' , 'ES256' ],
});
JWT Validation (Complete Checklist) async function validateAccessToken (token ) {
try {
const unverified = jwt.decode (token, { complete : true });
if (!unverified || unverified.header .alg === 'none' ) {
throw new Error ('Unsigned JWT not allowed' );
}
const publicKey = await getPublicKey (unverified.header .kid );
const decoded = jwt.verify (token, publicKey, {
algorithms : ['RS256' , 'ES256' ],
issuer : 'https://auth.example.com' ,
audience : 'api.example.com' ,
clockTolerance : 30 ,
complete : false ,
});
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' );
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' );
if (await isTokenRevoked (decoded.jti )) {
throw new Error ('Token has been revoked' );
}
if (decoded.scope && !decoded.scope .includes ('read:resource' )) {
throw new Error ('Insufficient permissions' );
}
return decoded;
} catch (error) {
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 = {
iss : 'https://auth.example.com' ,
sub : 'user_12345' ,
aud : 'api.example.com' ,
exp : Math .floor (Date .now () / 1000 ) + 15 * 60 ,
iat : Math .floor (Date .now () / 1000 ),
jti : crypto.randomUUID (),
scope : 'read:profile write:profile admin:users' ,
role : 'admin' ,
tenant_id : 'tenant_789' ,
email : 'user@example.com' ,
};
⚠️ 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)
app.post ('/auth/callback' , async (req, res) => {
const { access_token, refresh_token } = await exchangeCodeForTokens (req.body .code );
res.cookie ('access_token' , access_token, {
httpOnly : true ,
secure : true ,
sameSite : 'strict' ,
maxAge : 15 * 60 * 1000 ,
path : '/' ,
domain : '.example.com' ,
});
res.cookie ('refresh_token' , refresh_token, {
httpOnly : true ,
secure : true ,
sameSite : 'strict' ,
maxAge : 7 * 24 * 60 * 60 * 1000 ,
path : '/auth/refresh' ,
domain : '.example.com' ,
});
res.json ({ success : true });
});
fetch ('https://api.example.com/user/profile' , {
credentials : 'include' ,
});
❌ WRONG: localStorage/sessionStorage
localStorage .setItem ('access_token' , token);
sessionStorage .setItem ('access_token' , token);
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 {
const decoded = jwt.verify (oldRefreshToken, publicKey, {
algorithms : ['RS256' ],
issuer : 'https://auth.example.com' ,
});
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' );
}
if (tokenRecord.isUsed ) {
await db.refreshTokens .deleteMany ({ userId : decoded.sub });
await logSecurityEvent ('REFRESH_TOKEN_REUSE_DETECTED' , {
userId : decoded.sub ,
tokenId : decoded.jti ,
ip : req.ip ,
userAgent : req.headers ['user-agent' ],
});
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' ,
});
}
await db.refreshTokens .updateOne (
{ tokenHash },
{
$set : { isUsed : true , lastUsedAt : new Date () },
}
);
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 ,
jti : crypto.randomUUID (),
},
privateKey,
{ algorithm : 'RS256' }
);
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 ,
});
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) {
res.clearCookie ('refresh_token' );
res.status (401 ).json ({ error : 'invalid_token' });
}
});
Database Schema (Refresh Tokens):
{
userId : 'user_12345' ,
tokenHash : 'sha256_hash_of_refresh_token' ,
isUsed : false ,
expiresAt : ISODate ('2026-02-01T00:00:00Z' ),
createdAt : ISODate ('2026-01-25T00:00:00Z' ),
lastUsedAt : null ,
userAgent : 'Mozilla/5.0...' ,
ipAddress : '192.168.1.1' ,
jti : 'uuid-v4' ,
}
Password Hashing (2026 Best Practices)
Winner of Password Hashing Competition (2015)
Resistant to both GPU cracking and side-channel attacks
Configurable memory, time, and parallelism parameters
import argon2 from 'argon2' ;
const hash = await argon2.hash (password, {
type : argon2.argon2id ,
memoryCost : 19456 ,
timeCost : 2 ,
parallelism : 1 ,
});
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)
import bcrypt from 'bcryptjs' ;
const hash = await bcrypt.hash (password, 14 );
const isValid = await bcrypt.compare (password, hash);
MD5, SHA-1, SHA-256 alone (not designed for passwords)
Plain text storage
Reversible encryption
Multi-Factor Authentication (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
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
Phishing-resistant (cryptographic binding to origin)
No shared secrets to leak or intercept
Passwordless authentication
Synced across devices (Apple, Google, Microsoft ecosystems)
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
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
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
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:
Related Skills
security-architect - Threat modeling (STRIDE), OWASP Top 10, and security architecture patterns
Memory Protocol (MANDATORY) cat .claude/context/memory/learnings.md
After completing: Record any new patterns or exceptions discovered.
ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.