From a51de7f4a0bc3b3b68957862c77d18cc0a003df9 Mon Sep 17 00:00:00 2001 From: Lucas Jiang <2862605953@qq.com> Date: Tue, 12 May 2026 17:14:13 +0800 Subject: [PATCH 1/3] docs(skills): add constructive-oauth skill Documents OAuth identity sign-in with cross-origin token exchange: - Quick start guide for frontend integration - Identity provider configuration (GitHub, Google) - signInCrossOrigin mutation usage - Multi-tenant support - Troubleshooting guide Co-Authored-By: Claude Opus 4.5 --- .agents/skills/constructive-oauth/SKILL.md | 180 ++++++++++++++++++ .../references/troubleshooting.md | 129 +++++++++++++ 2 files changed, 309 insertions(+) create mode 100644 .agents/skills/constructive-oauth/SKILL.md create mode 100644 .agents/skills/constructive-oauth/references/troubleshooting.md diff --git a/.agents/skills/constructive-oauth/SKILL.md b/.agents/skills/constructive-oauth/SKILL.md new file mode 100644 index 0000000..8fb28f3 --- /dev/null +++ b/.agents/skills/constructive-oauth/SKILL.md @@ -0,0 +1,180 @@ +# Constructive OAuth + +OAuth identity sign-in with cross-origin token exchange for Constructive platform. + +## Features + +| Feature | Status | +|---------|--------| +| GitHub OAuth | ✅ Ready | +| Google OAuth | ✅ Ready | +| Apple OAuth | ✅ Ready | +| Cross-origin token exchange | ✅ Ready | +| Multi-tenant support | ✅ Ready | + +## Architecture + +``` +┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ Frontend │────▶│ Auth Server │────▶│ OAuth │ +│ (SPA/App) │ │ (Express) │ │ Provider │ +└─────────────┘ └──────────────────┘ └──────────────┘ + │ │ + │ signInCrossOrigin │ sign_in_identity (DB) + ▼ ▼ +┌─────────────┐ ┌──────────────────┐ +│ API │◀────│ PostgreSQL │ +│ Server │ │ (sessions) │ +└─────────────┘ └──────────────────┘ +``` + +## Quick Start + +### 1. Redirect to OAuth + +```typescript +const authEndpoint = 'http://auth.localhost:3000'; +const provider = 'github'; +const callbackUrl = encodeURIComponent(window.location.origin + '/auth/callback'); + +localStorage.setItem('oauth_auth_endpoint', authEndpoint); +window.location.href = `${authEndpoint}/auth/${provider}?redirect_uri=${callbackUrl}`; +``` + +### 2. Handle Callback + +```typescript +const params = new URLSearchParams(window.location.search); +const token = params.get('token'); +const error = params.get('error'); + +if (error) { + console.error('OAuth failed:', error); + return; +} +``` + +### 3. Exchange Token + +```typescript +const authEndpoint = localStorage.getItem('oauth_auth_endpoint'); + +const response = await fetch(`${authEndpoint}/graphql`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: ` + mutation SignInCrossOrigin($input: SignInCrossOriginInput!) { + signInCrossOrigin(input: $input) { + result { + id + userId + accessToken + accessTokenExpiresAt + isVerified + totpEnabled + } + } + } + `, + variables: { + input: { token, credentialKind: 'bearer' } + } + }) +}); + +const { accessToken, userId } = (await response.json()).data.signInCrossOrigin.result; +``` + +### 4. Use Access Token + +```typescript +fetch('http://api.localhost:3000/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}` + }, + body: JSON.stringify({ query: '{ currentUserId }' }) +}); +``` + +## Configure Identity Provider + +### 1. Create OAuth App + +**GitHub:** https://github.com/settings/developers +- Callback URL: `http://auth.localhost:3000/auth/github/callback` + +**Google:** https://console.cloud.google.com/apis/credentials +- Redirect URI: `http://auth.localhost:3000/auth/google/callback` + +### 2. Configure Database + +```sql +-- Set client_id and enable +UPDATE "{schema}-auth-private".identity_providers +SET client_id = 'your-client-id', enabled = true +WHERE slug = 'github'; + +-- Set client secret +SELECT "{schema}-auth-private".rotate_identity_provider_secret( + 'provider-uuid', + 'your-client-secret' +); + +-- Enable identity sign-in +UPDATE "{schema}-auth-private".app_settings_auth +SET allow_identity_sign_in = true, + allow_identity_sign_up = true; +``` + +## Query Available Providers + +No authentication required: + +```graphql +query { + identityProviders { + nodes { + slug + kind + displayName + enabled + } + } +} +``` + +## Multi-Tenant + +Each tenant has its own auth endpoint and providers: + +```typescript +const tenantAuthEndpoint = `http://auth-${tenantSubdomain}.localhost:3000`; +``` + +Find tenant endpoint: +```sql +SELECT dom.subdomain, dom.domain +FROM services_public.domains dom +JOIN metaschema_public.database d ON dom.database_id = d.id +WHERE dom.subdomain LIKE 'auth-%'; +``` + +## References + +- `references/troubleshooting.md` - Common issues and fixes + +## Key Files + +| File | Purpose | +|------|---------| +| `graphql/server/src/middleware/oauth.ts` | OAuth callback handling | +| `graphql/server/src/middleware/auth.ts` | Authentication middleware | +| `packages/oauth/src/index.ts` | OAuth provider configuration | + +## Related + +- Issue #735 - Server-Side Auth Implementation Plan +- `constructive-cookie-csrf` - Cookie auth and CSRF (partial) diff --git a/.agents/skills/constructive-oauth/references/troubleshooting.md b/.agents/skills/constructive-oauth/references/troubleshooting.md new file mode 100644 index 0000000..d3fcd1e --- /dev/null +++ b/.agents/skills/constructive-oauth/references/troubleshooting.md @@ -0,0 +1,129 @@ +# OAuth Troubleshooting + +## OAuth Callback Errors + +### fetch failed + +**Symptom:** `CALLBACK_FAILED` with message `fetch failed` + +**Cause:** HTTP_PROXY/HTTPS_PROXY environment variables interfere with Node.js fetch. + +**Fix:** +```bash +HTTP_PROXY="" HTTPS_PROXY="" NO_PROXY="*" pnpm start +``` + +### PROVIDER_NOT_CONFIGURED + +**Symptom:** OAuth redirect fails immediately + +**Check:** +```sql +SELECT slug, client_id, client_secret_id, enabled +FROM "{schema}-auth-private".identity_providers +WHERE slug = 'github'; +``` + +**Requirements:** +- `client_id` set +- `client_secret_id` set (use `rotate_identity_provider_secret`) +- `enabled = true` + +### IDENTITY_SIGN_IN_DISABLED + +**Symptom:** OAuth succeeds but returns error + +**Fix:** +```sql +UPDATE "{schema}-auth-private".app_settings_auth +SET allow_identity_sign_in = true, + allow_identity_sign_up = true; +``` + +### GitHub "redirect_uri not associated" + +**Cause:** OAuth App callback URL mismatch + +**Fix:** Update GitHub OAuth App callback URL to: +``` +http://auth.localhost:3000/auth/github/callback +``` + +For tenants: +``` +http://auth-{subdomain}.localhost:3000/auth/github/callback +``` + +## Token Exchange Errors + +### Invalid token or token expired + +**Causes:** +1. Token already used (one-time only) +2. Token expired (5 min TTL) +3. Wrong endpoint (must match issuer) +4. JWT claims not persisted (server issue) + +**Debug:** +```sql +-- Check recent sessions +SELECT id, user_id, created_at +FROM "{schema}-auth-private".sessions +ORDER BY created_at DESC LIMIT 5; +``` + +### JWT Claims Not Persisted + +**Cause:** `set_config(..., true)` loses settings with connection pooling. + +**Fix in oauth.ts:** +```typescript +// WRONG +await pool.query(`SELECT set_config('jwt.claims.user_agent', $1, true)`, [ua]); + +// CORRECT - use dedicated client with session-level config +const client = await pool.connect(); +try { + await client.query(`SELECT set_config('jwt.claims.user_agent', $1, false)`, [ua]); + // use same client for sign_in_identity +} finally { + client.release(); +} +``` + +## Database Queries + +### Find Tenant Schema + +```sql +SELECT schema_name FROM information_schema.schemata +WHERE schema_name LIKE '%auth-private'; +``` + +### Find Tenant Auth Endpoint + +```sql +SELECT d.name, dom.subdomain, dom.domain +FROM services_public.domains dom +JOIN metaschema_public.database d ON dom.database_id = d.id +WHERE dom.subdomain LIKE 'auth-%'; +``` + +### Verify Provider Config + +```sql +SELECT id, slug, client_id, client_secret_id, enabled +FROM "{schema}-auth-private".identity_providers; +``` + +## Server Logs + +```bash +# Hub environment +pnpm log public-server + +# Check log files +cat .local/logs/public-server.log | tail -100 +``` + +Look for `[oauth]`, `[auth]`, or `[server]` prefixed messages. From f8df96bd2434092c1835c093747d19b0a734be84 Mon Sep 17 00:00:00 2001 From: Lucas Jiang <2862605953@qq.com> Date: Tue, 12 May 2026 17:24:45 +0800 Subject: [PATCH 2/3] docs(oauth): add same-origin vs cross-origin comparison Explains when to use Bearer token (cross-origin) vs cookie auth (same-origin), including pros/cons for each approach. Co-Authored-By: Claude Opus 4.5 --- .agents/skills/constructive-oauth/SKILL.md | 40 +++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/.agents/skills/constructive-oauth/SKILL.md b/.agents/skills/constructive-oauth/SKILL.md index 8fb28f3..c8d4f3f 100644 --- a/.agents/skills/constructive-oauth/SKILL.md +++ b/.agents/skills/constructive-oauth/SKILL.md @@ -28,7 +28,45 @@ OAuth identity sign-in with cross-origin token exchange for Constructive platfor └─────────────┘ └──────────────────┘ ``` -## Quick Start +## Same-Origin vs Cross-Origin + +OAuth flow supports two credential modes depending on your deployment: + +| Mode | When to Use | Credential | +|------|-------------|------------| +| **Cross-Origin** | Frontend and auth server on different domains | Bearer token (Authorization header) | +| **Same-Origin** | Frontend and auth server on same domain | Cookie (HttpOnly session) | + +### Cross-Origin (Bearer Token) + +Use when frontend (`app.example.com`) and auth server (`auth.example.com`) are on different origins: + +1. OAuth callback returns a one-time `token` in URL +2. Frontend exchanges token via `signInCrossOrigin` mutation +3. Response contains `accessToken` for Bearer authentication +4. Store token in localStorage/sessionStorage +5. Include `Authorization: Bearer ` header on all API requests + +**Pros:** Works across any origin, no CSRF concerns +**Cons:** Token management, must handle expiry/refresh + +### Same-Origin (Cookie) + +Use when frontend and auth server share the same origin or are on subdomains with shared cookies: + +1. OAuth callback sets session cookie directly (HttpOnly) +2. No token exchange needed +3. Cookies sent automatically with `credentials: 'include'` +4. CSRF protection required (see `constructive-cookie-csrf`) + +**Pros:** Simpler flow, automatic credential handling +**Cons:** Requires CSRF protection, same-origin constraints + +**Note:** Cookie auth is partially implemented (see issue #749). Use cross-origin Bearer token flow for now. + +--- + +## Quick Start (Cross-Origin) ### 1. Redirect to OAuth From 77ed6bf188bc57418b01dfb15ad2dd7201caf08a Mon Sep 17 00:00:00 2001 From: Lucas Jiang <2862605953@qq.com> Date: Tue, 12 May 2026 23:10:30 +0800 Subject: [PATCH 3/3] docs(oauth): add OAUTH_SECRET configuration OAUTH_SECRET is required in all environments. Server throws error if not configured. Co-Authored-By: Claude Opus 4.5 --- .agents/skills/constructive-oauth/SKILL.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.agents/skills/constructive-oauth/SKILL.md b/.agents/skills/constructive-oauth/SKILL.md index c8d4f3f..0bf39fc 100644 --- a/.agents/skills/constructive-oauth/SKILL.md +++ b/.agents/skills/constructive-oauth/SKILL.md @@ -137,6 +137,26 @@ fetch('http://api.localhost:3000/graphql', { }); ``` +## Server Configuration + +### Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `OAUTH_SECRET` | **Yes** | Secret for signing OAuth state (CSRF protection) | + +**Required in all environments.** Server throws error if not configured. + +```bash +# Generate a secure secret +openssl rand -base64 32 + +# Set in environment +export OAUTH_SECRET="your-generated-secret" +``` + +--- + ## Configure Identity Provider ### 1. Create OAuth App