@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.
@@ -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.