Skip to content
Back to skills

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
securityjavascripttypescriptgojavabashreactnodetestingdebugginggit

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 011-main_a67eb62b --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 011 Main A67eb62b?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 011 Main A67eb62b
[![Security: A โ€” Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-011-main-a67eb62b/badge)](https://www.skillsdirectory.com/skills/tools-only-011-main-a67eb62b)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# 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

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading commentsโ€ฆ

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.