# Auth

> Auth rules for modern frontend - session vs token models, secure credential storage (httpOnly cookies, __Host-), JWT validation, access/refresh with rotation and reuse detection, server-side sessions, password hashing (bcrypt/argon2), password reset, OAuth 2.0/OIDC/PKCE, MFA/TOTP, WebAuthn/passkeys, protected routes, CSRF, XSS, session state, anti-abuse

- Skill: `14bryanespinoza/auth` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 14bryanespinoza/auth`
- Raw SKILL.md: https://api.skillmd.com/api/skills/14bryanespinoza/auth/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: 14BryanEspinoza (https://skillmd.com/u/14bryanespinoza)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/14bryanespinoza/auth

---


# Auth — Authentication & Authorization

---

## 1. Philosophy

1. **Sessions for SSR, tokens for SPA** — Choose model by architecture.
2. **httpOnly cookies only** — Never localStorage/sessionStorage for credentials.
3. **Rotate + detect reuse** — Refresh tokens rotate; reuse = revoke all.
4. **Argon2 for passwords** — Never bcrypt/scrypt in new projects.
5. **PKCE mandatory** — OAuth public clients must use PKCE.

---

## 2. Session vs Token

| Factor           | Sessions (Cookie)    | JWT (Token)                          |
| ---------------- | -------------------- | ------------------------------------ |
| **Architecture** | SSR, traditional     | SPA, microservices                   |
| **Storage**      | httpOnly cookie      | httpOnly cookie (never localStorage) |
| **Revocation**   | Immediate (server)   | Requires blocklist / short expiry    |
| **Scalability**  | Shared store (Redis) | Stateless                            |
| **CSRF risk**    | Yes (mitigate)       | No (if cookie SameSite=Strict)       |
| **Payload size** | Session ID only      | Claims embedded                      |

### Decision

- **Next.js / Astro SSR** → Sessions (cookie + Redis)
- **React SPA / Vite** → JWT in httpOnly cookie
- **Mobile / native** → JWT (no cookie support)

---

## 3. Client Credential Storage

### httpOnly Cookie (mandatory)

```http
Set-Cookie: access_token=eyJ...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=900
Set-Cookie: refresh_token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800
```

### Cookie attributes

| Attribute        | Access Token | Refresh Token   |
| ---------------- | ------------ | --------------- |
| `HttpOnly`       | ✅           | ✅              |
| `Secure`         | ✅           | ✅              |
| `SameSite`       | `Lax`        | `Strict`        |
| `Path`           | `/`          | `/auth/refresh` |
| `__Host-` prefix | ✅           | ✅              |
| `Max-Age`        | 900 (15 min) | 604800 (7 days) |

### Rules

- **`__Host-` prefix** — requires `Secure`, `Path=/`, no `Domain`
- **Never `localStorage`/`sessionStorage`** — XSS steals tokens
- **Short-lived access** — 15 min max
- **Refresh restricted path** — only sent to `/auth/refresh`

---

## 4. JWT

### Structure

```json
// Header
{ "alg": "RS256", "typ": "JWT" }

// Payload
{
  "sub": "usr_abc123",
  "email": "user@example.com",
  "roles": ["user"],
  "iat": 1704067200,
  "exp": 1704068100,
  "jti": "tok_xyz789"
}
```

### Validation (server)

```ts
import { jwtVerify } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function verifyToken(token: string) {
  const { payload } = await jwtVerify(token, secret, {
    issuer: "https://api.example.com",
    audience: "https://app.example.com",
    maxTokenAge: "15m",
  });
  return payload;
}
```

### Rules JWT

- **RS256/ES256** — asymmetric, rotate keys via JWKS
- **Short expiry** — 15 min access, 7 days refresh
- **`jti` claim** — unique ID for revocation
- **Validate `iss`, `aud`, `exp`, `nbf`** — always
- **JWKS endpoint** — `/.well-known/jwks.json` for key rotation

---

## 5. Access + Refresh Tokens

### Rotation + Reuse Detection

```ts
// Refresh endpoint
async function refresh(refreshToken: string) {
  const payload = await verifyToken(refreshToken);

  // Check reuse
  const existing = await db.refreshToken.findUnique({
    where: { token: refreshToken },
  });
  if (!existing || existing.revoked) {
    // REUSE DETECTED — revoke entire family
    await db.refreshToken.updateMany({
      where: { familyId: existing.familyId },
      data: { revoked: true },
    });
    throw new Error("Token reuse detected");
  }

  // Rotate: revoke old, issue new pair
  await db.refreshToken.update({
    where: { id: existing.id },
    data: { revoked: true },
  });

  const newAccess = createAccessToken(payload.sub);
  const newRefresh = await createRefreshToken(payload.sub, existing.familyId);

  return { accessToken: newAccess, refreshToken: newRefresh };
}
```

### Rules Access + Refresh Tokens

- **Rotate on every refresh** — new access + new refresh
- **Reuse = revoke family** — immediate logout everywhere
- **Refresh in httpOnly cookie** — `SameSite=Strict`, path `/auth/refresh`
- **Access in httpOnly cookie** — `SameSite=Lax`
- **Track family ID** — link all tokens from same login

---

## 6. Server-Side Sessions

```ts
// Session store (Redis)
interface SessionData {
  userId: string;
  roles: string[];
  createdAt: number;
  lastActivity: number;
}

// Create
await redis.setex(`session:${sessionId}`, 86400, JSON.stringify(data));

// Validate
const session = await redis.get(`session:${sessionId}`);
if (!session) throw new Error("Invalid session");
```

### Rules Server-Side Sessions

- **Redis with TTL** — auto-expiry
- **Session ID in httpOnly cookie** — `__Host-session_id`
- **Rotate on privilege change** — new session on role change
- **Destroy on logout** — `DEL` + clear cookie

---

## 7. Password Hashing

```ts
import { hash, verify } from "@node-rs/argon2";

// Hash (register)
const hash = await hash(password, {
  memoryCost: 19456, // 19 MB
  timeCost: 2, // iterations
  parallelism: 1,
  outputLen: 32,
  type: "Argon2id",
});

// Verify (login)
const valid = await verify(hash, password);
```

### Rules Password Hashing

- **Argon2id only** — `argon2` npm or `@node-rs/argon2`
- **Memory cost ≥ 19 MB** — resist GPU
- **Time cost ≥ 2** — tune for ~100ms
- **Never bcrypt/scrypt** — legacy
- **Constant-time verify** — prevents timing attacks

---

## 8. Password Reset

```ts
// Request reset
const token = crypto.randomBytes(32).toString("hex");
const hash = await hashToken(token);
await db.passwordReset.create({
  userId: user.id,
  tokenHash: hash,
  expiresAt: new Date(Date.now() + 3600000), // 1 hour
});
await email.send(user.email, { resetToken: token });

// Confirm reset
const record = await db.passwordReset.findFirst({ where: { userId } });
if (!record || record.expiresAt < new Date())
  throw new Error("Invalid/expired");
const valid = await verify(record.tokenHash, token);
if (!valid) throw new Error("Invalid token");
await updatePassword(userId, newPassword);
await db.passwordReset.delete({ where: { id: record.id } });
```

### Rules Password Reset

- **Token = 32 bytes random** — not guessable
- **Hash stored** — not plaintext
- **1-hour expiry** — short window
- **Single use** — delete after use
- **Rate limit requests** — 3/hour per email

---

## 9. OAuth 2.0 / OIDC / PKCE

### PKCE Flow (mandatory for public clients)

```ts
// 1. Generate code verifier + challenge
const codeVerifier = base64url(crypto.randomBytes(32));
const codeChallenge = base64url(
  await crypto.subtle.digest("SHA-256", codeVerifier),
);

// 2. Redirect to auth server
const url = `https://auth.example.com/authorize?
  response_type=code&
  client_id=${CLIENT_ID}&
  redirect_uri=${REDIRECT_URI}&
  scope=openid profile email&
  code_challenge=${codeChallenge}&
  code_challenge_method=S256&
  state=${state}`;

// 3. Callback: exchange code for tokens
const tokens = await fetch("https://auth.example.com/token", {
  method: "POST",
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    code_verifier: codeVerifier,
  }),
});
```

### Rules OAuth 2.0

- **PKCE mandatory** — prevents code interception
- **`state` parameter** — CSRF protection
- **Validate `redirect_uri` exactly** — pre-registered
- **Store tokens in httpOnly cookies** — not localStorage

---

## 10. MFA / TOTP

```ts
import { generateSecret, verifyToken } from "otplib";

// Setup
const secret = generateSecret();
const otpauth = `otpauth://totp/Example:user@example.com?secret=${secret}&issuer=Example`;

// Verify
const valid = verifyToken(userInput, user.totpSecret);
```

### Rules MFA / TOTP

- **TOTP (RFC 6238)** — 30s window, 6 digits
- **Backup codes** — 8 single-use codes on setup
- **Enforce for admins** — optional for users
- **Rate limit attempts** — 5/min

---

## 11. WebAuthn / Passkeys

```ts
// Registration
const options = await generateRegistrationOptions({
  rpName: "Example",
  rpID: "example.com",
  userID: user.id,
  userName: user.email,
  userDisplayName: user.name,
  authenticatorSelection: {
    residentKey: "preferred",
    userVerification: "required",
  },
});
// Send options to client -> navigator.credentials.create()

// Authentication
const options = await generateAuthenticationOptions({ rpID: "example.com" });
// Client -> navigator.credentials.get() -> send to server -> verify
```

### Rules WebAuthn

- **Passkeys preferred** — discoverable credentials
- **User verification required** — biometric/PIN
- **Resident keys** — no username entry needed
- **Fallback to password** — during transition

---

## 12. Protected Routes

### Server (middleware)

```ts
// Next.js middleware.ts
export async function middleware(request: NextRequest) {
  const token = request.cookies.get("access_token")?.value;
  if (!token) return redirect("/login");

  try {
    await verifyToken(token);
    return NextResponse.next();
  } catch {
    return redirect("/login");
  }
}
```

### Client (React)

```tsx
function ProtectedRoute({ children }) {
  const { user, loading } = useAuth();
  if (loading) return <Skeleton />;
  if (!user) return <Navigate to="/login" replace />;
  return children;
}
```

---

## 13. CSRF

> **CSRF prevention**: see `security` skill.

### Summary

- **SameSite cookies** — `Lax` for access, `Strict` for refresh
- **Double-submit cookie** — for non-GET if SameSite unavailable
- **Custom header** — `X-CSRF-Token` for API mutations

---

## 14. XSS

> **XSS prevention**: see `security` skill.

### Summary XSS

- **Content-Type: application/json** — no sniffing
- **CSP header** — `script-src 'self'`
- **Sanitize user content** — DOMPurify for HTML

---

## 15. Session State (Client)

```tsx
// React context
const AuthContext = createContext<AuthState | null>(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("/api/auth/me")
      .then((res) => res.json())
      .then((data) => {
        setUser(data.user);
        setLoading(false);
      })
      .catch(() => setLoading(false));
  }, []);

  return (
    <AuthContext.Provider value={{ user, setUser, loading }}>
      {children}
    </AuthContext.Provider>
  );
}
```

---

## 16. Anti-Abuse

| Measure                  | Config                             |
| ------------------------ | ---------------------------------- |
| **Login rate limit**     | 5 attempts / 15 min / IP           |
| **Registration limit**   | 3 / hour / IP                      |
| **Password reset limit** | 3 / hour / email                   |
| **MFA attempts**         | 5 / min / user                     |
| **Account lockout**      | 30 min after 10 failures           |
| **Bot detection**        | Turnstile / hCaptcha on auth forms |

---

## 17. Methodology

Before using ANY auth pattern not documented in this skill:

1. **MCP Context7** (priority): `context7_resolve-library-id` + `context7_query-docs` for jose, oidc-client-ts, WebAuthn.
2. **Official docs**: OWASP Auth Cheatsheet, RFC 7519, RFC 7636, WebAuthn spec.
3. **Project config**: `package.json`, auth middleware, cookie config — verify against actual setup.
4. **HARD RULE**: If not in this skill AND cannot be verified against 2 authoritative sources → DO NOT USE IT. Document as assumption or risk in report to orchestrator.

---

## 18. Prohibitions

- ❌ Do not store tokens in localStorage/sessionStorage
- ❌ Do not use symmetric JWT (HS256) — use RS256/ES256
- ❌ Do not skip refresh token rotation
- ❌ Do not skip reuse detection
- ❌ Do not use bcrypt for new projects — Argon2id only
- ❌ Do not expose tokens in URLs
- ❌ Do not use OAuth without PKCE
- ❌ Do not skip rate limiting on auth endpoints
- ❌ Do not use SameSite=None without Secure

---

## 19. References

> **Note:** For API design (auth endpoints), see [API Design](../api-design/SKILL.md)
> **Note:** For JavaScript conventions (crypto, fetch), see [JavaScript](../javascript/SKILL.md)
> **Note:** For Security (CSRF, XSS, rate limiting), see [Security](../security/SKILL.md)
> **Note:** For Next.js patterns, see [Next.js](../nextjs/SKILL.md)
> **Note:** For React patterns, see [React](../reactjs/SKILL.md)

---

Last updated: 2026-08

