git clone https://github.com/vibeforge1111/vibeship-spawner-skills
security/authentication-oauth/skill.yamlAuthentication & OAuth Skill
Patterns for implementing secure authentication with OAuth, JWT, and sessions
id: authentication-oauth name: Authentication & OAuth version: 1.0.0 layer: 2 # Infrastructure skill
description: | Expert guidance on authentication implementation including OAuth 2.0/OIDC, JWT tokens, session management, and secure password handling. Covers both implementing auth from scratch and integrating auth providers.
owns:
- OAuth 2.0 / OpenID Connect flows
- JWT token generation and validation
- Session management strategies
- Password hashing and storage
- Multi-factor authentication
- Token refresh flows
- Social login integration
- Auth middleware design
does_not_own:
- Authorization/permissions → authorization skill
- API rate limiting → rate-limiting skill
- Encryption at rest → security-hardening skill
- Network security → infrastructure-as-code
- Audit logging → logging-strategies skill
triggers:
- "implement authentication"
- "oauth login"
- "jwt tokens"
- "session management"
- "social login"
- "password reset"
- "multi-factor auth"
- "refresh tokens"
- Working with Auth0, Clerk, NextAuth, Passport.js
pairs_with:
- security-hardening
- backend
- database-schema-design
- frontend
requires:
- HTTPS for all auth endpoints
- Secure password storage (never plaintext)
- Understanding of token security
tags:
- authentication
- oauth
- jwt
- session
- security
- login
- password
- mfa
- oidc
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.
============================================================================
PATTERNS
============================================================================
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
============================================================================
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
============================================================================
PROVIDER COMPARISON
============================================================================
provider_comparison:
-
name: Auth0 type: Managed pricing: "Free tier, then per-user" best_for: "Enterprise, complex requirements" features:
- Universal Login
- MFA
- Social connections
- Enterprise SSO
-
name: Clerk type: Managed pricing: "Free tier, then per-user" best_for: "Modern apps, great DX" features:
- Drop-in components
- Multi-factor
- Organizations
- Webhooks
-
name: Supabase Auth type: Self-hosted/Managed pricing: "Included with Supabase" best_for: "Supabase users, simple needs" features:
- Email/password
- Social OAuth
- Magic links
- Row-level security
-
name: Firebase Auth type: Managed pricing: "Free, paid for phone auth" best_for: "Mobile apps, Firebase ecosystem" features:
- Social login
- Phone auth
- Anonymous auth
- Custom tokens
-
name: NextAuth.js / Auth.js type: Library pricing: "Free (self-hosted)" best_for: "Next.js apps, full control" features:
- Many providers
- Database adapters
- JWT or database sessions
- Fully customizable
============================================================================
HANDOFFS
============================================================================
handoffs: receives_from: - skill: backend context: "Backend needs user authentication" receives: - "User model requirements" - "Protected routes" - "API security needs" provides: "Complete authentication implementation"
- skill: frontend context: "Frontend needs login/auth UI" receives: - "User flow requirements" - "Social login needs" - "Session handling" provides: "Auth integration and token handling"
hands_to: - skill: security-hardening trigger: "Need additional security measures" provides: - "Current auth implementation" - "Security requirements" receives: "Security hardening recommendations"
- skill: database-schema-design trigger: "Need to design user/session tables" provides: - "User data requirements" - "Session storage needs" - "OAuth token storage" receives: "Database schema for auth" - skill: logging-strategies trigger: "Need audit logging for auth events" provides: - "Auth event types" - "Sensitive data to redact" receives: "Audit logging implementation"