agentleFS
medy-gribkovmedy-gribkov/arcana/skills/oauth-auth/SKILL.md

oauth-auth

Skill1 starsChanged 8 months agoApache-2.0View on GitHub

Implements secure sign-in with OAuth2 authorization code flow with PKCE, OIDC ID token checks, JWT signing and validation, httpOnly cookie sessions with CSRF protection, refresh token rotation with reuse detection, password hashing, and auth middleware. Use when adding login or SSO, reviewing token or session handling, choosing where to store tokens, or debugging an OAuth callback, JWT or refresh failure.

  • Reads credentials
  • Sends data out

We split this skill into the rules it gives an agent

1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Run workflow: detect auth pattern needed, choose flow (OAuth2 code + PKCE for third-party, session cookies for first-party), implement …

    line 427

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

OAuth and Authentication

Implement secure OAuth2 flows, JWT validation, session management, and authentication patterns.

For Auth.js (NextAuth) with Google and refresh handling, see references/authjs.md.

OAuth2 Authorization Code Flow with PKCE

BAD: No PKCE, state stored in localStorage

// Client initiates OAuth without PKCE
const authUrl = `https://auth.example.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code`;
localStorage.setItem('oauth_state', Math.random().toString());
window.location.href = authUrl;

GOOD: PKCE with httpOnly cookie state

import crypto from 'crypto';

// Generate PKCE challenge
function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString('base64url');
  const challenge = crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');
  return { verifier, challenge };
}

// Initiate OAuth with PKCE
export async function initiateOAuth(res: Response) {
  const { verifier, challenge } = generatePKCE();
  const state = crypto.randomBytes(32).toString('base64url');

  // Store verifier and state in httpOnly cookie
  res.setHeader('Set-Cookie', [
    `pkce_verifier=${verifier}; HttpOnly; Secure; SameSite=Lax; Max-Age=600`,
    `oauth_state=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600`
  ]);

  const params = new URLSearchParams({
    client_id: process.env.OAUTH_CLIENT_ID!,
    redirect_uri: process.env.OAUTH_REDIRECT_URI!,
    response_type: 'code',
    scope: 'openid profile email',
    state,
    code_challenge: challenge,
    code_challenge_method: 'S256'
  });

  return `https://auth.example.com/oauth/authorize?${params}`;
}

Token Exchange and Validation

BAD: No validation, symmetric JWT with weak secret

// Exchange code without validating state
const response = await fetch('https://auth.example.com/oauth/token', {
  method: 'POST',
  body: JSON.stringify({ code, client_id: clientId, client_secret: secret })
});
const { access_token } = await response.json();

// Weak symmetric JWT
import jwt from 'jsonwebtoken';
const token = jwt.sign({ userId }, 'weak-secret', { expiresIn: '1h' });

GOOD: State validation, PKCE verification, asymmetric JWT

import { SignJWT, jwtVerify, importSPKI, importPKCS8 } from 'jose';

export async function handleOAuthCallback(
  code: string,
  state: string,
  cookies: Record<string, string>
) {
  // Validate state to prevent CSRF
  if (state !== cookies.oauth_state) {
    throw new Error('Invalid state parameter');
  }

  // Exchange code with PKCE verifier
  const response = await fetch('https://auth.example.com/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: process.env.OAUTH_REDIRECT_URI!,
      client_id: process.env.OAUTH_CLIENT_ID!,
      code_verifier: cookies.pkce_verifier
    })
  });

  if (!response.ok) {
    throw new Error('Token exchange failed');
  }

  const tokens = await response.json();
  return tokens;
}

// Create asymmetric JWT (RS256)
export async function createAccessToken(payload: Record<string, any>) {
  const privateKey = await importPKCS8(
    process.env.JWT_PRIVATE_KEY!,
    'RS256'
  );

  return await new SignJWT(payload)
    .setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
    .setIssuedAt()
    .setIssuer(process.env.JWT_ISSUER!)
    .setAudience(process.env.JWT_AUDIENCE!)
    .setExpirationTime('15m')
    .sign(privateKey);
}

// Verify JWT with public key
export async function verifyAccessToken(token: string) {
  const publicKey = await importSPKI(
    process.env.JWT_PUBLIC_KEY!,
    'RS256'
  );

  const { payload } = await jwtVerify(token, publicKey, {
    issuer: process.env.JWT_ISSUER!,
    audience: process.env.JWT_AUDIENCE!
  });

  return payload;
}

Refresh Token Rotation with Reuse Detection

BAD: No rotation, long-lived refresh tokens

// Single refresh token, no rotation
const refreshToken = crypto.randomBytes(32).toString('hex');
await db.saveRefreshToken(userId, refreshToken, { expiresIn: '30d' });

GOOD: Rotation with family tracking and reuse detection

interface RefreshToken {
  tokenHash: string;
  userId: string;
  family: string;
  expiresAt: Date;
  revokedAt?: Date;
  replacedBy?: string;
}

export async function createRefreshToken(userId: string, family?: string) {
  const token = crypto.randomBytes(32).toString('base64url');
  const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
  const tokenFamily = family || crypto.randomBytes(16).toString('hex');

  await db.refreshTokens.create({
    tokenHash,
    userId,
    family: tokenFamily,
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
  });

  return { token, family: tokenFamily };
}

export async function rotateRefreshToken(token: string) {
  const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
  const existing = await db.refreshTokens.findOne({ tokenHash });

  if (!existing || existing.expiresAt < new Date()) {
    throw new Error('Invalid or expired refresh token');
  }

  // Reuse detection: if token already replaced, revoke entire family
  if (existing.revokedAt || existing.replacedBy) {
    await db.refreshTokens.updateMany(
      { family: existing.family },
      { revokedAt: new Date() }
    );
    throw new Error('Token reuse detected, family revoked');
  }

  // Create new token in same family
  const { token: newToken, family } = await createRefreshToken(
    existing.userId,
    existing.family
  );

  // Mark old token as replaced
  await db.refreshTokens.updateOne(
    { tokenHash },
    {
      revokedAt: new Date(),
      replacedBy: crypto.createHash('sha256').update(newToken).digest('hex')
    }
  );

  return { accessToken: await createAccessToken({ userId: existing.userId }), refreshToken: newToken };
}

Session Management

BAD: Tokens in localStorage, no CSRF protection

// Client stores tokens in localStorage
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);

// No CSRF protection
fetch('/api/sensitive', {
  headers: { Authorization: `Bearer ${localStorage.getItem('access_token')}` }
});

GOOD: httpOnly cookies with CSRF tokens

import { serialize } from 'cookie';

export function setAuthCookies(
  res: Response,
  accessToken: string,
  refreshToken: string
) {
  const csrfToken = crypto.randomBytes(32).toString('base64url');

  res.setHeader('Set-Cookie', [
    serialize('access_token', accessToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: 15 * 60, // 15 minutes
      path: '/'
    }),
    serialize('refresh_token', refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: 7 * 24 * 60 * 60, // 7 days
      path: '/api/auth/refresh'
    }),
    serialize('csrf_token', csrfToken, {
      httpOnly: false, // Accessible to JS for request headers
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: 15 * 60,
      path: '/'
    })
  ]);

  return csrfToken;
}

// Middleware validates CSRF token
export function validateCSRF(req: Request) {
  const cookieCSRF = req.cookies.csrf_token;
  const headerCSRF = req.headers.get('X-CSRF-Token');

  if (!cookieCSRF || cookieCSRF !== headerCSRF) {
    throw new Error('CSRF validation failed');
  }
}

Password Hashing with bcrypt

BAD: Plain text or weak hashing

// Plain text password
await db.users.create({ email, password });

// Weak hash
import crypto from 'crypto';
const hash = crypto.createHash('md5').update(password).digest('hex');

GOOD: bcrypt with salt rounds

import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12;

export async function hashPassword(password: string): Promise<string> {
  return await bcrypt.hash(password, SALT_ROUNDS);
}

export async function verifyPassword(
  password: string,
  hash: string
): Promise<boolean> {
  return await bcrypt.compare(password, hash);
}

// Usage in registration
export async function registerUser(email: string, password: string) {
  const existingUser = await db.users.findOne({ email });
  if (existingUser) {
    throw new Error('User already exists');
  }

  const passwordHash = await hashPassword(password);
  return await db.users.create({ email, passwordHash });
}

Authentication Middleware

import { NextRequest, NextResponse } from 'next/server';

export async function authMiddleware(req: NextRequest) {
  const token = req.cookies.get('access_token')?.value;

  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url));
  }

  try {
    const payload = await verifyAccessToken(token);

    // Attach user to request headers for route handlers
    const requestHeaders = new Headers(req.headers);
    requestHeaders.set('X-User-Id', payload.userId as string);

    return NextResponse.next({
      request: { headers: requestHeaders }
    });
  } catch (error) {
    // Token invalid or expired. The refresh cookie is scoped to
    // /api/auth/refresh, so the browser does not send it here. Hand off to
    // that route: it rotates the token, sets the cookies, and redirects back.
    const refreshUrl = new URL('/api/auth/refresh', req.url);
    refreshUrl.searchParams.set('next', req.nextUrl.pathname);
    return NextResponse.redirect(refreshUrl);
  }
}

// middleware.ts (named proxy.ts, exporting proxy, from Next.js 16)
export { authMiddleware as middleware };

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*']
};

Token Revocation

// Revoke all tokens for a user (on password change, logout all devices)
export async function revokeAllUserTokens(userId: string) {
  await db.refreshTokens.updateMany(
    { userId, revokedAt: null },
    { revokedAt: new Date() }
  );
}

// Revoke specific token
export async function revokeRefreshToken(token: string) {
  const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
  await db.refreshTokens.updateOne(
    { tokenHash },
    { revokedAt: new Date() }
  );
}

// Cleanup expired tokens (run daily)
export async function cleanupExpiredTokens() {
  const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days
  await db.refreshTokens.deleteMany({
    expiresAt: { $lt: cutoff }
  });
}

OIDC ID Token Validation

import { jwtVerify, createRemoteJWKSet } from 'jose';

export async function validateIDToken(
  idToken: string,
  clientId: string,
  issuer: string,
  expectedNonce: string
) {
  // Read jwks_uri from `${issuer}/.well-known/openid-configuration` when the
  // provider does not publish /.well-known/jwks.json.
  const JWKS = createRemoteJWKSet(
    new URL(`${issuer}/.well-known/jwks.json`)
  );

  const { payload } = await jwtVerify(idToken, JWKS, {
    issuer,
    audience: clientId,
    maxTokenAge: '10m'
  });

  // Compare the nonce with the one stored when the login started.
  // Reject the token if it is missing or different.
  if (payload.nonce !== expectedNonce) {
    throw new Error('Invalid nonce');
  }

  return {
    sub: payload.sub,
    email: payload.email as string,
    email_verified: payload.email_verified as boolean,
    name: payload.name as string
  };
}

Run workflow: detect auth pattern needed, choose flow (OAuth2 code + PKCE for third-party, session cookies for first-party), implement token storage with httpOnly cookies, add CSRF protection, implement refresh rotation with reuse detection, add middleware guards, validate all tokens with asymmetric keys.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.