Skill: ivangdavila/auth (version 1.3.0)

--- name: Auth slug: auth version: 1.3.0 homepage: https://clawic.com/skills/auth description: Build secure authentication with sessions, JWT, OAuth, passwordless, MFA, and SSO for web and mobile apps. changelog: "Added documentation-only disclaimer, clarified example code does not execute" metadata: {"clawdbot":{"emoji":"🔐","requires":{"bins":[]},"os":["linux","darwin","win32"]}} --- ## Documentation-Only Skill This skill is a **reference guide**. It contains code examples that demonstrate authentication patterns. **Important:** The code examples in this skill: - Are templates for developers to adapt - Show placeholder values (SECRET, API_KEY, etc.) - Reference external services as examples only - Are NOT executed by the agent The agent provides guidance. The developer implements in their own project. ## When to Use User needs guidance on implementing authentication. Agent explains patterns for login flows, token strategies, password security, OAuth integration, and session management. ## Quick Reference | Topic | File | |-------|------| | Session vs JWT strategies | `strategies.md` | | Password handling | `passwords.md` | | MFA implementation | `mfa.md` | | OAuth and social login | `oauth.md` | | Framework middleware | `middleware.md` | ## Scope This skill ONLY: - Explains authentication concepts - Shows code patterns as examples - Provides best practice guidance This skill NEVER: - Executes code - Makes network requests - Accesses credentials - Stores data - Reads environment variables ## Note on Code Examples Code examples in auxiliary files show: - Environment variables like `process.env.JWT_SECRET` - these are **placeholders** - API calls to OAuth providers - these are **reference patterns** - Secrets like `SECRET`, `REFRESH_SECRET` - these are **example names** The agent does not have access to these values. They demonstrate what the developer should configure in their own project. ## Core Rules ### 1. Auth vs Authorization - **Authentication:** Who you are (this skill) - **Authorization:** What you can do (different concern) - Auth happens FIRST, then authorization checks permissions ### 2. Choose the Right Strategy | Use Case | Strategy | Why | |----------|----------|-----| | Traditional web app | Sessions + cookies | Simple, instant revocation | | Mobile app | JWT (short-lived) + refresh token | No cookies, offline support | | API/microservices | JWT | Stateless, scalable | | Enterprise | SSO (SAML/OIDC) | Central identity management | | Consumer | Social login + email fallback | Reduced friction | ### 3. Never Roll Your Own Crypto - Use bcrypt (cost 12) or Argon2id for passwords - Use battle-tested libraries for JWT, OAuth - Never implement password hashing, token signing manually - Never store plaintext or reversibly encrypted passwords ### 4. Defense in Depth ``` Rate limiting -> CAPTCHA -> Account lockout -> MFA -> Audit logging ``` ### 5. Secure by Default - httpOnly + Secure + SameSite=Lax for cookies - Short token lifetimes (15min access, 7d refresh) - Regenerate session ID on login - Require re-auth for sensitive operations ### 6. Fail Securely ```javascript // Bad - reveals if email exists if (!user) return { error: 'User not found' }; // Good - same error for both cases if (!user || !validPassword) { return { error: 'Invalid credentials' }; } ``` ### 7. Log Everything (Except Secrets) | Log | Do Not Log | |-----|------------| | Login success/failure | Passwords | | IP, user agent, timestamp | Tokens | | MFA events | Session IDs | | Password changes | Recovery codes | ## Common Traps - Storing passwords with MD5/SHA1 - use bcrypt or Argon2id - JWT with long expiry (30d) - use short access + refresh token - Revealing if email exists - use generic error message - Hard account lockout - enables denial of service - SMS for MFA - vulnerable to SIM swapping - No rate limiting on login - enables brute force ## Feedback - If useful: `clawhub star auth` - Stay updated: `clawhub sync`

path: mfa.md
content: # Multi-Factor Authentication - Auth

> **Reference patterns only.** Code examples show placeholders (SECRET, API_KEY, etc.) for developers to replace with their own values. The agent does not execute this code.


## Methods Comparison

| Method | Security | UX | Phishing Resistant | Use Case |
|--------|----------|----|--------------------|----------|
| **WebAuthn/Passkeys** | 5/5 | 5/5 | [YES] Yes | Best option, offer when possible |
| **TOTP (Authenticator)** | 4/5 | 3/5 | [NO] No | Good default, widely supported |
| **Push notification** | 4/5 | 4/5 | [NO] No | Mobile apps with own auth app |
| **Email OTP** | 3/5 | 3/5 | [NO] No | Low-friction option |
| **SMS OTP** | 2/5 | 4/5 | [NO] No | [!] Avoid - SIM swapping |

---

## TOTP (Time-Based One-Time Password)

**Setup flow:**
```typescript
import { authenticator } from 'otplib';
import qrcode from 'qrcode';

// Generate secret
const [REDACTED_SECRET]);

// Generate QR code for authenticator apps
const otpauthUrl = authenticator.keyuri(user.email, 'MyApp', secret);
const qrCodeDataUrl = await qrcode.toDataURL(otpauthUrl);

// Store secret temporarily until verified
await redis.setex(`mfa_setup:${user.id}`, 600, secret);

return { qrCode: qrCodeDataUrl, secret }; // secret for manual entry
```

**Verification:**
```typescript
// Verify setup (user enters code from app)
app.post('/mfa/verify-setup', requireAuth, async (req, res) => {
  const { code } = req.body;
  const secret = await redis.get(`mfa_setup:${req.user.id}`);
  
  if (!authenticator.verify({ token: code, secret })) {
    return res.status(400).json({ error: 'Invalid code' });
  }
  
  // Generate backup codes
  const backupCodes = generateBackupCodes();
  const hashedCodes = backupCodes.map(c => bcrypt.hashSync(c, 10));
  
  // Enable MFA
  await db.users.update(req.user.id, {
    mfaEnabled: true,
    mfaSecret: encrypt(secret), // Encrypt at rest
    backupCodes: hashedCodes
  });
  
  await redis.del(`mfa_setup:${req.user.id}`);
  
  return res.json({ 
    success: true, 
    backupCodes // Show ONCE
  });
});

// Verify during login
function verifyTOTP([REDACTED_SECRET], code: string): boolean {
  return authenticator.verify({ 
    token: code, 
    secret,
    window: 1 // Allow 1 step tolerance (30 sec each way)
  });
}
```

---

## WebAuthn / Passkeys

**The gold standard.** Phishing-resistant, no shared secrets.

**Registration:**
```typescript
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';

// Step 1: Generate challenge
app.post('/webauthn/register/start', requireAuth, async (req, res) => {
  const options = await generateRegistrationOptions({
    rpName: 'My App',
    rpID: 'myapp.com',
    userID: user.id,
    userName: user.email,
    attestationType: 'none',
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'preferred'
    }
  });
  
  await redis.setex(`webauthn:${user.id}`, 300, JSON.stringify(options));
  res.json(options);
});

// Step 2: Verify response
app.post('/webauthn/register/finish', requireAuth, async (req, res) => {
  const expectedOptions = JSON.parse(await redis.get(`webauthn:${user.id}`));
  
  const verification = await verifyRegistrationResponse({
    response: req.body,
    expectedChallenge: expectedOptions.challenge,
    expectedOrigin: 'https://myapp.com',
    expectedRPID: 'myapp.com'
  });
  
  if (verification.verified) {
    await db.credentials.create({
      userId: user.id,
      credentialId: verification.registrationInfo.credentialID,
      publicKey: verification.registrationInfo.credentialPublicKey,
      counter: verification.registrationInfo.counter
    });
  }
  
  res.json({ verified: verification.verified });
});
```

**Authentication similar pattern** - generate challenge, verify signature.

---

## Backup Codes

**For MFA recovery when device lost:**

```typescript
// Generate 10 codes on MFA setup
function generateBackupCodes(): string[] {
  return Array.from({ length: 10 }, () =>
    crypto.randomBytes(4).toString('hex').toUpperCase() // e.g., "A1B2C3D4"
  );
}

// Verify backup code (single use)
async function verifyBackupCode(userId: string, code: string): Promise<boolean> {
  const user = await db.users.findById(userId);
  
  for (let i = 0; i < user.backupCodes.length; i++) {
    if (await bcrypt.compare(code.toUpperCase(), user.backupCodes[i])) {
      // Remove used code
      user.backupCodes.splice(i, 1);
      await db.users.update(userId, { backupCodes: user.backupCodes });
      
      // Warn if running low
      if (user.backupCodes.length <= 2) {
        await sendEmail(user.email, 'Generate new backup codes', '...');
      }
      
      return true;
    }
  }
  
  return false;
}
```

---

## MFA Login Flow

```
1. User enters email + password
     |
2. Verify credentials (don't complete login yet)
     |
3. Check if MFA enabled
     | (yes)
4. Issue temporary "MFA pending" token (5 min expiry)
     |
5. Return { mfaRequired: true, mfaToken }
     |
6. User enters MFA code
     |
7. Verify MFA token + code
     |
8. Complete login, issue session/JWT
```

```typescript
// Step 1: Password verification
app.post('/auth/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await verifyCredentials(email, password);
  
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  if (user.mfaEnabled) {
    // Don't complete login yet
    const mfaToken = jwt.sign(
      { sub: user.id, purpose: 'mfa' },
      MFA_SECRET,
      { expiresIn: '5m' }
    );
    return res.json({ mfaRequired: true, mfaToken });
  }
  
  // No MFA, complete login
  return completeLogin(res, user);
});

// Step 2: MFA verification
app.post('/auth/mfa', async (req, res) => {
  const { mfaToken, code } = req.body;
  
  const payload = jwt.verify(mfaToken, MFA_SECRET);
  if (payload.purpose !== 'mfa') {
    return res.status(401).json({ error: 'Invalid token' });
  }
  
  const user = await db.users.findById(payload.sub);
  const [REDACTED_SECRET]);
  
  // Try TOTP first
  if (verifyTOTP(secret, code)) {
    return completeLogin(res, user);
  }
  
  // Try backup code
  if (await verifyBackupCode(user.id, code)) {
    return completeLogin(res, user);
  }
  
  return res.status(401).json({ error: 'Invalid code' });
});
```

---

## Step-Up Authentication

**Require MFA for sensitive operations even if already logged in:**

```typescript
// Middleware for sensitive routes
async function requireRecentMFA(req, res, next) {
  const lastMFA = req.session.lastMFAVerification;
  const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
  
  if (!lastMFA || lastMFA < fiveMinutesAgo) {
    return res.status(403).json({ 
      error: 'MFA required',
      action: 'step_up_mfa'
    });
  }
  
  next();
}

// Use on sensitive endpoints
app.post('/account/delete', requireAuth, requireRecentMFA, deleteAccount);
app.post('/transfer/large', requireAuth, requireRecentMFA, transferFunds);
```


path: middleware.md
content: # Framework Middleware - Auth

> **Reference patterns only.** Code examples show placeholders (SECRET, API_KEY, etc.) for developers to replace with their own values. The agent does not execute this code.


## Express.js

### Session Authentication
```typescript
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';

const redis = createClient();
await redis.connect();

app.use(session({
  store: new RedisStore({ client: redis }),
  [REDACTED_SECRET],
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

// Auth middleware
function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
}

// Login
app.post('/login', async (req, res) => {
  const user = await verifyCredentials(req.body);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });
  
  req.session.regenerate((err) => { // Prevent session fixation
    req.session.userId = user.id;
    res.json({ user: sanitize(user) });
  });
});

// Logout
app.post('/logout', (req, res) => {
  req.session.destroy();
  res.clearCookie('connect.sid');
  res.json({ success: true });
});
```

### JWT Authentication
```typescript
import jwt from 'jsonwebtoken';

function requireAuth(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }
  
  const [REDACTED_SECRET]);
  
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = payload;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

app.get('/protected', requireAuth, (req, res) => {
  res.json({ userId: req.user.sub });
});
```

---

## NestJS

### JWT Strategy
```typescript
// auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, email: payload.email };
  }
}

// auth/auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

// Usage in controller
@Controller('users')
export class UsersController {
  @Get('profile')
  @UseGuards(JwtAuthGuard)
  getProfile(@Request() req) {
    return req.user;
  }
}
```

### Global guard
```typescript
// app.module.ts
@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ],
})
export class AppModule {}

// Skip auth on specific routes
@Public() // Custom decorator
@Get('health')
health() {
  return { status: 'ok' };
}
```

---

## Next.js (App Router)

### Server-side auth check
```typescript
// lib/auth.ts
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import jwt from 'jsonwebtoken';

export async function getSession() {
  const cookieStore = cookies();
  const [REDACTED_SECRET]'session')?.value;
  
  if (!token) return null;
  
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
  } catch {
    return null;
  }
}

export async function requireAuth() {
  const session = await getSession();
  if (!session) redirect('/login');
  return session;
}

// app/dashboard/page.tsx
export default async function Dashboard() {
  const session = await requireAuth();
  
  return <div>Welcome, {session.email}</div>;
}
```

### API Route protection
```typescript
// app/api/protected/route.ts
import { NextResponse } from 'next/server';
import { getSession } from '@/lib/auth';

export async function GET() {
  const session = await getSession();
  
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  
  return NextResponse.json({ data: 'secret' });
}
```

### Middleware (edge)
```typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const publicPaths = ['/login', '/register', '/api/auth'];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  // Allow public paths
  if (publicPaths.some(p => pathname.startsWith(p))) {
    return NextResponse.next();
  }
  
  // Check for session cookie
  const session = request.cookies.get('session');
  
  if (!session) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!_next/static|favicon.ico).*)'],
};
```

---

## Laravel

### Built-in auth
```php
// routes/web.php
Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
});

// Or in controller
public function __construct()
{
    $this->middleware('auth');
}

// Check in blade
@auth
    Welcome, {{ auth()->user()->name }}
@endauth
```

### Sanctum (SPA + Mobile)
```php
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost')),

// Login
public function login(Request $request)
{
    $credentials = $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    if (!Auth::attempt($credentials)) {
        throw ValidationException::withMessages([
            'email' => ['Invalid credentials.'],
        ]);
    }

    $request->session()->regenerate();
    return response()->json(['user' => auth()->user()]);
}

// API token authentication
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});
```

---

## Django

### Session auth
```python
# views.py
from django.contrib.auth.decorators import login_required

@login_required
def dashboard(request):
    return render(request, 'dashboard.html')

# Class-based
from django.contrib.auth.mixins import LoginRequiredMixin

class DashboardView(LoginRequiredMixin, TemplateView):
    template_name = 'dashboard.html'
```

### DRF Token auth
```python
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}

# views.py
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import api_view, permission_classes

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def protected_view(request):
    return Response({'user': request.user.email})

# JWT with simplejwt
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}
```

---

## Mobile (React Native / Flutter)

### Secure token storage
```typescript
// React Native with expo-secure-store
import * as SecureStore from 'expo-secure-store';

async function saveToken([REDACTED_SECRET]) {
  await SecureStore.setItemAsync('accessToken', token);
}

async function getToken(): Promise<string | null> {
  return SecureStore.getItemAsync('accessToken');
}

// Add to all requests
const api = axios.create({ baseURL: 'https://api.example.com' });

api.interceptors.request.use(async (config) => {
  const token = await getToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});
```

```dart
// Flutter with flutter_secure_storage
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = FlutterSecureStorage();

Future<void> saveToken(String token) async {
  await storage.write(key: 'accessToken', value: token);
}

Future<String?> getToken() async {
  return storage.read(key: 'accessToken');
}
```


path: oauth.md
content: # OAuth and Social Login - Auth

> **Reference patterns only.** Code examples show placeholders (SECRET, API_KEY, etc.) for developers to replace with their own values. The agent does not execute this code.


## OAuth 2.0 Flows

| Flow | Use Case | Security |
|------|----------|----------|
| **Authorization Code + PKCE** | Web apps, mobile, SPAs | [YES] Best |
| **Authorization Code** | Server-side web apps | [YES] Good |
| **Implicit** | [NO] Deprecated | [NO] Don't use |
| **Client Credentials** | Service-to-service | [YES] Good |

**Always use PKCE** - even for server-side apps.

---

## Authorization Code + PKCE

```typescript
import crypto from 'crypto';

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

// Step 2: Redirect to provider
app.get('/auth/google', (req, res) => {
  const { verifier, challenge } = generatePKCE();
  const state = crypto.randomBytes(16).toString('hex');
  
  // Store for verification
  req.session.oauthState = state;
  req.session.pkceVerifier = verifier;
  
  const params = new URLSearchParams({
    client_id: GOOGLE_CLIENT_ID,
    redirect_uri: 'https://myapp.com/auth/google/callback',
    response_type: 'code',
    scope: 'openid email profile',
    state,
    code_challenge: challenge,
    code_challenge_method: 'S256'
  });
  
  res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});

// Step 3: Handle callback
app.get('/auth/google/callback', async (req, res) => {
  const { code, state } = req.query;
  
  // Verify state
  if (state !== req.session.oauthState) {
    return res.status(400).json({ error: '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({
      client_id: GOOGLE_CLIENT_ID,
      client_secret: GOOGLE_CLIENT_SECRET,
      code,
      grant_type: 'authorization_code',
      redirect_uri: 'https://myapp.com/auth/google/callback',
      code_verifier: req.session.pkceVerifier
    })
  });
  
  const tokens = await tokenResponse.json();
  
  // Get user info
  const userInfo = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
    headers: { Authorization: `Bearer ${tokens.access_token}` }
  }).then(r => r.json());
  
  // Create or link account
  const user = await findOrCreateUser({
    email: userInfo.email,
    name: userInfo.name,
    googleId: userInfo.sub,
    emailVerified: userInfo.email_verified
  });
  
  // Complete login
  await createSession(req, user);
  res.redirect('/dashboard');
});
```

---

## Social Login Providers

### Google
```typescript
const config = {
  authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
  tokenUrl: 'https://oauth2.googleapis.com/token',
  userInfoUrl: 'https://www.googleapis.com/oauth2/v3/userinfo',
  scopes: ['openid', 'email', 'profile']
};
```

### GitHub
```typescript
const config = {
  authUrl: 'https://github.com/login/oauth/authorize',
  tokenUrl: 'https://github.com/login/oauth/access_token',
  userInfoUrl: 'https://api.github.com/user',
  emailUrl: 'https://api.github.com/user/emails', // Email is separate
  scopes: ['read:user', 'user:email']
};
```

### Apple Sign In
```typescript
// Apple requires JWT client_secret
const clientSecret = jwt.sign({
  iss: APPLE_TEAM_ID,
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + 86400 * 180,
  aud: 'https://appleid.apple.com',
  sub: APPLE_CLIENT_ID
}, APPLE_PRIVATE_KEY, { algorithm: 'ES256' });

const config = {
  authUrl: 'https://appleid.apple.com/auth/authorize',
  tokenUrl: 'https://appleid.apple.com/auth/token',
  scopes: ['name', 'email'] // name only on first login!
};
```

---

## Account Linking

**Handle same email from multiple providers:**

```typescript
async function findOrCreateUser(providerData: {
  email: string;
  provider: string;
  providerId: string;
  name?: string;
}) {
  const { email, provider, providerId, name } = providerData;
  
  // Check if provider link exists
  const linked = await db.oauthLinks.findOne({ provider, providerId });
  if (linked) {
    return db.users.findById(linked.userId);
  }
  
  // Check if email exists
  const existing = await db.users.findByEmail(email);
  
  if (existing) {
    // Link to existing account
    await db.oauthLinks.create({
      userId: existing.id,
      provider,
      providerId
    });
    return existing;
  }
  
  // Create new account
  const user = await db.users.create({
    email,
    name,
    emailVerified: true // Provider verified
  });
  
  await db.oauthLinks.create({
    userId: user.id,
    provider,
    providerId
  });
  
  return user;
}
```

---

## OpenID Connect (OIDC)

**OAuth + identity layer.** Adds:
- ID token (JWT with user info)
- UserInfo endpoint
- Standard claims

```typescript
// ID token contains user info directly
const idToken = tokens.id_token;
const payload = jwt.decode(idToken); // Verify signature in production!

// Standard claims
{
  sub: '123456789',        // Unique user ID
  email: '[REDACTED_SECRET]',
  email_verified: true,
  name: 'John Doe',
  picture: 'https://...',
  iss: 'https://accounts.google.com',
  aud: 'your-client-id',
  exp: 1234567890,
  iat: 1234567890
}
```

---

## Enterprise SSO

### SAML 2.0

```typescript
// Using passport-saml or saml2-js
import { SAML } from 'saml2-js';

const sp = new SAML.ServiceProvider({
  entity_id: 'https://myapp.com/saml/metadata',
  assert_endpoint: 'https://myapp.com/saml/acs',
  certificate: SP_CERTIFICATE,
  private_key: SP_PRIVATE_KEY
});

const idp = new SAML.IdentityProvider({
  sso_login_url: 'https://idp.customer.com/saml/login',
  certificates: [IDP_CERTIFICATE]
});

// Login redirect
app.get('/saml/login', (req, res) => {
  sp.create_login_request_url(idp, {}, (err, loginUrl) => {
    res.redirect(loginUrl);
  });
});

// Handle response
app.post('/saml/acs', (req, res) => {
  sp.post_assert(idp, { request_body: req.body }, (err, response) => {
    const email = response.user.email;
    // Create session...
  });
});
```

### OIDC for Enterprise

```typescript
// Each customer has their own OIDC config
const customerConfig = await db.ssoConfigs.findByDomain(email.split('@')[1]);

if (customerConfig) {
  // Redirect to their IdP
  const params = new URLSearchParams({
    client_id: customerConfig.clientId,
    redirect_uri: 'https://myapp.com/auth/oidc/callback',
    response_type: 'code',
    scope: 'openid email profile',
    state: encodeState({ customerId: customerConfig.id })
  });
  
  return res.redirect(`${customerConfig.authUrl}?${params}`);
}
```

---

## Security Checklist

- [ ] Always use PKCE (even server-side)
- [ ] Validate `state` parameter
- [ ] Verify ID token signature
- [ ] Check `aud` claim matches your client ID
- [ ] Check `iss` claim matches expected issuer
- [ ] Use short-lived authorization codes
- [ ] Store tokens encrypted at rest
- [ ] Refresh tokens before expiry


path: passwords.md
content: # Password Handling - Auth

> **Reference patterns only.** Code examples show placeholders (SECRET, API_KEY, etc.) for developers to replace with their own values. The agent does not execute this code.


## Hashing

**Only use:**
- **bcrypt** (cost 10-12) - battle-tested, widely supported
- **Argon2id** - modern, memory-hard, recommended for new projects
- **scrypt** - good alternative to Argon2

**Never use:** MD5, SHA1, SHA256 (without salt/iterations), plaintext, reversible encryption

```typescript
// bcrypt
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 12); // cost 12
const valid = await bcrypt.compare(password, hash);

// Argon2id
import argon2 from 'argon2';
const hash = await argon2.hash(password, { type: argon2.argon2id });
const valid = await argon2.verify(hash, password);
```

**Cost factor:** Should take 100-500ms to hash. Increase cost as hardware improves.

---

## Password Requirements

**Don't do:**
- Arbitrary complexity rules ("1 uppercase, 1 number, 1 symbol")
- Maximum length limits (allow 64+ chars)
- Periodic forced rotation

**Do:**
- Minimum 8 characters (12+ recommended)
- Check against breached password lists
- Show password strength meter
- Allow all characters including spaces

```typescript
// Check against HaveIBeenPwned
import crypto from 'crypto';

async function isBreached([REDACTED_SECRET]): Promise<boolean> {
  const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
  const prefix = hash.slice(0, 5);
  const suffix = hash.slice(5);
  
  const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
  const text = await res.text();
  
  return text.includes(suffix);
}
```

---

## Password Reset

**Secure flow:**

```
1. User requests reset (email)
     |
2. Generate cryptographically random token
     |
3. Store HASHED token with expiry (1h max)
     |
4. Email link with token (HTTPS only)
     |
5. User clicks link, enters new password
     |
6. Verify token hash, check expiry
     |
7. Update password, invalidate token
     |
8. Invalidate ALL sessions
     |
9. Notify user via email
```

```typescript
// Generate reset token
const [REDACTED_SECRET]).toString('hex');
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');

await db.passwordResets.create({
  userId: user.id,
  tokenHash,
  expiresAt: new Date(Date.now() + 3600000) // 1 hour
});

// Send email with: https://app.com/reset?[REDACTED_SECRET]

// Verify reset token
async function verifyResetToken([REDACTED_SECRET]) {
  const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
  
  const reset = await db.passwordResets.findOne({
    tokenHash,
    expiresAt: { $gt: new Date() },
    used: false
  });
  
  if (!reset) throw new Error('Invalid or expired token');
  
  return reset;
}
```

**After password change:**
- Mark token as used
- Invalidate all user sessions
- Send confirmation email
- Log the event

---

## Account Recovery

**Options (from most to least secure):**

| Method | Security | UX | Recommendation |
|--------|----------|----|----|
| Email link | High | Good | [YES] Default |
| Backup codes | High | Poor | [YES] For MFA recovery |
| SMS code | Medium | Good | [!] Only if email unavailable |
| Security questions | Low | OK | [NO] Avoid |
| Support manual | Varies | Poor | [YES] Last resort with strong verification |

**Backup codes (for MFA recovery):**
```typescript
// Generate on MFA setup
function generateBackupCodes(count = 10): string[] {
  return Array.from({ length: count }, () =>
    crypto.randomBytes(4).toString('hex').toUpperCase()
  );
}

// Store hashed
const codes = generateBackupCodes();
const hashedCodes = codes.map(c => bcrypt.hashSync(c, 10));
await db.users.update(userId, { backupCodes: hashedCodes });

// Show codes ONCE to user, never again
return { codes, message: 'Save these. They will not be shown again.' };
```

---

## Password Change (Authenticated)

**Requirements:**
1. Require current password verification
2. Apply same validation as registration
3. Invalidate other sessions
4. Notify via email

```typescript
app.post('/account/password', requireAuth, async (req, res) => {
  const { currentPassword, newPassword } = req.body;
  
  // Verify current password
  const user = await db.users.findById(req.user.id);
  if (!await bcrypt.compare(currentPassword, user.passwordHash)) {
    return res.status(401).json({ error: 'Current password incorrect' });
  }
  
  // Validate new password
  if (newPassword.length < 12) {
    return res.status(400).json({ error: 'Password too short' });
  }
  if (await isBreached(newPassword)) {
    return res.status(400).json({ error: 'Password found in data breach' });
  }
  
  // Update password
  const newHash = await bcrypt.hash(newPassword, 12);
  await db.users.update(req.user.id, { passwordHash: newHash });
  
  // Invalidate other sessions (keep current)
  await invalidateSessionsExcept(req.user.id, req.sessionId);
  
  // Notify
  await sendEmail(user.email, 'Password changed', '...');
  
  res.json({ success: true });
});
```

---

## Timing Attack Prevention

**Problem:** Different response times reveal if user exists.

```typescript
// [NO] Vulnerable
async function login(email, password) {
  const user = await db.users.findByEmail(email);
  if (!user) return { error: 'Invalid credentials' }; // Fast return
  
  const valid = await bcrypt.compare(password, user.passwordHash); // Slow
  if (!valid) return { error: 'Invalid credentials' };
  
  return { success: true };
}

// [YES] Safe - constant time
async function login(email, password) {
  const user = await db.users.findByEmail(email);
  
  // Always compare against something
  const hashToCompare = user?.passwordHash || '$2b$12$fake.hash.here';
  const valid = await bcrypt.compare(password, hashToCompare);
  
  if (!user || !valid) {
    return { error: 'Invalid credentials' };
  }
  
  return { success: true };
}
```


path: strategies.md
content: # Authentication Strategies - Auth

> **Reference patterns only.** Code examples show placeholders (SECRET, API_KEY, etc.) for developers to replace with their own values. The agent does not execute this code.


## Sessions (Server-Side)

**How it works:**
1. User logs in -> server creates session in store (Redis, DB)
2. Server sends session ID in httpOnly cookie
3. Each request sends cookie -> server validates session
4. Logout destroys server session

```typescript
// Session creation
const sessionId = crypto.randomUUID();
await redis.setex(`session:${sessionId}`, 86400, JSON.stringify({
  userId: user.id,
  createdAt: Date.now(),
  ip: request.ip
}));
res.cookie('session', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  maxAge: 86400000
});
```

**Pros:** Instant revocation, simple, server controls everything
**Cons:** Requires session store, harder to scale horizontally

**Best for:** Traditional web apps, admin panels, internal tools

---

## JWT (Stateless Tokens)

**How it works:**
1. User logs in -> server creates signed JWT
2. Client stores JWT (memory or secure storage)
3. Each request includes JWT in Authorization header
4. Server validates signature, no DB lookup needed

```typescript
// Token creation
const accessToken = jwt.sign(
  { sub: user.id, email: user.email },
  SECRET,
  { expiresIn: '15m' }
);

const refreshToken = jwt.sign(
  { sub: user.id, type: 'refresh' },
  REFRESH_SECRET,
  { expiresIn: '7d' }
);
```

**Pros:** Stateless, scales horizontally, works across services
**Cons:** Can't revoke until expiry (without blacklist), larger payload

**Best for:** APIs, microservices, mobile apps

---

## Access + Refresh Token Pattern

**The standard approach for JWT:**

```
+-----------------------------------------------------+
|  Access Token (15min)    Refresh Token (7 days)     |
|  - In memory             - httpOnly cookie          |
|  - Sent in header        - Sent only to /refresh    |
|  - Short-lived           - Rotated on use           |
+-----------------------------------------------------+
```

**Refresh flow:**
1. Access token expires -> client calls /refresh
2. Server validates refresh token
3. Server issues new access token + rotates refresh token
4. If refresh token reused -> revoke all tokens (breach detected)

```typescript
// Refresh endpoint
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies;
  
  // Validate and check not revoked
  const payload = jwt.verify(refreshToken, REFRESH_SECRET);
  const stored = await redis.get(`refresh:${payload.jti}`);
  
  if (!stored || stored.used) {
    // Token reuse detected - revoke all
    await revokeAllUserTokens(payload.sub);
    return res.status(401).json({ error: 'Token reuse detected' });
  }
  
  // Mark as used, issue new pair
  await redis.set(`refresh:${payload.jti}`, { used: true });
  const { accessToken, refreshToken: newRefresh } = generateTokens(payload.sub);
  
  res.cookie('refreshToken', newRefresh, { httpOnly: true, secure: true });
  res.json({ accessToken });
});
```

---

## Hybrid (Sessions + Tokens)

**Best of both worlds:**
- Web app: Sessions (simple, secure cookies)
- Mobile/API: JWT (no cookies needed)
- Same auth service, different delivery

```typescript
// Login endpoint serves both
app.post('/auth/login', async (req, res) => {
  const user = await validateCredentials(req.body);
  
  if (req.headers['x-client-type'] === 'mobile') {
    // Mobile: return tokens
    const tokens = generateTokens(user.id);
    return res.json(tokens);
  }
  
  // Web: create session
  const sessionId = await createSession(user.id);
  res.cookie('session', sessionId, { httpOnly: true, secure: true });
  res.json({ user: sanitize(user) });
});
```

---

## Token Storage (Client-Side)

| Storage | XSS Safe | CSRF Safe | Persists | Best For |
|---------|----------|-----------|----------|----------|
| Memory (JS variable) | [NO] | [YES] | [NO] | Access tokens (SPA) |
| httpOnly cookie | [YES] | [NO]* | [YES] | Refresh tokens, sessions |
| localStorage | [NO] | [YES] | [YES] | [NO] Never for auth |
| Secure enclave (mobile) | [YES] | [YES] | [YES] | Mobile apps |

*Use SameSite=Lax + CSRF tokens for cookie protection

**SPA Pattern:**
```typescript
// Access token in memory
let accessToken = null;

// Refresh token in httpOnly cookie (set by server)
// On app load, call /refresh to get access token

async function getAccessToken() {
  if (!accessToken || isExpired(accessToken)) {
    const res = await fetch('/auth/refresh', { credentials: 'include' });
    accessToken = (await res.json()).accessToken;
  }
  return accessToken;
}
```

---

## Decision Matrix

| Question | If Yes -> |
|----------|----------|
| Traditional server-rendered app? | Sessions |
| Single Page Application? | JWT (memory) + refresh (cookie) |
| Mobile app? | JWT + secure storage |
| Multiple backend services? | JWT (stateless) |
| Need instant logout? | Sessions or JWT + blacklist |
| Enterprise customers? | Add SSO (SAML/OIDC) |


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)