Skilltypescript

Oauth Integrations Skill

|

View Source
SKILL.md
---
name: oauth-integrations
description: |
  Implement OAuth 2.0 authentication with GitHub and Microsoft Entra (Azure AD) in Cloudflare Workers
  and other edge environments. Covers provider-specific quirks, required headers, scope requirements,
  and token handling without MSAL.

  Use when: implementing GitHub OAuth, Microsoft/Azure AD authentication, handling OAuth callbacks,
  or troubleshooting 403 errors in OAuth flows.
license: MIT
---

# OAuth Integrations for Edge Environments

Implement GitHub and Microsoft OAuth in Cloudflare Workers and other edge runtimes.

## GitHub OAuth

### Required Headers

GitHub API has strict requirements that differ from other providers.

| Header | Requirement |
|--------|-------------|
| `User-Agent` | **REQUIRED** - Returns 403 without it |
| `Accept` | `application/vnd.github+json` recommended |

```typescript
const resp = await fetch('https://api.github.com/user', {
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'User-Agent': 'MyApp/1.0',  // Required!
    'Accept': 'application/vnd.github+json',
  },
});
```

### Private Email Handling

GitHub users can set email to private (`/user` returns `email: null`).

```typescript
if (!userData.email) {
  const emails = await fetch('https://api.github.com/user/emails', { headers })
    .then(r => r.json());
  userData.email = emails.find(e => e.primary && e.verified)?.email;
}
```

Requires `user:email` scope.

### Token Exchange

Token exchange returns form-encoded by default. Add Accept header for JSON:

```typescript
const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Accept': 'application/json',  // Get JSON response
  },
  body: new URLSearchParams({ code, client_id, client_secret, redirect_uri }),
});
```

### GitHub OAuth Notes

| Issue | Solution |
|-------|----------|
| Callback URL | Must be EXACT - no wildcards, no subdirectory matching |
| Token exchange returns form-encoded | Add `'Accept': 'application/json'` header |
| Tokens don't expire | No refresh flow needed, but revoked = full re-auth |

## Microsoft Entra (Azure AD) OAuth

### Why MSAL Doesn't Work in Workers

MSAL.js depends on:
- Browser APIs (localStorage, sessionStorage, DOM)
- Node.js crypto module

Cloudflare's V8 isolate runtime has neither. Use manual OAuth instead:
1. Manual OAuth URL construction
2. Direct token exchange via fetch
3. JWT validation with `jose` library

### Required Scopes

```typescript
// For user identity (email, name, profile picture)
const scope = 'openid email profile User.Read';

// For refresh tokens (long-lived sessions)
const scope = 'openid email profile User.Read offline_access';
```

**Critical**: `User.Read` is required for Microsoft Graph `/me` endpoint. Without it, token exchange succeeds but user info fetch returns 403.

### User Info Endpoint

```typescript
// Microsoft Graph /me endpoint
const resp = await fetch('https://graph.microsoft.com/v1.0/me', {
  headers: { Authorization: `Bearer ${accessToken}` },
});

// Email may be in different fields
const email = data.mail || data.userPrincipalName;
```

### Tenant Configuration

| Tenant Value | Who Can Sign In |
|--------------|-----------------|
| `common` | Any Microsoft account (personal + work) |
| `organizations` | Work/school accounts only |
| `consumers` | Personal Microsoft accounts only |
| `{tenant-id}` | Specific organization only |

### Azure Portal Setup

1. App Registration → New registration
2. Platform: **Web** (not SPA) for server-side OAuth
3. Redirect URIs: Add both `/callback` and `/admin/callback`
4. Certificates & secrets → New client secret

### Token Lifetimes

| Token Type | Default Lifetime | Notes |
|------------|------------------|-------|
| Access token | 60-90 minutes | Configurable via token lifetime policies |
| Refresh token | 90 days | Revoked on password change |
| ID token | 60 minutes | Same as access token |

**Best Practice**: Always request `offline_access` scope and implement refresh token flow for sessions longer than 1 hour.

## Common Corrections

| If Claude suggests... | Use instead... |
|----------------------|----------------|
| GitHub fetch without User-Agent | Add `'User-Agent': 'AppName/1.0'` (REQUIRED) |
| Using MSAL.js in Workers | Manual OAuth + jose for JWT validation |
| Microsoft scope without User.Read | Add `User.Read` scope |
| Fetching email from token claims only | Use Graph `/me` endpoint |

## Error Reference

### GitHub Errors

| Error | Cause | Fix |
|-------|-------|-----|
| 403 Forbidden | Missing User-Agent header | Add User-Agent header |
| `email: null` | User has private email | Fetch `/user/emails` with `user:email` scope |

### Microsoft Errors

| Error | Cause | Fix |
|-------|-------|-----|
| AADSTS50058 | Silent auth failed | Use interactive flow |
| AADSTS700084 | Refresh token expired | Re-authenticate user |
| 403 on Graph /me | Missing User.Read scope | Add User.Read to scopes |

## Reference

- GitHub API: https://docs.github.com/en/rest
- GitHub OAuth: https://docs.github.com/en/apps/oauth-apps
- Microsoft Graph permissions: https://learn.microsoft.com/en-us/graph/permissions-reference
- AADSTS error codes: https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes