@edcalderon/auth 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +4 -4
- package/dist/AuthentikOidcClient.js +4 -2
- package/dist/authentik/callback.d.ts +71 -0
- package/dist/authentik/callback.js +163 -0
- package/dist/authentik/config.d.ts +53 -0
- package/dist/authentik/config.js +169 -0
- package/dist/authentik/index.d.ts +17 -0
- package/dist/authentik/index.js +22 -0
- package/dist/authentik/logout.d.ts +50 -0
- package/dist/authentik/logout.js +96 -0
- package/dist/authentik/provisioning.d.ts +124 -0
- package/dist/authentik/provisioning.js +342 -0
- package/dist/authentik/redirect.d.ts +20 -0
- package/dist/authentik/redirect.js +52 -0
- package/dist/authentik/relay.d.ts +48 -0
- package/dist/authentik/relay.js +146 -0
- package/dist/authentik/types.d.ts +264 -0
- package/dist/authentik/types.js +8 -0
- package/docs/authentik-integration-guide.md +286 -0
- package/docs/cig-reference-map.md +118 -0
- package/docs/nextjs-examples.md +784 -0
- package/docs/provisioning-model.md +416 -0
- package/docs/upgrade-migration.md +256 -0
- package/package.json +19 -1
- package/supabase/migrations/003_authentik_shadow_auth_users.sql +81 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# Provisioning Model
|
|
2
|
+
|
|
3
|
+
> **Package:** `@edcalderon/auth/authentik` (v1.4.0+)
|
|
4
|
+
> **Runtime:** Server-side only (requires `service_role` key)
|
|
5
|
+
|
|
6
|
+
This document describes the Authentik-to-Supabase user provisioning model — a first-class path for ensuring every Authentik-authenticated user exists in your application's local user store before they can access protected resources.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
1. [Overview](#overview)
|
|
13
|
+
2. [Three-Layer Identity Model](#three-layer-identity-model)
|
|
14
|
+
3. [Identity-First Match Strategy](#identity-first-match-strategy)
|
|
15
|
+
4. [Provisioning Adapter Interface](#provisioning-adapter-interface)
|
|
16
|
+
5. [SupabaseSyncAdapter](#supabasesyncadapter)
|
|
17
|
+
6. [Rollback Behavior](#rollback-behavior)
|
|
18
|
+
7. [SQL Migrations](#sql-migrations)
|
|
19
|
+
8. [Runtime Requirements](#runtime-requirements)
|
|
20
|
+
9. [Startup Validation](#startup-validation)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
The provisioning model is **fail-closed**: if user sync fails, the callback handler does not redirect the user into the protected app. This ensures no user can access the application without a corresponding local user record.
|
|
27
|
+
|
|
28
|
+
The `SupabaseSyncAdapter` is the production-grade adapter that implements the full CIG-proven sync flow:
|
|
29
|
+
|
|
30
|
+
1. Normalize the OIDC payload (lowercase email, resolve name from claims)
|
|
31
|
+
2. Ensure a shadow `auth.users` record exists (identity-first matching)
|
|
32
|
+
3. Call `upsert_oidc_user()` RPC to sync into `public.users`
|
|
33
|
+
4. Link the shadow auth user via `link_shadow_auth_user()` RPC
|
|
34
|
+
5. Roll back on failure (delete newly created shadow records)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Three-Layer Identity Model
|
|
39
|
+
|
|
40
|
+
The provisioning system uses a three-layer identity architecture:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
┌──────────────────────────────────────────────────────────┐
|
|
44
|
+
│ Layer 1: Authentik │
|
|
45
|
+
│ │
|
|
46
|
+
│ The upstream OIDC identity provider. Users authenticate │
|
|
47
|
+
│ here via social login (Google, GitHub, Discord, etc.) │
|
|
48
|
+
│ or direct Authentik credentials. │
|
|
49
|
+
│ │
|
|
50
|
+
│ Identity key: (sub, iss) │
|
|
51
|
+
│ ┌─────────────────────────────────────────────────┐ │
|
|
52
|
+
│ │ sub: "google-oauth2|12345" │ │
|
|
53
|
+
│ │ iss: "https://auth.example.com/application/o/…" │ │
|
|
54
|
+
│ │ email: "user@example.com" │ │
|
|
55
|
+
│ │ name: "Jane Doe" │ │
|
|
56
|
+
│ └─────────────────────────────────────────────────┘ │
|
|
57
|
+
└──────────────────────┬───────────────────────────────────┘
|
|
58
|
+
│
|
|
59
|
+
▼
|
|
60
|
+
┌──────────────────────────────────────────────────────────┐
|
|
61
|
+
│ Layer 2: public.users │
|
|
62
|
+
│ │
|
|
63
|
+
│ Application-owned user table. This is the canonical │
|
|
64
|
+
│ user record for your app's business logic, RLS │
|
|
65
|
+
│ policies, and authorization. │
|
|
66
|
+
│ │
|
|
67
|
+
│ Primary key: id (uuid) │
|
|
68
|
+
│ Unique key: (sub, iss) │
|
|
69
|
+
│ ┌─────────────────────────────────────────────────┐ │
|
|
70
|
+
│ │ id: "uuid-abc-123" │ │
|
|
71
|
+
│ │ sub: "google-oauth2|12345" │ │
|
|
72
|
+
│ │ iss: "https://auth.example.com/application/o/…" │ │
|
|
73
|
+
│ │ email: "user@example.com" │ │
|
|
74
|
+
│ │ shadow_auth_user_id: "uuid-def-456" (optional) │ │
|
|
75
|
+
│ └─────────────────────────────────────────────────┘ │
|
|
76
|
+
└──────────────────────┬───────────────────────────────────┘
|
|
77
|
+
│
|
|
78
|
+
▼
|
|
79
|
+
┌──────────────────────────────────────────────────────────┐
|
|
80
|
+
│ Layer 3: auth.users (Shadow — Optional) │
|
|
81
|
+
│ │
|
|
82
|
+
│ Supabase's built-in auth.users table. A "shadow" │
|
|
83
|
+
│ record mirrors the Authentik identity so that Supabase │
|
|
84
|
+
│ RLS policies, storage rules, and realtime subscriptions │
|
|
85
|
+
│ work out of the box. │
|
|
86
|
+
│ │
|
|
87
|
+
│ ┌─────────────────────────────────────────────────┐ │
|
|
88
|
+
│ │ id: "uuid-def-456" │ │
|
|
89
|
+
│ │ email: "user@example.com" │ │
|
|
90
|
+
│ │ app_metadata: { │ │
|
|
91
|
+
│ │ provider: "authentik", │ │
|
|
92
|
+
│ │ oidc_sub: "google-oauth2|12345", │ │
|
|
93
|
+
│ │ oidc_issuer: "https://auth.example.com/…" │ │
|
|
94
|
+
│ │ } │ │
|
|
95
|
+
│ └─────────────────────────────────────────────────┘ │
|
|
96
|
+
└──────────────────────────────────────────────────────────┘
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Why three layers?**
|
|
100
|
+
|
|
101
|
+
- **Layer 1 (Authentik)** handles authentication — "who are you?"
|
|
102
|
+
- **Layer 2 (`public.users`)** is your app's source of truth — "what can you do?"
|
|
103
|
+
- **Layer 3 (`auth.users` shadow)** enables Supabase-native features (RLS, storage, realtime) without requiring users to sign up through Supabase Auth directly.
|
|
104
|
+
|
|
105
|
+
The shadow layer is **optional** (controlled by `createShadowAuthUser` config, default: `true`). If your app does not use Supabase RLS or storage features, you can disable it.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Identity-First Match Strategy
|
|
110
|
+
|
|
111
|
+
The adapter uses a two-tier matching strategy when looking for existing user records:
|
|
112
|
+
|
|
113
|
+
### Priority 1: OIDC Identity Match (sub + iss)
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
app_metadata.auth_source === "authentik"
|
|
117
|
+
AND app_metadata.oidc_sub === payload.sub
|
|
118
|
+
AND app_metadata.oidc_issuer === payload.iss
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
This is the **primary** and **safest** match. The OIDC `sub` claim is immutable for a given user at a given issuer. It cannot be changed by the user.
|
|
122
|
+
|
|
123
|
+
### Priority 2: Email Fallback
|
|
124
|
+
|
|
125
|
+
If no identity match is found, the adapter falls back to matching by email address:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
user.email.toLowerCase() === payload.email.toLowerCase()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This handles the case where a shadow `auth.users` record already exists (e.g. from a previous Supabase Auth signup) but hasn't been linked to an Authentik identity yet. When matched by email, the adapter **updates** the record to include the OIDC identity markers.
|
|
132
|
+
|
|
133
|
+
### Why Identity-First?
|
|
134
|
+
|
|
135
|
+
Email-based matching alone is risky:
|
|
136
|
+
- Users can change their email at the upstream provider
|
|
137
|
+
- Different providers might have different emails for the same person
|
|
138
|
+
- Email conflicts could lead to unauthorized access
|
|
139
|
+
|
|
140
|
+
By matching on `(oidc_sub, oidc_issuer)` first, the adapter prevents these scenarios. Email fallback exists only as a migration path for pre-existing records.
|
|
141
|
+
|
|
142
|
+
### Pagination
|
|
143
|
+
|
|
144
|
+
The adapter paginates through **all** `auth.users` pages (1000 users per page) to ensure matches beyond the first page are not missed. This is critical for larger Supabase projects.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Provisioning Adapter Interface
|
|
149
|
+
|
|
150
|
+
All provisioning adapters implement the `ProvisioningAdapter` interface:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
interface ProvisioningAdapter {
|
|
154
|
+
sync(payload: ProvisioningPayload): Promise<ProvisioningResult>;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### ProvisioningPayload
|
|
159
|
+
|
|
160
|
+
The input to every adapter:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
interface ProvisioningPayload {
|
|
164
|
+
/** OIDC subject identifier. */
|
|
165
|
+
sub: string;
|
|
166
|
+
/** OIDC issuer URL. */
|
|
167
|
+
iss: string;
|
|
168
|
+
/** User email (normalized to lowercase). */
|
|
169
|
+
email: string;
|
|
170
|
+
/** Whether the email is verified by the OIDC provider. */
|
|
171
|
+
emailVerified?: boolean;
|
|
172
|
+
/** Display name. */
|
|
173
|
+
name?: string;
|
|
174
|
+
/** Avatar / profile picture URL. */
|
|
175
|
+
picture?: string;
|
|
176
|
+
/** Upstream social provider slug (e.g. "google", "github"). */
|
|
177
|
+
provider?: string;
|
|
178
|
+
/** The full set of OIDC claims as received from Authentik. */
|
|
179
|
+
rawClaims?: Record<string, unknown>;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### ProvisioningResult
|
|
184
|
+
|
|
185
|
+
The output from every adapter:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
interface ProvisioningResult {
|
|
189
|
+
/** Must be true for the callback to proceed with redirect. */
|
|
190
|
+
synced: boolean;
|
|
191
|
+
/** The app-level user ID (from public.users or equivalent). */
|
|
192
|
+
appUserId?: string;
|
|
193
|
+
/** Supabase auth.users ID when shadow-user mode is used. */
|
|
194
|
+
authUserId?: string;
|
|
195
|
+
/** Whether a new shadow auth.users row was created. */
|
|
196
|
+
authUserCreated?: boolean;
|
|
197
|
+
/** Whether an existing shadow auth.users row was updated. */
|
|
198
|
+
authUserUpdated?: boolean;
|
|
199
|
+
/** Human-readable diagnostic on failure. */
|
|
200
|
+
error?: string;
|
|
201
|
+
/** Machine-readable error code. */
|
|
202
|
+
errorCode?: string;
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The `synced` flag is the gating condition: the callback handler will **not** redirect the user unless `synced === true`.
|
|
207
|
+
|
|
208
|
+
### Built-in Adapters
|
|
209
|
+
|
|
210
|
+
| Adapter | Use Case |
|
|
211
|
+
|---------|----------|
|
|
212
|
+
| `NoopProvisioningAdapter` | No user sync needed — always returns `{ synced: true }` |
|
|
213
|
+
| `createProvisioningAdapter(fn)` | Custom sync logic via a plain function |
|
|
214
|
+
| `SupabaseSyncAdapter` | Full Authentik ↔ Supabase integrated sync |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## SupabaseSyncAdapter
|
|
219
|
+
|
|
220
|
+
The `SupabaseSyncAdapter` is the production-grade adapter for Authentik ↔ Supabase integration:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { createClient } from "@supabase/supabase-js";
|
|
224
|
+
import { createSupabaseSyncAdapter } from "@edcalderon/auth/authentik";
|
|
225
|
+
|
|
226
|
+
const supabase = createClient(
|
|
227
|
+
process.env.SUPABASE_URL!,
|
|
228
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const adapter = createSupabaseSyncAdapter(supabase, {
|
|
232
|
+
supabaseUrl: process.env.SUPABASE_URL!,
|
|
233
|
+
supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
234
|
+
// Optional overrides:
|
|
235
|
+
createShadowAuthUser: true, // default: true
|
|
236
|
+
rollbackOnFailure: true, // default: true
|
|
237
|
+
upsertRpcName: "upsert_oidc_user", // default
|
|
238
|
+
linkShadowRpcName: "link_shadow_auth_user", // default
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Configuration (SupabaseSyncConfig)
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
interface SupabaseSyncConfig {
|
|
246
|
+
/** Supabase project URL. */
|
|
247
|
+
supabaseUrl: string;
|
|
248
|
+
/** Supabase service_role key (server-side only). */
|
|
249
|
+
supabaseServiceRoleKey: string;
|
|
250
|
+
/** RPC name for upserting into public.users. Default: "upsert_oidc_user". */
|
|
251
|
+
upsertRpcName?: string;
|
|
252
|
+
/** RPC name for linking shadow auth user. Default: "link_shadow_auth_user". */
|
|
253
|
+
linkShadowRpcName?: string;
|
|
254
|
+
/** Whether to create a shadow auth.users record. Default: true. */
|
|
255
|
+
createShadowAuthUser?: boolean;
|
|
256
|
+
/** Default OIDC issuer when payload.iss is not provided. */
|
|
257
|
+
defaultIssuer?: string;
|
|
258
|
+
/** Whether to rollback on failure. Default: true. */
|
|
259
|
+
rollbackOnFailure?: boolean;
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Sync Flow
|
|
264
|
+
|
|
265
|
+
1. **Normalize payload** — Lowercase email, resolve display name from multiple claim fields (`name`, `preferred_username`, email prefix), apply default issuer.
|
|
266
|
+
|
|
267
|
+
2. **Ensure shadow `auth.users`** (when `createShadowAuthUser: true`):
|
|
268
|
+
- Search for existing record using identity-first matching
|
|
269
|
+
- If found and metadata needs updating (e.g. email matched but OIDC identity not set), update the record
|
|
270
|
+
- If not found, create a new shadow user with `role: "authenticated"`
|
|
271
|
+
|
|
272
|
+
3. **Upsert `public.users`** via `upsert_oidc_user()` RPC:
|
|
273
|
+
- Inserts or updates the application user record keyed by `(sub, iss)`
|
|
274
|
+
- Merges `raw_claims` JSONB (additive, never destructive)
|
|
275
|
+
- `email_verified` flag ORs (once verified, stays verified)
|
|
276
|
+
|
|
277
|
+
4. **Link shadow** via `link_shadow_auth_user()` RPC:
|
|
278
|
+
- Updates `public.users` to set `shadow_auth_user_id` and `shadow_linked_at`
|
|
279
|
+
- Enables cross-table joins between `public.users` and `auth.users`
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Rollback Behavior
|
|
284
|
+
|
|
285
|
+
The adapter includes automatic rollback for newly created shadow `auth.users` rows when downstream sync fails:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
Shadow auth.users created → upsert_oidc_user() RPC fails → Shadow deleted
|
|
289
|
+
Shadow auth.users created → link_shadow_auth_user() RPC fails → Shadow deleted
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Key details:**
|
|
293
|
+
|
|
294
|
+
- Rollback only applies to **newly created** shadow records (`authUserCreated: true`)
|
|
295
|
+
- Pre-existing shadow records matched by identity or email are **never** deleted on failure
|
|
296
|
+
- Rollback is **best-effort** — if the delete fails, the original error is still returned
|
|
297
|
+
- Controlled by `rollbackOnFailure` config (default: `true`)
|
|
298
|
+
|
|
299
|
+
This prevents orphaned `auth.users` records from accumulating when the downstream `public.users` sync fails.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## SQL Migrations
|
|
304
|
+
|
|
305
|
+
The provisioning model requires two SQL migrations applied to your Supabase project:
|
|
306
|
+
|
|
307
|
+
### Migration 001: `001_create_app_users.sql`
|
|
308
|
+
|
|
309
|
+
Creates the `public.users` table and `upsert_oidc_user()` RPC:
|
|
310
|
+
|
|
311
|
+
- Table keyed by `(sub, iss)` unique constraint
|
|
312
|
+
- `upsert_oidc_user()` — security-definer RPC restricted to `service_role`
|
|
313
|
+
- Indexes on `email` (lowercase) and `provider`
|
|
314
|
+
- Auto-updating `updated_at` trigger
|
|
315
|
+
|
|
316
|
+
### Migration 003: `003_authentik_shadow_auth_users.sql`
|
|
317
|
+
|
|
318
|
+
Adds shadow auth user support:
|
|
319
|
+
|
|
320
|
+
- `shadow_auth_user_id` (uuid) column on `public.users`
|
|
321
|
+
- `shadow_linked_at` (timestamptz) column on `public.users`
|
|
322
|
+
- Index on `shadow_auth_user_id` for lookup
|
|
323
|
+
- `link_shadow_auth_user()` RPC — links a `public.users` row to its shadow `auth.users` record
|
|
324
|
+
|
|
325
|
+
**Applying migrations:**
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
# Copy migrations to your Supabase project
|
|
329
|
+
cp packages/auth/supabase/migrations/001_create_app_users.sql \
|
|
330
|
+
supabase/migrations/
|
|
331
|
+
|
|
332
|
+
cp packages/auth/supabase/migrations/003_authentik_shadow_auth_users.sql \
|
|
333
|
+
supabase/migrations/
|
|
334
|
+
|
|
335
|
+
# Apply
|
|
336
|
+
supabase db push
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### `link_shadow_auth_user()` RPC
|
|
340
|
+
|
|
341
|
+
This RPC is called by `SupabaseSyncAdapter` after creating or finding the shadow `auth.users` record:
|
|
342
|
+
|
|
343
|
+
```sql
|
|
344
|
+
function link_shadow_auth_user(
|
|
345
|
+
p_sub text, -- OIDC subject identifier
|
|
346
|
+
p_iss text, -- OIDC issuer URL
|
|
347
|
+
p_shadow_auth_user_id uuid -- Supabase auth.users ID
|
|
348
|
+
) returns public.users
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
- **Security:** `SECURITY DEFINER` — requires `service_role` caller
|
|
352
|
+
- **Behavior:** Updates `shadow_auth_user_id` and `shadow_linked_at` on the `public.users` row matching `(p_sub, p_iss)`
|
|
353
|
+
- **Error:** Raises exception if no matching `public.users` row is found
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Runtime Requirements
|
|
358
|
+
|
|
359
|
+
Server-side provisioning requires these environment variables:
|
|
360
|
+
|
|
361
|
+
| Variable | Required | Description |
|
|
362
|
+
|----------|----------|-------------|
|
|
363
|
+
| `SUPABASE_URL` | Yes | Supabase project URL (e.g. `https://xxx.supabase.co`) |
|
|
364
|
+
| `SUPABASE_SERVICE_ROLE_KEY` | Yes | Supabase service_role JWT key |
|
|
365
|
+
|
|
366
|
+
The `service_role` key grants admin access to the Supabase Admin API, which is needed to:
|
|
367
|
+
- List, create, update, and delete `auth.users` records
|
|
368
|
+
- Call security-definer RPCs that check for `service_role` in JWT claims
|
|
369
|
+
|
|
370
|
+
> ⚠️ **Security:** The `service_role` key must never be exposed to the client. Provisioning must run exclusively in server-side code (API routes, server components, edge functions).
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Startup Validation
|
|
375
|
+
|
|
376
|
+
Use `validateFullConfig()` at application startup to detect misconfigurations before users attempt to log in:
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
import { validateFullConfig } from "@edcalderon/auth/authentik";
|
|
380
|
+
|
|
381
|
+
const result = validateFullConfig(
|
|
382
|
+
{
|
|
383
|
+
issuer: process.env.AUTHENTIK_ISSUER,
|
|
384
|
+
clientId: process.env.AUTHENTIK_CLIENT_ID,
|
|
385
|
+
redirectUri: process.env.AUTHENTIK_REDIRECT_URI,
|
|
386
|
+
tokenEndpoint: endpoints.token,
|
|
387
|
+
userinfoEndpoint: endpoints.userinfo,
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
supabaseUrl: process.env.SUPABASE_URL,
|
|
391
|
+
supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
if (!result.valid) {
|
|
396
|
+
for (const check of result.checks.filter(c => !c.passed)) {
|
|
397
|
+
console.error(`[config] ${check.name}: ${check.message}`);
|
|
398
|
+
}
|
|
399
|
+
// In production, fail fast:
|
|
400
|
+
throw new Error("Configuration validation failed — check environment variables");
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### The `supabase_not_configured` Error
|
|
405
|
+
|
|
406
|
+
When Supabase URL or service role key is missing, the validation emits the `supabase_not_configured` error signature:
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
supabase_not_configured: Supabase URL is required for sync
|
|
410
|
+
supabase_not_configured: Supabase service_role key is required for sync
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
This matches the CIG convention for detecting unconfigured Supabase environments. Use this error code to:
|
|
414
|
+
- Gate provisioning routes at deploy time
|
|
415
|
+
- Show a clear diagnostic in health-check endpoints
|
|
416
|
+
- Prevent partial deployments where OIDC is configured but Supabase is not
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Upgrade & Migration Guide
|
|
2
|
+
|
|
3
|
+
> **Package:** `@edcalderon/auth` v1.3.0 → v1.4.0
|
|
4
|
+
|
|
5
|
+
This document covers the upgrade path from v1.3.0 to v1.4.0 and provides migration notes for teams adopting the new Authentik integration kit.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
1. [Upgrade Summary](#upgrade-summary)
|
|
12
|
+
2. [What Changed in v1.4.0](#what-changed-in-v140)
|
|
13
|
+
3. [Upgrade Steps](#upgrade-steps)
|
|
14
|
+
4. [Impact on Existing Imports](#impact-on-existing-imports)
|
|
15
|
+
5. [Migrating Custom Relay/Callback/Logout Code](#migrating-custom-relaycallbacklogout-code)
|
|
16
|
+
6. [SQL Migration Path](#sql-migration-path)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Upgrade Summary
|
|
21
|
+
|
|
22
|
+
**v1.4.0 is fully additive.** There are no breaking changes to existing `@edcalderon/auth` exports. All existing imports, types, and behavior remain unchanged.
|
|
23
|
+
|
|
24
|
+
| Aspect | Impact |
|
|
25
|
+
|--------|--------|
|
|
26
|
+
| Existing `@edcalderon/auth` imports | ✅ No changes required |
|
|
27
|
+
| Existing `@edcalderon/auth/supabase` imports | ✅ No changes required |
|
|
28
|
+
| Existing `@edcalderon/auth/firebase-*` imports | ✅ No changes required |
|
|
29
|
+
| Existing `@edcalderon/auth/hybrid-*` imports | ✅ No changes required |
|
|
30
|
+
| New subpath `@edcalderon/auth/authentik` | 🆕 Opt-in only |
|
|
31
|
+
| SQL migrations 001-002 | ✅ Unchanged |
|
|
32
|
+
| SQL migration 003 | 🆕 Optional, only needed for shadow auth user support |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## What Changed in v1.4.0
|
|
37
|
+
|
|
38
|
+
### New Subpath Export: `@edcalderon/auth/authentik`
|
|
39
|
+
|
|
40
|
+
v1.4.0 adds a new subpath export that provides a complete Authentik flow and provisioning kit:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import {
|
|
44
|
+
// Relay (cross-origin PKCE)
|
|
45
|
+
createRelayPageHtml, parseRelayParams, readRelayStorage, clearRelayStorage,
|
|
46
|
+
// Callback (token exchange + provisioning gate)
|
|
47
|
+
exchangeCode, fetchClaims, processCallback,
|
|
48
|
+
// Logout (token revocation + RP-initiated logout)
|
|
49
|
+
revokeToken, buildEndSessionUrl, orchestrateLogout,
|
|
50
|
+
// Provisioning adapters
|
|
51
|
+
NoopProvisioningAdapter, createProvisioningAdapter,
|
|
52
|
+
SupabaseSyncAdapter, createSupabaseSyncAdapter,
|
|
53
|
+
// Config validation
|
|
54
|
+
validateAuthentikConfig, validateSupabaseSyncConfig, validateFullConfig,
|
|
55
|
+
discoverEndpoints,
|
|
56
|
+
// Safe redirect
|
|
57
|
+
resolveSafeRedirect,
|
|
58
|
+
} from "@edcalderon/auth/authentik";
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This subpath is completely independent — importing from `@edcalderon/auth/authentik` does not affect any other subpath exports.
|
|
62
|
+
|
|
63
|
+
### New SQL Migration
|
|
64
|
+
|
|
65
|
+
`003_authentik_shadow_auth_users.sql` adds shadow auth user columns and the `link_shadow_auth_user()` RPC to `public.users`. This migration is **optional** and only needed if you use the `SupabaseSyncAdapter` with shadow auth users enabled (the default).
|
|
66
|
+
|
|
67
|
+
### New Tests
|
|
68
|
+
|
|
69
|
+
96 tests across 6 test suites covering all new functionality.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Upgrade Steps
|
|
74
|
+
|
|
75
|
+
### 1. Update the Package
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install @edcalderon/auth@1.4.0
|
|
79
|
+
# or
|
|
80
|
+
pnpm add @edcalderon/auth@1.4.0
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. Verify Existing Functionality
|
|
84
|
+
|
|
85
|
+
Your existing code should work exactly as before. No changes to existing imports or configuration are required.
|
|
86
|
+
|
|
87
|
+
### 3. (Optional) Adopt the Authentik Kit
|
|
88
|
+
|
|
89
|
+
If you want to use the new Authentik integration:
|
|
90
|
+
|
|
91
|
+
1. Set up your Authentik resources — see the [Authentik Integration Guide](./authentik-integration-guide.md)
|
|
92
|
+
2. Apply SQL migration 003 if using Supabase provisioning — see [SQL Migration Path](#sql-migration-path)
|
|
93
|
+
3. Create your relay, callback, and logout routes — see [Next.js Examples](./nextjs-examples.md)
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Impact on Existing Imports
|
|
98
|
+
|
|
99
|
+
### Unaffected Imports
|
|
100
|
+
|
|
101
|
+
All of these continue to work without any changes:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// Core (unchanged)
|
|
105
|
+
import { AuthProvider, useAuth } from "@edcalderon/auth";
|
|
106
|
+
import type { AuthClient, User, SignInOptions } from "@edcalderon/auth";
|
|
107
|
+
|
|
108
|
+
// Supabase adapter (unchanged)
|
|
109
|
+
import { SupabaseClient } from "@edcalderon/auth/supabase";
|
|
110
|
+
|
|
111
|
+
// Firebase adapters (unchanged)
|
|
112
|
+
import { FirebaseWebClient } from "@edcalderon/auth/firebase-web";
|
|
113
|
+
import { FirebaseNativeClient } from "@edcalderon/auth/firebase-native";
|
|
114
|
+
|
|
115
|
+
// Hybrid adapters (unchanged)
|
|
116
|
+
import { HybridWebClient } from "@edcalderon/auth/hybrid-web";
|
|
117
|
+
import { HybridNativeClient } from "@edcalderon/auth/hybrid-native";
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### New Import (Opt-in)
|
|
121
|
+
|
|
122
|
+
The Authentik kit is available via a new subpath:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { processCallback, orchestrateLogout } from "@edcalderon/auth/authentik";
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This import is independent of all other subpaths. You can use it alongside any existing adapter configuration.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Migrating Custom Relay/Callback/Logout Code
|
|
133
|
+
|
|
134
|
+
If your app already has custom Authentik relay, callback, or logout code (e.g. copied from CIG), you can migrate it to use the package's standardized implementations.
|
|
135
|
+
|
|
136
|
+
### Relay Migration
|
|
137
|
+
|
|
138
|
+
**Before** (custom implementation):
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// app/auth/login/[provider]/route.ts
|
|
142
|
+
export async function GET(request: Request) {
|
|
143
|
+
const url = new URL(request.url);
|
|
144
|
+
const provider = url.searchParams.get("provider");
|
|
145
|
+
// ... custom PKCE generation, sessionStorage manipulation, redirect logic
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**After** (using the package):
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// app/auth/login/[provider]/route.ts
|
|
153
|
+
import { parseRelayParams, createRelayPageHtml } from "@edcalderon/auth/authentik";
|
|
154
|
+
|
|
155
|
+
export async function GET(request: Request) {
|
|
156
|
+
const params = parseRelayParams(new URL(request.url).searchParams);
|
|
157
|
+
if (!params) return new Response("Missing relay params", { status: 400 });
|
|
158
|
+
|
|
159
|
+
const { html } = createRelayPageHtml(relayConfig, params);
|
|
160
|
+
return new Response(html, {
|
|
161
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Callback Migration
|
|
167
|
+
|
|
168
|
+
**Before** (custom implementation):
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// Manual token exchange, userinfo fetch, state validation, provisioning
|
|
172
|
+
const tokens = await customExchangeCode(code, verifier);
|
|
173
|
+
const claims = await customFetchUserInfo(tokens.access_token);
|
|
174
|
+
await customSyncUser(claims);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**After** (using the package):
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { processCallback } from "@edcalderon/auth/authentik";
|
|
181
|
+
|
|
182
|
+
const result = await processCallback({
|
|
183
|
+
config: callbackConfig,
|
|
184
|
+
code,
|
|
185
|
+
codeVerifier: verifier,
|
|
186
|
+
state,
|
|
187
|
+
expectedState,
|
|
188
|
+
provider,
|
|
189
|
+
provisioningAdapter: adapter, // optional
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (!result.success) {
|
|
193
|
+
// Structured error handling
|
|
194
|
+
console.error(result.errorCode, result.error);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Logout Migration
|
|
199
|
+
|
|
200
|
+
**Before** (custom implementation):
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
// Manual token revocation, end-session URL construction
|
|
204
|
+
await fetch(revocationEndpoint, { method: "POST", body: new URLSearchParams({ token }) });
|
|
205
|
+
const logoutUrl = `${endSessionEndpoint}?id_token_hint=${idToken}&post_logout_redirect_uri=${redirectUri}`;
|
|
206
|
+
window.location.href = logoutUrl;
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**After** (using the package):
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { orchestrateLogout } from "@edcalderon/auth/authentik";
|
|
213
|
+
|
|
214
|
+
const { endSessionUrl, tokenRevoked } = await orchestrateLogout(logoutConfig, {
|
|
215
|
+
accessToken,
|
|
216
|
+
idToken,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Navigate to Authentik to clear the session
|
|
220
|
+
window.location.href = endSessionUrl;
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## SQL Migration Path
|
|
226
|
+
|
|
227
|
+
### If you already have migrations 001-002 applied
|
|
228
|
+
|
|
229
|
+
Migration 003 is **additive** — it adds new columns to `public.users` without modifying existing ones:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
cp packages/auth/supabase/migrations/003_authentik_shadow_auth_users.sql \
|
|
233
|
+
supabase/migrations/
|
|
234
|
+
|
|
235
|
+
supabase db push
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### If you are starting fresh
|
|
239
|
+
|
|
240
|
+
Apply all migrations in order:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
cp packages/auth/supabase/migrations/001_create_app_users.sql \
|
|
244
|
+
packages/auth/supabase/migrations/003_authentik_shadow_auth_users.sql \
|
|
245
|
+
supabase/migrations/
|
|
246
|
+
|
|
247
|
+
# Optionally also copy 002 if you want the auth.users → public.users trigger
|
|
248
|
+
cp packages/auth/supabase/migrations/002_sync_auth_users_to_app_users.sql \
|
|
249
|
+
supabase/migrations/
|
|
250
|
+
|
|
251
|
+
supabase db push
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### If you do not use Supabase provisioning
|
|
255
|
+
|
|
256
|
+
You do not need migration 003. The Authentik kit works without Supabase — use `NoopProvisioningAdapter` or a custom adapter for your backend.
|