Back to skills
SKILL.md
011 Main A67eb62b
ASecurity**๐ฏ Zweck**: Verhindert OAuth Login Loops durch Cookie-Handling-Fehler. **โ ๏ธ KRITISCH**: Basiert auf Lessons Learned vom 2025-11-12 (3 kritische Root Causes).
- 9 stars
- 0 votes
- 0 copies
- 1 view
- Added October 11, 2026
Works with
Security analysis
100/100npx -y skills add tools-only/X-Skills --skill 011-main_a67eb62b --agent claude-codeAre you the author of 011 Main A67eb62b?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/tools-only-011-main-a67eb62b)# OAuth Cookie Patterns Skill
**๐ฏ Zweck**: Verhindert OAuth Login Loops durch Cookie-Handling-Fehler.
**โ ๏ธ KRITISCH**: Basiert auf Lessons Learned vom 2025-11-12 (3 kritische Root Causes).
## Wann wird dieser Skill aktiviert?
- Du implementierst OAuth 2.0 (Google, GitHub, etc.)
- Du arbeitest mit Session Cookies (`Set-Cookie`, `credentials`)
- Du debuggst Login Loops oder "Session not found" Fehler
- Du erwรคhnst Keywords: OAuth, authentication, cookies, redirect_uri
## Die 3 Kritischen Root Causes (Lessons Learned)
### Root Cause #1: Missing `credentials: 'include'` in Frontend ๐จ
**Problem**: Browser sendet Cookies NICHT automatisch in `fetch()` Requests.
**Symptom**:
- Session Cookie existiert in DevTools โ Application โ Cookies โ
- Session Cookie NICHT in Network โ Request Headers โ
- Endless Login Loop: Login โ Redirect โ Check Session โ Not Found โ Login
**SCHLECHT:**
```typescript
// โ Cookie exists but NEVER sent to server
async function checkSession() {
const response = await fetch('/api/auth/session');
const data = await response.json();
// data.session is ALWAYS null, even though cookie exists!
}
```
**GUT:**
```typescript
// โ
Cookie is included in request
async function checkSession() {
const response = await fetch('/api/auth/session', {
credentials: 'include' // ๐ฏ CRITICAL: Required for same-origin cookies
});
const data = await response.json();
// data.session now correctly populated
}
```
**Regel**: JEDER `fetch()` Call zu auth endpoints braucht `credentials: 'include'`.
**Affected Files (ManufacturingInsideAnalyzer):**
```typescript
// App.tsx - 4 locations fixed
const checkSession = async () => {
const res = await fetch('/api/auth/session', { credentials: 'include' });
};
const handleLoginSuccess = async () => {
const res = await fetch('/api/auth/session', { credentials: 'include' });
};
const persistPreferences = async () => {
await fetch('/api/preferences', { credentials: 'include' });
};
const handleLogout = async () => {
await fetch('/api/auth/logout', { credentials: 'include' });
};
```
---
### Root Cause #2: `Domain=.vercel.app` Blocked by Browser ๐จ
**Problem**: Setting `Domain=.vercel.app` causes browser to REJECT cookie entirely.
**Symptom**:
- Server logs show `Set-Cookie` header sent โ
- Browser DevTools โ Application โ Cookies shows NOTHING โ
- Silent failure, no error message
**SCHLECHT:**
```typescript
// โ Browser rejects this cookie (security restriction)
function buildSessionCookie(token: string): string {
return [
`session=${token}`,
'Domain=.vercel.app', // ๐จ CAUSES REJECTION
'HttpOnly',
'Secure',
'SameSite=Lax',
'Path=/',
`Max-Age=${30 * 24 * 60 * 60}`
].join('; ');
}
```
**GUT:**
```typescript
// โ
NO Domain attribute โ browser scopes automatically
function buildSessionCookie(token: string): string {
const attributes = [
`session=${token}`,
'HttpOnly', // Prevent XSS attacks
'SameSite=Lax', // Allow OAuth redirects, prevent CSRF
'Path=/', // Available on all routes
`Max-Age=${30 * 24 * 60 * 60}` // 30 days
];
if (process.env.NODE_ENV === 'production') {
attributes.push('Secure'); // HTTPS only
// NO Domain attribute!
}
return attributes.join('; ');
}
```
**Warum funktioniert das?**
- Ohne Domain โ Cookie ist scoped zu `manufacturing-insights.vercel.app`
- Mit `Domain=.vercel.app` โ Browser lehnt ab (security policy)
- Vercel Production Aliases funktionieren perfekt ohne explizites Domain
**Regel**: NIEMALS `Domain` setzen auf Vercel. Lass Browser automatisch scopen.
---
### Root Cause #3: Invalid Google OAuth Credentials ๐จ
**Problem**: Even 1 character wrong in `GOOGLE_CLIENT_SECRET` โ total failure.
**Symptom**:
- Redirect to `/?error=token_exchange_failed`
- Vercel Logs zeigen: `{ "error": "invalid_client", "error_description": "Unauthorized" }`
**Debugging Steps:**
```bash
# 1. Check Vercel Environment Variables
npx vercel env ls
# 2. Verify exact match with Google Cloud Console
# Go to: https://console.cloud.google.com/apis/credentials
# - Click your OAuth 2.0 Client
# - Copy EXACT Client ID and Secret
# - Compare character-by-character with Vercel
# 3. Update Vercel
GOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=[REDACTED] # EXACT match required!
GOOGLE_REDIRECT_URI=https://manufacturing-insights.vercel.app/api/auth/google/callback
# 4. Trigger redeploy
git commit --allow-empty -m "chore: trigger redeploy for env vars"
git push
```
**Regel**: Copy-Paste Credentials, never type manually!
---
## Cookie Attribute Best Practices
### รbersicht
| Attribute | Wert | Zweck | Pflicht? |
|-----------|------|-------|----------|
| `HttpOnly` | (flag) | Verhindert JavaScript-Zugriff (XSS Protection) | โ
JA |
| `Secure` | (flag) | Nur HTTPS (production) | โ
JA (prod) |
| `SameSite` | `Lax` | OAuth-Redirects erlauben, CSRF verhindern | โ
JA |
| `Path` | `/` | Cookie auf allen Routes verfรผgbar | โ
JA |
| `Max-Age` | `2592000` | 30 Tage Lebensdauer | โ
JA |
| `Domain` | (omit) | Browser scoped automatisch | โ NEIN |
### SameSite Attribute Explained
**`SameSite=Strict`** โ Blockiert OAuth!
```
Browser: "Cookie nur bei same-site requests senden"
Problem: OAuth redirect von accounts.google.com โ Blocked!
```
**`SameSite=Lax`** โ
OAuth funktioniert!
```
Browser: "Cookie bei top-level navigation (GET) senden"
OAuth: accounts.google.com โ GET redirect โ Cookie sent โ
CSRF: POST from evil.com โ Cookie NOT sent โ
```
**`SameSite=None`** โ ๏ธ Nur mit 3rd-party cookies!
```
Requires: SameSite=None; Secure
Use Case: iframes, cross-site embeds
OAuth: Nicht nรถtig!
```
**Regel**: `SameSite=Lax` fรผr OAuth flows!
---
## Browser DevTools Debugging
### Schritt 1: Check if Cookie is Set
**Chrome DevTools โ Application โ Cookies โ `https://your-domain.com`**
โ
**Expected after login:**
```
Name: session
Value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... (JWT token)
Domain: manufacturing-insights.vercel.app (NO leading dot!)
Path: /
Secure: โ (production only)
HttpOnly: โ
SameSite: Lax
Max-Age: 2592000 (30 days)
```
โ **If NO cookie** โ Server isn't setting it OR browser is blocking it.
**Debugging:**
```bash
# Check server logs
npx vercel logs --prod --since 5m
# Look for:
[AUTH] Google callback: Creating session for user@example.com
[AUTH] Setting cookie: session=eyJ...
```
If logs show cookie set, but browser doesn't receive it:
โ **Domain attribute problem** (Remove Domain!)
---
### Schritt 2: Check if Cookie is Sent
**Chrome DevTools โ Network โ Select request โ Headers โ Request Headers**
โ
**Expected:**
```
Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
โ **If cookie exists in Application tab but NOT in Network tab:**
โ **Missing `credentials: 'include'`** in fetch!
**Quick Fix:**
```typescript
// Before
fetch('/api/auth/session')
// After
fetch('/api/auth/session', { credentials: 'include' })
```
---
### Schritt 3: Check Server-Side Cookie Parsing
**Server Logs:**
```typescript
// api/auth.ts - Session check endpoint
export default async function handler(req: VercelRequest, res: VercelResponse) {
console.log('[AUTH SESSION] Received cookies:', req.cookies);
const sessionToken = req.cookies.session;
if (!sessionToken) {
console.log('[AUTH SESSION] No session cookie found');
return res.json({ session: null });
}
// Verify JWT...
}
```
**Expected Logs:**
```
[AUTH SESSION] Received cookies: { session: 'eyJ...' }
[AUTH SESSION] Session valid for user@example.com
```
**If cookies empty:**
```
[AUTH SESSION] Received cookies: {}
```
โ Frontend not sending cookies (`credentials: 'include'` missing)
---
## Complete Working Implementation
### Server Side - `api/auth.ts`
```typescript
import { sign, verify } from 'jsonwebtoken';
import type { VercelRequest, VercelResponse } from '@vercel/node';
const JWT_SECRET = process.env.JWT_SECRET!;
// Build session cookie with correct attributes
function buildSessionCookie(token: string): string {
const attributes = [
`session=${token}`,
'HttpOnly',
'SameSite=Lax',
'Path=/',
`Max-Age=${30 * 24 * 60 * 60}` // 30 days
];
if (process.env.NODE_ENV === 'production') {
attributes.push('Secure');
// NO Domain attribute!
}
return attributes.join('; ');
}
// Google OAuth Callback
export default async function handler(req: VercelRequest, res: VercelResponse) {
const { code } = req.query;
// 1. Exchange code for tokens
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: process.env.GOOGLE_REDIRECT_URI,
grant_type: 'authorization_code'
})
});
if (!tokenResponse.ok) {
const error = await tokenResponse.json();
console.error('[OAUTH] Token exchange failed:', error);
return res.redirect('/?error=token_exchange_failed');
}
const tokens = await tokenResponse.json();
// 2. Get user info
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${tokens.access_token}` }
});
const user = await userResponse.json();
// 3. Create session token (JWT)
const sessionToken = sign(
{
email: user.email,
name: user.name,
picture: user.picture,
exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30 days
},
JWT_SECRET
);
// 4. Set cookie
res.setHeader('Set-Cookie', buildSessionCookie(sessionToken));
console.log('[AUTH] Google callback: Creating session for', user.email);
// 5. Redirect to success
return res.redirect('/?login=success');
}
```
### Client Side - `App.tsx`
```typescript
import { useState, useEffect } from 'react';
interface Session {
email: string;
name: string;
picture: string;
}
export function App() {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
// Check session on app load
useEffect(() => {
checkSession();
}, []);
// Check session with credentials: 'include'
async function checkSession() {
try {
const response = await fetch('/api/auth/session', {
credentials: 'include' // ๐ฏ CRITICAL!
});
const data = await response.json();
setSession(data.session);
} catch (error) {
console.error('Session check failed:', error);
} finally {
setLoading(false);
}
}
// Handle OAuth redirect
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get('login') === 'success') {
// Remove query param
window.history.replaceState({}, '', '/');
// Refresh session
checkSession();
}
}, []);
// Logout handler
async function handleLogout() {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include' // ๐ฏ CRITICAL!
});
setSession(null);
}
if (loading) return <div>Loading...</div>;
if (!session) {
return (
<div>
<h1>Sign In</h1>
<a href="/api/auth?action=google">
<button>Sign in with Google</button>
</a>
</div>
);
}
return (
<div>
<h1>Welcome {session.name}</h1>
<img src={session.picture} alt={session.name} />
<button onClick={handleLogout}>Logout</button>
</div>
);
}
```
---
## Testing Checklist
โ
**Test in Incognito Window** (fresh state, no old cookies)
**Steps:**
1. Open incognito window
2. Open DevTools โ Application โ Cookies
3. Navigate to your app
4. Click "Sign in with Google"
5. After redirect, check:
- [ ] `session` cookie exists
- [ ] Domain: `your-domain.com` (no leading dot)
- [ ] Secure: โ (production)
- [ ] HttpOnly: โ
- [ ] SameSite: `Lax`
6. Open DevTools โ Network
7. Refresh page
8. Click on request to `/api/auth/session`
9. Verify **Cookie header is present** in Request Headers
10. User should stay logged in โ
---
## Common Mistakes & Fixes
### Mistake 1: `SameSite=Strict` for OAuth
```typescript
// โ Blocks OAuth redirects from accounts.google.com
attributes.push('SameSite=Strict');
// โ
Use Lax - allows top-level GET navigation
attributes.push('SameSite=Lax');
```
### Mistake 2: Setting `Domain=.vercel.app`
```typescript
// โ Browser rejects the cookie
attributes.push('Domain=.vercel.app');
// โ
Omit Domain - browser scopes to current host
// (no Domain attribute)
```
### Mistake 3: Forgetting `credentials: 'include'`
```typescript
// โ Cookie never sent to server
fetch('/api/auth/session')
// โ
Add to EVERY fetch that needs cookies
fetch('/api/auth/session', { credentials: 'include' })
```
### Mistake 4: Not testing in Incognito
```typescript
// โ Old cookies with wrong attributes stay in browser
// Test in normal browser window
// โ
Always test OAuth in fresh incognito window
// Cmd+Shift+N (Chrome) / Cmd+Shift+P (Firefox)
```
---
## Debugging Flowchart
```
Session not found?
โ
โโ Cookie exists in DevTools โ Application โ Cookies?
โ โ
โ โโ YES โ Cookie sent in Network โ Request Headers?
โ โ โ
โ โ โโ YES โ Server-side issue (check JWT validation)
โ โ โ
โ โ โโ NO โ Missing credentials: 'include' in fetch
โ โ
โ โโ NO โ Cookie not set by server
โ โ
โ โโ Check server logs: Set-Cookie header sent?
โ โ โ
โ โ โโ YES โ Browser blocking (remove Domain attribute)
โ โ โ
โ โ โโ NO โ OAuth callback not creating session
โ โ
โ โโ Verify Google credentials match exactly
```
---
## Ressourcen
- [MDN: Using Fetch - Credentials](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included)
- [MDN: Set-Cookie Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)
- [OWASP: SameSite Cookie Attribute](https://owasp.org/www-community/SameSite)
- [OAuth 2.0 RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749)
- [Vercel: Understanding Cookies](https://vercel.com/blog/understanding-cookies)
---
## Lesson Learned Origin
**Date**: 2025-11-12
**Incident**: Google OAuth login loop
**Duration**: ~4 hours debugging
**Root Causes**: 3 separate issues compounded
**Impact**: Login completely broken
**Prevention**: This skill + automated testing
**Files Fixed:**
- `App.tsx`: Added `credentials: 'include'` to 4 fetch calls
- `api/auth.ts`: Removed `Domain=.vercel.app` from cookie attributes
- Vercel Dashboard: Fixed `GOOGLE_CLIENT_SECRET` typo
**Final Commit**: `34a0e7a` - "fix: remove Domain=.vercel.app from cookies"
---
**๐ฏ Ziel**: Zero OAuth Login Loops!
Attribution
Comments
Loading commentsโฆ
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.