Skip to content

Commit d418398

Browse files
feat(skills): add constructive-oauth skill
OAuth/SSO configuration and identity providers management: - Third-party provider setup (GitHub, Google, Apple) - Platform vs tenant URL patterns - REST API endpoints with subdomain context - Auth settings and redirect behavior - Common issues and debugging Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 370054c commit d418398

2 files changed

Lines changed: 353 additions & 0 deletions

File tree

3.39 KB
Binary file not shown.
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
---
2+
name: constructive-oauth
3+
description: "OAuth/SSO configuration and Identity Providers management — configure social login, manage identity providers, debug OAuth flows. Use when asked to 'configure OAuth', 'setup SSO', 'identity provider', 'GitHub login', 'social login', 'OAuth not working', or when working with authentication providers."
4+
metadata:
5+
author: constructive-io
6+
version: "1.0.0"
7+
---
8+
9+
# Constructive OAuth
10+
11+
OAuth/SSO configuration and Identity Providers management.
12+
13+
## When to Apply
14+
15+
Use this skill when:
16+
- Configuring OAuth identity providers (GitHub, Google, Apple, etc.)
17+
- Debugging OAuth login flow (PROVIDER_NOT_CONFIGURED, EMAIL_NOT_VERIFIED)
18+
- Managing auth settings (oauthEnabled, allowIdentitySignIn, etc.)
19+
- Understanding identity providers and membership relationships
20+
21+
## Architecture Overview
22+
23+
```
24+
Identity Providers (App-level)
25+
├── github, google, apple... (social login)
26+
└── Shared across all users
27+
```
28+
29+
**Current Design**: Identity providers are configured at app-level, shared by all users.
30+
31+
## Third-Party Provider Setup
32+
33+
### Callback URL Format
34+
35+
```
36+
Platform: https://auth.your-app.com/auth/callback/{provider}
37+
Tenant: https://auth-{tenant-slug}.your-app.com/auth/callback/{provider}
38+
```
39+
40+
**Platform vs Tenant**: Each has its own OAuth App registration with matching callback URL.
41+
42+
### GitHub OAuth App
43+
44+
1. Go to **GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**
45+
46+
2. Configure:
47+
48+
**Platform**:
49+
| Field | Value |
50+
|-------|-------|
51+
| Application name | Your App Name |
52+
| Homepage URL | `https://your-app.com` |
53+
| Authorization callback URL | `https://auth.your-app.com/auth/callback/github` |
54+
55+
**Tenant**:
56+
| Field | Value |
57+
|-------|-------|
58+
| Application name | Your App Name - {Tenant} |
59+
| Homepage URL | `https://{tenant-slug}.your-app.com` |
60+
| Authorization callback URL | `https://auth-{tenant-slug}.your-app.com/auth/callback/github` |
61+
62+
3. Copy **Client ID** and generate **Client Secret**
63+
64+
### Google OAuth
65+
66+
1. Go to **Google Cloud Console → APIs & Services → Credentials → Create OAuth Client ID**
67+
68+
2. Configure:
69+
70+
**Platform**:
71+
| Field | Value |
72+
|-------|-------|
73+
| Application type | Web application |
74+
| Authorized redirect URIs | `https://auth.your-app.com/auth/callback/google` |
75+
76+
**Tenant**:
77+
| Field | Value |
78+
|-------|-------|
79+
| Application type | Web application |
80+
| Authorized redirect URIs | `https://auth-{tenant-slug}.your-app.com/auth/callback/google` |
81+
82+
3. Copy **Client ID** and **Client Secret**
83+
84+
### Apple Sign In
85+
86+
1. Go to **Apple Developer → Certificates, Identifiers & Profiles → Identifiers**
87+
88+
2. Create App ID with Sign In with Apple capability
89+
90+
3. Create Services ID:
91+
92+
**Platform**:
93+
| Field | Value |
94+
|-------|-------|
95+
| Return URLs | `https://auth.your-app.com/auth/callback/apple` |
96+
97+
**Tenant**:
98+
| Field | Value |
99+
|-------|-------|
100+
| Return URLs | `https://auth-{tenant-slug}.your-app.com/auth/callback/apple` |
101+
102+
### URL Patterns
103+
104+
OAuth flow uses the `auth` subdomain from `services_public.domains`:
105+
106+
```
107+
Database schema:
108+
domain: "localhost" | "your-app.com"
109+
subdomain: "auth" | "auth-{tenant-slug}" | null
110+
```
111+
112+
**Start OAuth Flow** (user clicks "Login with GitHub"):
113+
```
114+
https://{subdomain}.{domain}/auth/oauth/{provider_slug}?redirect_uri={success_url}
115+
116+
Platform: https://auth.your-app.com/auth/oauth/github?redirect_uri=/dashboard
117+
Tenant: https://auth-{tenant-slug}.your-app.com/auth/oauth/github?redirect_uri=/dashboard
118+
Local: https://auth.localhost:3000/auth/oauth/github?redirect_uri=/dashboard
119+
```
120+
121+
Query parameters:
122+
- `redirect_uri`: URL to redirect after successful login (default: `/`)
123+
124+
**Callback URL** (configure in provider's developer console):
125+
```
126+
https://{subdomain}.{domain}/auth/callback/{provider_slug}
127+
128+
Platform: https://auth.your-app.com/auth/callback/github
129+
Tenant: https://auth-{tenant-slug}.your-app.com/auth/callback/github
130+
Local: https://auth.localhost:3000/auth/callback/github
131+
```
132+
133+
**Note**: For tenant-specific subdomains, you may need to register a wildcard callback URL or multiple callback URLs in the provider's console.
134+
135+
## REST API Endpoints
136+
137+
API endpoints are accessed via the `auth` subdomain. The subdomain determines which tenant database to operate on:
138+
139+
```
140+
Base URL: https://{auth-subdomain}.{domain}
141+
142+
Platform: https://auth.your-app.com
143+
Tenant: https://auth-{tenant-slug}.your-app.com
144+
Local: https://auth.localhost:3000
145+
```
146+
147+
### Identity Providers
148+
149+
```
150+
GET {base}/identity-providers # List all providers
151+
GET {base}/identity-providers/:slug # Get single provider
152+
PATCH {base}/identity-providers/:slug # Update provider config
153+
POST {base}/identity-providers/:slug/rotate-secret # Rotate client secret
154+
```
155+
156+
### Auth Settings
157+
158+
```
159+
GET {base}/app-settings-auth # Get auth settings
160+
PATCH {base}/app-settings-auth # Update auth settings
161+
```
162+
163+
**Example** (local development):
164+
```http
165+
GET https://auth.localhost:3000/identity-providers
166+
GET https://auth.localhost:3000/app-settings-auth
167+
```
168+
169+
## Configuration Flow
170+
171+
### 1. Create Identity Provider
172+
173+
```http
174+
POST /identity-providers
175+
Content-Type: application/json
176+
177+
{
178+
"slug": "github",
179+
"kind": "oauth2",
180+
"displayName": "GitHub",
181+
"enabled": true,
182+
"clientId": "<GITHUB_CLIENT_ID>",
183+
"pkceEnabled": true
184+
}
185+
```
186+
187+
### 2. Set Client Secret
188+
189+
```http
190+
POST /identity-providers/github/rotate-secret
191+
Content-Type: application/json
192+
193+
{
194+
"clientSecret": "<GITHUB_CLIENT_SECRET>"
195+
}
196+
```
197+
198+
### 3. Configure Auth Settings
199+
200+
```http
201+
PATCH /app-settings-auth
202+
Content-Type: application/json
203+
204+
{
205+
"oauthEnabled": true,
206+
"oauthRequireVerifiedEmail": false,
207+
"allowIdentitySignIn": true,
208+
"allowIdentitySignUp": true
209+
}
210+
```
211+
212+
### 4. Test Login
213+
214+
```
215+
1. Start: GET https://auth.localhost:3000/auth/oauth/github
216+
2. Redirect: → GitHub authorization page
217+
3. Callback: → https://auth.localhost:3000/auth/callback/github
218+
4. Result: → Session cookie set, user logged in
219+
```
220+
221+
## Auth Settings Reference
222+
223+
| Field | Type | Description |
224+
|-------|------|-------------|
225+
| `oauthEnabled` | boolean | Enable OAuth login globally |
226+
| `oauthRequireVerifiedEmail` | boolean | Require verified email from IdP |
227+
| `allowIdentitySignIn` | boolean | Allow existing users to sign in via OAuth |
228+
| `allowIdentitySignUp` | boolean | Allow new user registration via OAuth |
229+
| `oauthStateMaxAge` | interval | OAuth state token expiry |
230+
| `oauthErrorRedirectPath` | string | Redirect path on OAuth error (default: `/auth/error`) |
231+
232+
### Redirect Behavior
233+
234+
| Scenario | Redirect To |
235+
|----------|-------------|
236+
| Success | `redirect_uri` query parameter (default: `/`) |
237+
| Error | `oauthErrorRedirectPath` setting (default: `/auth/error`) |
238+
239+
**Configure error redirect**:
240+
```http
241+
PATCH https://auth.localhost:3000/app-settings-auth
242+
Content-Type: application/json
243+
244+
{
245+
"oauthErrorRedirectPath": "/login?error=oauth"
246+
}
247+
```
248+
249+
Error redirect includes query params: `?error={code}&provider={slug}&error_description={msg}`
250+
251+
Example error redirect:
252+
```
253+
/login?error=oauth&error=EMAIL_NOT_VERIFIED&provider=github&error_description=Email+not+verified
254+
```
255+
256+
## Common Issues
257+
258+
### PROVIDER_NOT_CONFIGURED
259+
260+
**Cause**: Provider not configured or cache not refreshed
261+
262+
**Debug**:
263+
```sql
264+
SELECT slug, enabled, client_id, client_secret_id
265+
FROM <private_schema>.identity_providers
266+
WHERE slug = 'github';
267+
```
268+
269+
**Solution**:
270+
- Ensure both `client_id` and `client_secret_id` are not NULL
271+
- Wait 5 minutes for cache expiry, or restart GraphQL server
272+
273+
### EMAIL_NOT_VERIFIED
274+
275+
**Cause**: `oauth_require_verified_email = true` but user email not verified
276+
277+
**Solution**:
278+
```http
279+
PATCH /app-settings-auth
280+
Content-Type: application/json
281+
282+
{ "oauthRequireVerifiedEmail": false }
283+
```
284+
285+
### Cache Not Refreshing
286+
287+
**Key Code** (`create-loader.ts`):
288+
```typescript
289+
const cache = new LRUCache({
290+
ttl: opts.ttlMs ?? DEFAULT_TTL_MS,
291+
updateAgeOnGet: false, // Important: false = TTL from first set
292+
});
293+
```
294+
295+
- `updateAgeOnGet: true` → TTL resets on every read, never expires
296+
- `updateAgeOnGet: false` → TTL starts from first set, expires after 5 min
297+
298+
**Identity providers loader TTL**: 5 minutes (`ttlMs: 5 * 60_000`)
299+
300+
## Key Files
301+
302+
| File | Description |
303+
|------|-------------|
304+
| `graphql/server/src/middleware/identity-providers.ts` | REST API middleware |
305+
| `graphql/server/src/middleware/app-settings-auth.ts` | Auth settings middleware |
306+
| `graphql/server/src/middleware/oauth.ts` | OAuth callback handler |
307+
| `packages/express-context/src/loaders/identity-providers.ts` | Provider loader |
308+
| `packages/express-context/src/loaders/create-loader.ts` | Loader factory |
309+
| `constructive-db/.../identity_providers_module.sql` | DB schema generator |
310+
311+
## Known Issues
312+
313+
### rotate_identity_provider_platform_secret Function Bug
314+
315+
**Problem**: INSERT to platform_secrets missing `database_id` column
316+
317+
**Workaround**: Use direct SQL to insert secret
318+
319+
```sql
320+
INSERT INTO constructive_store_private.platform_secrets
321+
(database_id, namespace_id, algo, key_id, value)
322+
VALUES (
323+
'<DATABASE_ID>',
324+
'<NAMESPACE_ID>',
325+
'pgp',
326+
'<KEY_ID>',
327+
pgp_sym_encrypt(encode('<CLIENT_SECRET>', 'hex'), '<KEY_ID>')
328+
);
329+
330+
UPDATE <private_schema>.identity_providers
331+
SET client_secret_id = '<SECRET_ID>'
332+
WHERE slug = 'github';
333+
```
334+
335+
## Login Flow
336+
337+
```
338+
OAuth Login
339+
340+
sign_in_identity / sign_up_identity
341+
342+
Create user (users table)
343+
344+
Create app_membership (membership_type=1)
345+
346+
Session cookie set → User logged in
347+
```
348+
349+
## Cross-References
350+
351+
- **Authentication basics:** [`constructive-auth`](../constructive-auth/SKILL.md)
352+
- **Permissions & RLS:** [`constructive-security`](../constructive-security/SKILL.md)
353+
- **Multi-tenant entities:** [`constructive-entities`](../constructive-entities/SKILL.md)

0 commit comments

Comments
 (0)