Vibeship-spawner-skills authentication-oauth

Authentication & OAuth Skill

install
source · Clone the upstream repo
git clone https://github.com/vibeforge1111/vibeship-spawner-skills
manifest: security/authentication-oauth/skill.yaml
source content

Authentication & 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"