# Browser-Based OAuth Code Flow for Headless Agents

This reference documents the recurring pattern where a user must complete an OAuth consent flow in a browser, then copy-paste the authorization `code` back to the agent. The agent exchanges the code for access/refresh tokens via HTTP.

## When this pattern applies

- Google OAuth (YouTube, Calendar, Drive, Gmail)
- Notion OAuth integrations
- Any service using OAuth 2.0 authorization-code grant

## Prerequisites

- Client ID and Client Secret already in `.secrets/`
- Redirect URI set to `http://localhost` (headless — no actual web server)
- User must perform the browser step manually

## Step-by-step

### 1. Build the authorization URL

```python
import urllib.parse

params = {
    "client_id": client_id,
    "redirect_uri": "http://localhost",
    "response_type": "code",
    "scope": " ".join(scopes),
    "access_type": "offline",      # CRITICAL: enables refresh token
    "prompt": "consent",           # CRITICAL: forces consent screen even if already approved
    "include_granted_scopes": "true",
}

auth_url = "https://accounts.google.com/o/oauth2/auth?" + urllib.parse.urlencode(params)
```

**Why `prompt=consent` matters:** Without it, Google skips the consent screen if the user previously approved the app, and **returns no refresh token**. The agent would get only a short-lived access token.

### 2. Send the link to the user

Present the URL clearly:

```
ὑ7 <BUILT_AUTH_URL>
```

Followed by instructions:
1. Öffne den Link im Browser
2. Melde dich an und klicke "Zulassen"
3. Du wirst zu `http://localhost/?code=...` weitergeleitet
4. Kopiere den **Code** aus der URL ( nach `code=` und vor `&scope=`)
5. Schicke den Code hier zurück

**Warn the user:** Der Code ist nur ca. 10 Minuten gültig und ca. 200 Zeichen lang (z. B. `4/0AeaYSH...`).

### 3. Receive the code

The user sends an HTTP response snippet like:
```
http://localhost/?iss=https://accounts.google.com&code=4/0AeoWuM-ZusLuTyH1UBOA9s15Bk36GwHyJss2rwz0rzTaqeo87nZVSZmdzC_NvD1I_mApZQ&scope=...
```

Parse only the `code` parameter. Discard everything else in the response.

**Do NOT echo the full URL** in the agent response (it contains the auth code). Process it silently and discard.

### 4. Exchange code for tokens

```python
payload = urllib.parse.urlencode({
    "code": code,
    "client_id": client_id,
    "client_secret": client_secret,
    "redirect_uri": "http://localhost",
    "grant_type": "authorization_code"
}).encode('utf-8')

req = urllib.request.Request(
    "https://oauth2.googleapis.com/token",
    data=payload,
    headers={"Content-Type": "application/x-www-form-urlencoded"},
    method="POST"
)

with urllib.request.urlopen(req, timeout=20) as resp:
    data = json.loads(resp.read().decode())
    access_token = data["access_token"]
    refresh_token = data.get("refresh_token", "")
```

### 5. Validate immediately

```python
# YouTube
req = urllib.request.Request(
    "https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics&mine=true",
    headers={"Authorization": f"Bearer {access_token}"}
)

# Calendar
req = urllib.request.Request(
    "https://www.googleapis.com/calendar/v3/users/me/calendarList",
    headers={"Authorization": f"Bearer {access_token}"}
)

# Drive
req = urllib.request.Request(
    "https://www.googleapis.com/drive/v3/files?pageSize=10",
    headers={"Authorization": f"Bearer {access_token}"}
)
```

### 6. Store securely

- **Never** store credentials in agent memory/chat context
- Write to disk in `.secrets/` with `chmod 600`
- Update `~/.hermes/.env` with references to the secrets file
- Do NOT echo tokens back to the user in chat

### 7. Handle missing refresh token

If `refresh_token` is missing in the response:
1. Tell the user: *"Der Refresh Token fehlt oft, wenn 'prompt=consent' nicht gesetzt war oder du schonmal zugestimmt hast."*
2. Build a new auth URL with `approval_prompt=force` or have them revoke access at https://myaccount.google.com/permissions
3. Repeat the flow

## Pitfalls

1. **User pastes the full `http://localhost` URL** — parse `code=` from it, don't ask them to manually extract.
2. **Missing `access_type=offline`** — no refresh token returned, access token expires in 1 hour.
3. **Missing `prompt=consent`** — Google skips consent if previously granted → no refresh token.
4. **Code expires in 10 minutes** — warn the user before sending the link.
5. **Echoing the code in chat** — confirm storage by key name, not raw value.
6. **403 on Gmail** even after successful OAuth → Gmail API not enabled in Google Cloud Console. Enable at console.cloud.google.com.

## Variations

### Notion OAuth
- Auth URL: `https://api.notion.com/v1/oauth/authorize?client_id=...&redirect_uri=...&response_type=code`
- Token exchange: `POST https://api.notion.com/v1/oauth/token` with Basic Auth header

### Other providers
- Same pattern, different endpoints. Always check docs for `access_type` equivalent.
