mcpField
A
auth-security

auth-security

github.com/majiayu000Security288113 installsUpdated today
Repository

About auth-security

auth-security is a security skill outlining OAuth 2.1 and JWT implementation practices for developers. It covers token handling and defenses against common web authentication attacks.

Key features

  1. 01OAuth 2.1 with required PKCE
  2. 02JWT algorithm selection and verification
  3. 03Secure token storage guidance
  4. 04Refresh token rotation
  5. 05CSRF and XSS prevention
  6. 06Token revocation and DPoP guidance

Use cases

  1. 01Designing an OAuth 2.1 sign-in flow
  2. 02Reviewing JWT signing and verification
  3. 03Choosing browser token storage
  4. 04Adding token rotation and revocation

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/majiayu000/spellbook --skill auth-security

After installing, type/auth-securityin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

reference/
attacks.md
jwt.md
oauth2.1.md
templates/typescript/
auth.service.ts

Details

From the project README

Auth Security

Core Principles

  • OAuth 2.1 — Follow RFC 9700 (January 2025)
  • PKCE Required — All clients must use PKCE
  • Short-lived Tokens — Access tokens expire in 5-15 minutes
  • Token Rotation — Refresh tokens are single-use
  • HttpOnly Storage — Browser tokens in HttpOnly cookies
  • Explicit Algorithm — Never trust JWT header algorithm
  • No backwards compatibility — Delete deprecated auth flows

OAuth 2.1 Key Changes

Deprecated Flows (DO NOT USE)
FlowStatusReplacement
Implicit GrantRemovedAuthorization Code + PKCE
Password GrantRemovedAuthorization Code + PKCE
Auth Code without PKCERemovedMust use PKCE
Required: Authorization Code + PKCE
import crypto from 'crypto';

// 1. Generate code verifier (43-128 chars)
function generateCodeVerifier(): string {
  return crypto.randomBytes(32).toString('base64url');
}

// 2. Generate code challenge
function generateCodeChallenge(verifier: string): string {
  return crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');
}

// 3. Authorization request
const verifier = generateCodeVerifier();
const challenge = generateCodeChallenge(verifier);

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('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', generateState());

// 4. Token exchange (after redirect)
const tokenResponse = 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: authorizationCode,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    code_verifier: verifier, // Prove we initiated the request
  }),
});

JWT Best Practices

Algorithm Selection (2025)
PriorityAlgorithmNotes
1EdDSA (Ed25519)Most secure, quantum-resistant properties
2ES256 (ECDSA P-256)Widely supported, compact signatures
3PS256 (RSA-PSS)More secure than RS256
4RS256 (RSA PKCS#1)Best compatibility
// Recommended: ES256
import { SignJWT, jwtVerify } from 'jose';

const privateKey = await importPKCS8(PRIVATE_KEY_PEM, 'ES256');
const publicKey = await importSPKI(PUBLIC_KEY_PEM, 'ES256');

// Sign
const token = await new SignJWT({ sub: userId, scope: 'read write' })
  .setProtectedHeader({ alg: 'ES256', typ: 'JWT', kid: keyId })
  .setIssuer('https://auth.example.com')
  .setAudience('https://api.example.com')
  .setExpirationTime('15m')
  .setIssuedAt()
  .setJti(crypto.randomUUID())
  .sign(privateKey);
Token Structure
interface AccessTokenPayload {
  // Standard claims
  iss: string;  // Issuer
  sub: string;  // Subject (user ID)
  aud: string;  // Audience
  exp: number;  // Expiration (Unix timestamp)
  iat: number;  // Issued at
  jti: string;  // JWT ID (unique identifier)

  // Custom claims
  scope: string;      // Permissions
  email?: string;     // User email
  roles?: string[];   // User roles
}
Verification (Critical)
import { jwtVerify, errors } from 'jose';

async function verifyAccessToken(token: string): Promise<AccessTokenPayload> {
  try {
    const { payload } = await jwtVerify(token, publicKey, {
      // CRITICAL: Explicitly specify allowed algorithms
      algorithms: ['ES256'],

      // Validate standard claims
      issuer: 'https://auth.example.com',
      audience: 'https://api.example.com',

      // Clock tolerance for sync issues
      clockTolerance: 30,
    });

    // Additional validation
    if (!payload.scope?.includes('read')) {
      throw new Error('Insufficient scope');
    }

    return payload as AccessTokenPayload;
  } catch (err) {
    if (err instanceof errors.JWTExpired) {
      throw new AuthError('Token expired', 'TOKEN_EXPIRED');
    }
    if (err instanceof errors.JWTClaimValidationFailed) {
      throw new AuthError('Invalid token claims', 'INVALID_CLAIMS');
    }
    throw new AuthError('Invalid token', 'INVALID_TOKEN');
  }
}

Token Storage

Web Applications
// Set token in HttpOnly cookie (server-side)
function setAuthCookie(res: Response, token: string) {
  res.cookie('access_token', token, {
    httpOnly: true,     // Not accessible via JavaScript
    secure: true,       // HTTPS only
    sameSite: 'strict', // CSRF protection
    maxAge: 15 * 60 * 1000, // 15 minutes
    path: '/api',       // Only sent to API routes
  });
}

// Refresh token (longer-lived)
function setRefreshCookie(res: Response, token: string) {
  res.cookie('refresh_token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
    path: '/api/auth/refresh',  // Only for refresh endpoint
  });
}
Single Page Applications (SPA)
// Store in memory (NOT localStorage/sessionStorage)
class TokenManager {
  private accessToken: string | null = null;

  setToken(token: string) {
    this.accessToken = token;
  }

  getToken(): string | null {
    return this.accessToken;
  }

  clearToken() {
    this.accessToken = null;
  }
}

// Use with Refresh Token Rotation
// Refresh token in HttpOnly cookie
// Access token in memory
Storage Comparison
StorageXSS SafeCSRF SafePersistence
HttpOnly CookieYesNeeds SameSiteYes
MemoryYesYesNo (lost on reload)
localStorageNoYesYes
sessionStorageNoYesTab only

Refresh Token Rotation

Flow
1. Client sends refresh_token
2. Server validates refresh_token
3. Server generates NEW access_token + NEW refresh_token
4. Server INVALIDATES old refresh_token
5. Server returns new tokens
6. Client stores new tokens
Implementation
async function refreshTokens(refreshToken: string) {
  // Find token in database
  const stored = await db.refreshToken.findUnique({
    where: { token: hashToken(refreshToken) },
    include: { user: true },
  });

  if (!stored) {
    throw new AuthError('Invalid refresh token', 'INVALID_TOKEN');
  }

  // Check if already used (reuse detection)
  if (stored.usedAt) {
    // Potential token theft - revoke ALL user tokens
    await db.refreshToken.deleteMany({
      where: { userId: stored.userId },
    });

    // Alert security team
    await alertSecurityTeam({
      event: 'REFRESH_TOKEN_REUSE',
      userId: stored.userId,
      tokenId: stored.id,
    });

    throw new AuthError('Token reuse detected', 'TOKEN_REUSE');
  }

  // Check expiration
  if (stored.expiresAt < new Date()) {
    throw new AuthError('Refresh token expired', 'TOKEN_EXPIRED');
  }

  // Mark as used (but keep for reuse detection)
  await db.refreshToken.update({
    where: { id: stored.id },
    data: { usedAt: new Date() },
  });

  // Generate new tokens
  const newAccessToken = await generateAccessToken(stored.user);
  const newRefreshToken = await generateRefreshToken(stored.user);

  // Store new refresh token
  await db.refreshToken.create({
    data: {
      token: hashToken(newRefreshToken),
      userId: stored.userId,
      expiresAt: addDays(new Date(), 7),
      previousTokenId: stored.id, // Chain for audit
    },
  });

  return {
    accessToken: newAccessToken,
    refreshToken: newRefreshToken,
  };
}

Attack Prevention

Algorithm Confusion
// WRONG: Trusts header algorithm
jwt.verify(token, key); // Uses alg from header

// CORRECT: Explicit algorithm
jwt.verify(token, key, { algorithms: ['ES256'] });
CSRF Protection
// Use SameSite cookies
res.cookie('session', token, {
  sameSite: 'strict', // or 'lax' for cross-site links
});

// Or double-submit cookie pattern
const csrfToken = crypto.randomBytes(32).toString('hex');
res.cookie('csrf', csrfToken, { httpOnly: false });
// Client sends csrf token in header
XSS Protection
// Content Security Policy
res.setHeader('Content-Security-Policy', [
  "default-src 'self'",
  "script-src 'self'",
  "style-src 'self' 'unsafe-inline'",
].join('; '));

// Use HttpOnly cookies for tokens
// Never store tokens in localStorage
Token Binding (DPoP)
// Demonstration of Proof of Possession
// Bind token to client's key pair

const dpopProof = await new SignJWT({
  htm: 'POST',
  htu: 'https://api.example.com/resource',
  ath: await hashAccessToken(accessToken), // Access token hash
})
  .setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicKey })
  .setJti(crypto.randomUUID())
  .setIssuedAt()
  .sign(privateKey);

// Send with request
fetch('https://api.example.com/resource', {
  headers: {
    Authorization: `DPoP ${accessToken}`,
    DPoP: dpopProof,
  },
});

Token Revocation

// Revoke all user tokens (e.g., password change, logout all)
async function revokeAllUserTokens(userId: string) {
  await db.refreshToken.deleteMany({
    where: { userId },
  });

  // If using token blacklist for access tokens
  await redis.sadd(`revoked:${userId}`, Date.now());
  await redis.expire(`revoked:${userId}`, 15 * 60); // 15 min (access token lifetime)
}

// Check blacklist during verification
async function isTokenRevoked(userId: string, iat: number): Promise<boolean> {
  const revokedAt = await redis.get(`revoked:${userId}`);
  return revokedAt && parseInt(revokedAt) > iat * 1000;
}

Checklist

## OAuth 2.1
- [ ] Using Authorization Code flow
- [ ] PKCE enabled for all clients
- [ ] No implicit or password grants
- [ ] Redirect URI exact matching

## JWT
- [ ] Using ES256 or EdDSA algorithm
- [ ] Explicit algorithm verification
- [ ] Short expiration (≤15 min)
- [ ] Unique jti for each token
- [ ] Issuer and audience validation

## Tokens
- [ ] HttpOnly cookies for web apps
- [ ] Refresh token rotation enabled
- [ ] Reuse detection implemented
- [ ] Token revocation mechanism

## Security
- [ ] HTTPS everywhere
- [ ] SameSite cookies
- [ ] CSP headers configured
- [ ] Rate limiting on auth endpoints
- [ ] Brute force protection

See Also

Comments

Markdown supported