Skip to content

Commit 873c195

Browse files
committed
docs: add identity linking flow diagrams and data model to constructive-auth skill
- Mermaid flowcharts for sign_in_identity and link_identity decision trees - Sequence diagram showing end-to-end account collision → linking flow - ER diagram showing auth data model relationships - Error code reference table - Toggle documentation (allow_link_by_email, enforce_primary_auth_method) - SDK usage examples Ref: constructive-planning#1016
1 parent ac41fa6 commit 873c195

3 files changed

Lines changed: 227 additions & 1 deletion

File tree

3.14 KB
Binary file not shown.

.agents/skills/constructive-auth/SKILL.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: constructive-auth
3-
description: "Identity, login, sessions, MFA, devices — auth flow (sign-up/sign-in/JWT), auth settings (MFA, anonymous sessions, CAPTCHA, cookie auth, rate limits), device settings (tracking, trusted devices, approval gate), and service-level auth config. Use when asked to 'sign up', 'sign in', 'auth flow', 'MFA', 'magic link', 'passkeys', 'device approval', 'trusted devices', 'session management', 'cookie auth', 'anonymous sessions', 'CAPTCHA', 'rate limits', 'bootstrap user', 'JWT', or when working with authentication."
3+
description: "Identity, login, sessions, MFA, devices, identity linking — auth flow (sign-up/sign-in/JWT), auth settings (MFA, anonymous sessions, CAPTCHA, cookie auth, rate limits), device settings (tracking, trusted devices, approval gate), identity linking (link_identity, account collision resolution, allow_link_by_email, primary auth method), and service-level auth config. Use when asked to 'sign up', 'sign in', 'auth flow', 'MFA', 'magic link', 'passkeys', 'device approval', 'trusted devices', 'session management', 'cookie auth', 'anonymous sessions', 'CAPTCHA', 'rate limits', 'bootstrap user', 'JWT', 'link identity', 'identity linking', 'account collision', 'allow_link_by_email', 'primary auth method', 'connected accounts', or when working with authentication."
44
metadata:
55
author: constructive-io
66
version: "1.0.0"
@@ -19,6 +19,8 @@ Use this skill when:
1919
- Configuring session behavior (cookie auth, cross-origin tokens, expiry)
2020
- Setting up rate limiting and CAPTCHA
2121
- Understanding the JWT structure and auth endpoints
22+
- Linking multiple identity providers to a single account
23+
- Handling account collision (OAuth email matches existing user)
2224

2325
## Auth Flow
2426

@@ -93,6 +95,16 @@ See [service-settings.md](./references/service-settings.md) for the full service
9395
| Passkeys (WebAuthn) | `webauthn_credentials_module` + `webauthn_auth_module` |
9496
| Phone / SMS | `phone_numbers_module` |
9597

98+
## Identity Linking
99+
100+
Users can **link** multiple auth providers (Google, Facebook, password, phone) to one account. One method is **primary** (signs in by default); others are linked for identity purposes.
101+
102+
Two toggles control the system:
103+
- **`allow_link_by_email`** (per provider) — offer linking when OAuth email matches an existing account
104+
- **`enforce_primary_auth_method`** (app-wide) — lock sign-in to the user's primary method
105+
106+
See [identity-linking.md](./references/identity-linking.md) for the full flow diagrams, error codes, data model, and SDK usage.
107+
96108
## References
97109

98110
| File | Content |
@@ -101,6 +113,7 @@ See [service-settings.md](./references/service-settings.md) for the full service
101113
| [auth-settings.md](./references/auth-settings.md) | MFA, anonymous sessions, CAPTCHA, cookie auth, rate limits |
102114
| [device-settings.md](./references/device-settings.md) | Device tracking, trusted devices, approval gate |
103115
| [service-settings.md](./references/service-settings.md) | CORS, database, API, RLS, WebAuthn, pubkey settings |
116+
| [identity-linking.md](./references/identity-linking.md) | Identity linking flow diagrams, account collision resolution, toggles, data model |
104117

105118
## Cross-References
106119

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# Identity Linking & Account Collision Resolution
2+
3+
## Overview
4+
5+
Identity linking lets users attach multiple auth providers (Google, Facebook, GitHub, phone, password) to a single account. One method is **primary** (creates sessions), the rest are **linked** (connected but don't sign in by default).
6+
7+
Two toggles control the system:
8+
9+
| Toggle | Scope | Default | Effect |
10+
|--------|-------|---------|--------|
11+
| `allow_link_by_email` | Per identity provider | `false` | When an unknown OAuth identity's email matches an existing user, offer linking instead of failing |
12+
| `enforce_primary_auth_method` | App-wide (`auth_settings`) | `true` | Only the user's primary auth method can create sessions |
13+
14+
## Sign-In Identity Flow (OAuth Callback)
15+
16+
When a user signs in with an OAuth provider, `sign_in_identity(service, identifier, details)` runs this decision tree:
17+
18+
```mermaid
19+
flowchart TD
20+
A[OAuth callback] --> B{Provider enabled?}
21+
B -- No --> C[❌ IDENTITY_PROVIDER_NOT_CONFIGURED]
22+
B -- Yes --> D{Connected account exists<br/>for service + identifier?}
23+
D -- Yes --> E{enforce_primary_auth_method?}
24+
E -- "No (off)" --> F[✅ Sign in — create session]
25+
E -- "Yes (on)" --> G{Is identity the user's<br/>primary auth method?}
26+
G -- Yes --> F
27+
G -- No --> H[❌ PRIMARY_AUTH_METHOD_MISMATCH]
28+
D -- No --> I{allow_link_by_email<br/>on this provider?}
29+
I -- No --> J[❌ IDENTITY_ACCOUNT_NOT_FOUND]
30+
I -- Yes --> K{Email from OAuth details<br/>matches existing user?}
31+
K -- No --> J
32+
K -- Yes --> L[❌ IDENTITY_LINK_AVAILABLE]
33+
34+
style C fill:#fee,stroke:#c33
35+
style H fill:#fee,stroke:#c33
36+
style J fill:#fee,stroke:#c33
37+
style L fill:#fef,stroke:#93c
38+
style F fill:#efe,stroke:#3c3
39+
```
40+
41+
### Error code summary
42+
43+
| Error | Meaning | Frontend action |
44+
|-------|---------|----------------|
45+
| `IDENTITY_PROVIDER_NOT_CONFIGURED` | Provider slug doesn't exist or is disabled | Show "provider unavailable" |
46+
| `PRIMARY_AUTH_METHOD_MISMATCH` | User's primary method is different (e.g. password) | Prompt to sign in with primary method |
47+
| `IDENTITY_ACCOUNT_NOT_FOUND` | No account linked and no email match (or linking disabled) | Redirect to sign-up |
48+
| `IDENTITY_LINK_AVAILABLE` | Email matches an existing account — linking is possible | Prompt: "Sign in with your existing method to link this provider" |
49+
50+
## Link Identity Flow (Attach Provider to Existing Account)
51+
52+
After the frontend receives `IDENTITY_LINK_AVAILABLE`, the user signs in with their existing method, then calls `link_identity(service, identifier, details)`:
53+
54+
```mermaid
55+
flowchart TD
56+
A[link_identity called] --> B{User authenticated?<br/>current_user_id IS NOT NULL}
57+
B -- No --> C[❌ NOT_AUTHENTICATED]
58+
B -- Yes --> D{Step-up auth valid?<br/>recent password/MFA verification}
59+
D -- No --> E[❌ STEP_UP_REQUIRED]
60+
D -- Yes --> F{Provider enabled?}
61+
F -- No --> G[❌ IDENTITY_PROVIDER_NOT_CONFIGURED]
62+
F -- Yes --> H{service + identifier<br/>already linked to any user?}
63+
H -- Yes --> I[❌ IDENTITY_ALREADY_LINKED]
64+
H -- No --> J[INSERT connected_account<br/>owner_id = current user]
65+
J --> K[✅ return true]
66+
67+
style C fill:#fee,stroke:#c33
68+
style E fill:#fee,stroke:#c33
69+
style G fill:#fee,stroke:#c33
70+
style I fill:#fee,stroke:#c33
71+
style K fill:#efe,stroke:#3c3
72+
```
73+
74+
## End-to-End Linking Scenario
75+
76+
```mermaid
77+
sequenceDiagram
78+
participant U as User
79+
participant FE as Frontend
80+
participant API as Auth API
81+
participant DB as Database
82+
83+
Note over U: User has account via email+password.<br/>Tries to sign in with Facebook.
84+
85+
U->>FE: Click "Sign in with Facebook"
86+
FE->>API: sign_in_identity('facebook', 'fb-123', details)
87+
API->>DB: Lookup connected_accounts(facebook, fb-123)
88+
DB-->>API: NOT FOUND
89+
API->>DB: Lookup emails WHERE email = 'user@example.com'
90+
DB-->>API: Found — owner_id = user-456
91+
API->>DB: Lookup identity_providers WHERE slug = 'facebook'
92+
DB-->>API: allow_link_by_email = true
93+
API-->>FE: ❌ IDENTITY_LINK_AVAILABLE
94+
95+
FE->>U: "Account exists. Sign in with password to link Facebook."
96+
U->>FE: Enter password
97+
FE->>API: sign_in('user@example.com', password)
98+
API-->>FE: ✅ accessToken
99+
100+
FE->>API: link_identity('facebook', 'fb-123', details)
101+
Note over API: Checks: authenticated ✓<br/>step-up (recent password) ✓<br/>provider enabled ✓<br/>not already linked ✓
102+
API->>DB: INSERT connected_accounts(user-456, facebook, fb-123)
103+
API-->>FE: ✅ true
104+
105+
Note over U: Future Facebook logins work via sign_in_identity
106+
```
107+
108+
## Data Model
109+
110+
```mermaid
111+
erDiagram
112+
users ||--o{ emails : "has many"
113+
users ||--o{ connected_accounts : "has many (linked providers)"
114+
users ||--o{ phone_numbers : "has many"
115+
users ||--o{ sessions : "has many"
116+
users ||--|| user_settings_security : "has one"
117+
identity_providers ||--o{ connected_accounts : "provider config"
118+
app_settings_auth ||--|| app_settings_auth : "singleton"
119+
120+
users {
121+
uuid id PK
122+
}
123+
emails {
124+
uuid id PK
125+
uuid owner_id FK
126+
text email
127+
boolean is_primary
128+
boolean is_verified
129+
}
130+
connected_accounts {
131+
uuid id PK
132+
uuid owner_id FK
133+
text service "provider slug"
134+
text identifier "provider user ID"
135+
jsonb details "OAuth profile data"
136+
boolean is_verified
137+
}
138+
phone_numbers {
139+
uuid id PK
140+
uuid owner_id FK
141+
text phone
142+
boolean is_verified
143+
}
144+
sessions {
145+
uuid id PK
146+
uuid user_id FK
147+
text auth_method "password|identity|sms|magic_link|webauthn"
148+
timestamptz last_password_verified "step-up window"
149+
}
150+
user_settings_security {
151+
uuid id PK
152+
uuid user_id FK
153+
text primary_auth_method "auto-set on sign-up"
154+
}
155+
identity_providers {
156+
uuid id PK
157+
text slug UK "google|facebook|github|..."
158+
text kind "oidc|oauth2|saml"
159+
boolean enabled
160+
boolean allow_link_by_email "toggle: offer linking on email match"
161+
}
162+
app_settings_auth {
163+
uuid id PK
164+
boolean enforce_primary_auth_method "toggle: lock to primary method"
165+
}
166+
```
167+
168+
## Configuration
169+
170+
### Enable linking for a provider
171+
172+
```sql
173+
-- Allow Google sign-ins to offer linking when email matches existing account
174+
UPDATE auth_private.identity_providers
175+
SET allow_link_by_email = true
176+
WHERE slug = 'google';
177+
```
178+
179+
### Disable primary auth enforcement (allow any linked method to sign in)
180+
181+
```sql
182+
UPDATE auth_private.app_settings_auth
183+
SET enforce_primary_auth_method = false;
184+
```
185+
186+
## SDK Usage
187+
188+
```typescript
189+
import { createClient as createAuthClient } from '@constructive-db/sdk/auth';
190+
191+
const auth = createAuthClient({ endpoint });
192+
193+
// 1. Try OAuth sign-in
194+
try {
195+
const result = await auth.mutation.signInIdentity({
196+
input: { service: 'facebook', identifier: fbUserId, details: fbProfile }
197+
}).execute();
198+
// Success — user is signed in
199+
} catch (err) {
200+
if (err.message.includes('IDENTITY_LINK_AVAILABLE')) {
201+
// 2. Prompt user to sign in with existing method
202+
const signIn = await auth.mutation.signIn({
203+
input: { email, password }
204+
}).execute();
205+
206+
// 3. Link the new provider
207+
await auth.mutation.linkIdentity({
208+
input: { service: 'facebook', identifier: fbUserId, details: fbProfile }
209+
}).execute();
210+
// Facebook is now linked — future logins work
211+
}
212+
}
213+
```

0 commit comments

Comments
 (0)