@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,286 @@
1
+ # Authentik Integration Guide
2
+
3
+ > **Package:** `@edcalderon/auth/authentik` (v1.4.0+)
4
+ > **Runtime:** Server-side (Next.js App Router, Node.js)
5
+
6
+ This guide walks through setting up Authentik as your OIDC identity provider with the `@edcalderon/auth/authentik` package. By the end you will have a working social-login flow (e.g. Google, GitHub, Discord) that authenticates via Authentik and optionally provisions users into a Supabase-backed user store.
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ 1. [Prerequisites](#prerequisites)
13
+ 2. [Authentik Resource Setup](#authentik-resource-setup)
14
+ - [OIDC Application & Provider](#1-oidc-application--provider)
15
+ - [Social Login Sources](#2-social-login-sources)
16
+ - [Invalidation / Logout Flow](#3-invalidation--logout-flow)
17
+ 3. [Direct Social Login (Default Pattern)](#direct-social-login-default-pattern)
18
+ 4. [Source Configuration: Identifier-Based Matching](#source-configuration-identifier-based-matching)
19
+ 5. [Slug and Redirect URI Contract](#slug-and-redirect-uri-contract)
20
+ 6. [Environment Variables](#environment-variables)
21
+ 7. [Endpoint Discovery](#endpoint-discovery)
22
+ 8. [Config Validation](#config-validation)
23
+
24
+ ---
25
+
26
+ ## Prerequisites
27
+
28
+ - An Authentik instance (self-hosted or managed) running **2024.x** or later.
29
+ - A registered OIDC Application in Authentik.
30
+ - At least one social-login Source (Google, GitHub, Discord, etc.) configured in Authentik.
31
+ - `@edcalderon/auth` v1.4.0 or later installed in your project.
32
+
33
+ ```bash
34
+ npm install @edcalderon/auth
35
+ # or
36
+ pnpm add @edcalderon/auth
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Authentik Resource Setup
42
+
43
+ ### 1. OIDC Application & Provider
44
+
45
+ Create an **OAuth2/OIDC Provider** and link it to an **Application** in Authentik:
46
+
47
+ | Setting | Value | Notes |
48
+ |---------|-------|-------|
49
+ | **Provider type** | OAuth2/OIDC | Required for PKCE support |
50
+ | **Client type** | Public | SPA/Next.js apps cannot keep client secrets |
51
+ | **Client ID** | (auto-generated) | Copy this — used in package config |
52
+ | **Redirect URIs** | `https://app.example.com/auth/callback` | Must exactly match `redirectUri` in package config |
53
+ | **Signing key** | Select a signing key | Required for ID token validation |
54
+ | **Scopes** | `openid profile email` | Default scopes for the package |
55
+ | **PKCE** | Enabled | The package always uses S256 PKCE |
56
+
57
+ **Application settings:**
58
+ - **Slug**: e.g. `my-app` — this determines your issuer URL
59
+ - **Launch URL**: your application's landing page
60
+
61
+ Your issuer URL follows the pattern:
62
+
63
+ ```
64
+ https://<authentik-host>/application/o/<app-slug>/
65
+ ```
66
+
67
+ > **Important:** Authentik places most OIDC endpoints (token, userinfo, authorize, revocation) at the `/application/o/` level, _not_ under the per-app issuer path. For example:
68
+ > - Issuer: `https://auth.example.com/application/o/my-app/`
69
+ > - Token endpoint: `https://auth.example.com/application/o/token/`
70
+ >
71
+ > Always use [`discoverEndpoints()`](#endpoint-discovery) or supply explicit URLs — do not guess from the issuer.
72
+
73
+ ### 2. Social Login Sources
74
+
75
+ For each social provider (Google, GitHub, Discord, etc.), create a **Source** in Authentik:
76
+
77
+ 1. Navigate to **Directory → Federation & Social Login → Sources**
78
+ 2. Create a new **OAuth Source** for your provider (e.g. Google)
79
+ 3. Configure the upstream OAuth credentials (Client ID/Secret from the provider's developer console)
80
+ 4. Set the **Slug** — e.g. `google`, `github`, `discord`
81
+
82
+ The slug determines the social login URL pattern:
83
+
84
+ ```
85
+ https://<authentik-host>/source/oauth/login/<source-slug>/
86
+ ```
87
+
88
+ This is the **direct social login** pattern — the default and recommended approach (see below).
89
+
90
+ ### 3. Invalidation / Logout Flow
91
+
92
+ For RP-initiated logout to work, your Authentik Provider needs an **invalidation flow**:
93
+
94
+ 1. Navigate to **Flows & Stages → Flows**
95
+ 2. Create a new flow with designation **Invalidation**
96
+ 3. Add a **User Logout** stage — this clears the Authentik browser session
97
+ 4. Add a **Redirect** stage after the logout stage — configure it to redirect to your `postLogoutRedirectUri`
98
+ 5. Bind this flow as the **Invalidation Flow** on your OAuth2/OIDC Provider
99
+
100
+ Without this flow, `orchestrateLogout()` will be unable to clear the Authentik session, and users will remain logged in on the Authentik side.
101
+
102
+ **Example flow structure:**
103
+
104
+ ```
105
+ Invalidation Flow: "my-app-logout"
106
+ ├── Stage 1: User Logout
107
+ └── Stage 2: Redirect → https://app.example.com/
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Direct Social Login (Default Pattern)
113
+
114
+ The package defaults to **direct social login** — one button per provider, each button triggers a provider-specific Source flow. This is the recommended approach.
115
+
116
+ **How it works:**
117
+
118
+ 1. User clicks "Sign in with Google" in your app
119
+ 2. The relay handler redirects to Authentik's source-based login URL:
120
+ ```
121
+ https://auth.example.com/source/oauth/login/google/?next=<authorize-url>
122
+ ```
123
+ 3. Authentik handles the upstream OAuth flow (Google consent screen, etc.)
124
+ 4. Authentik redirects back to your OIDC authorize endpoint with an authorization code
125
+ 5. Your callback handler exchanges the code for tokens
126
+
127
+ **In code** (relay configuration):
128
+
129
+ ```ts
130
+ import { createRelayPageHtml, parseRelayParams } from "@edcalderon/auth/authentik";
131
+
132
+ // The relay config — no providerFlowSlugs means direct social login
133
+ const relayConfig = {
134
+ issuer: "https://auth.example.com/application/o/my-app/",
135
+ clientId: "your-client-id",
136
+ redirectUri: "https://app.example.com/auth/callback",
137
+ authorizePath: "https://auth.example.com/application/o/authorize/",
138
+ };
139
+ ```
140
+
141
+ When `providerFlowSlugs` is **not** provided, the relay automatically uses the source-based login URL (`/source/oauth/login/{provider}/`). This is the simplest and most common setup.
142
+
143
+ **Custom flow slugs** (advanced): If you need custom authentication flows per provider (e.g. flows with additional stages like MFA), map provider names to flow slugs:
144
+
145
+ ```ts
146
+ const relayConfig = {
147
+ // ...base config
148
+ providerFlowSlugs: {
149
+ google: "my-app-google-mfa-flow",
150
+ github: "my-app-github-flow",
151
+ },
152
+ };
153
+ ```
154
+
155
+ This routes through `/if/flow/{flowSlug}/` instead of `/source/oauth/login/{provider}/`.
156
+
157
+ ---
158
+
159
+ ## Source Configuration: Identifier-Based Matching
160
+
161
+ When configuring Sources in Authentik, use **identifier-based matching** as the safe default:
162
+
163
+ | Setting | Recommended Value | Why |
164
+ |---------|-------------------|-----|
165
+ | **User matching mode** | `identifier` | Prevents account takeover via email changes |
166
+ | **User path** | Leave as default | Standard user directory |
167
+
168
+ **Why identifier-based matching?**
169
+
170
+ - **Email-based matching** is risky: if an upstream provider allows users to change their email, a different person could receive tokens for an existing account.
171
+ - **Identifier-based matching** uses the stable OIDC `sub` claim from the upstream provider — this never changes for a given user.
172
+ - The package's `SupabaseSyncAdapter` mirrors this safety by using identity-first matching (see [Provisioning Model](./provisioning-model.md)).
173
+
174
+ ---
175
+
176
+ ## Slug and Redirect URI Contract
177
+
178
+ The package relies on a strict contract between your Authentik tenant configuration and your package config:
179
+
180
+ | Package Config | Authentik Resource | Must Match |
181
+ |---------------|-------------------|------------|
182
+ | `issuer` | Application slug | `https://<host>/application/o/<app-slug>/` |
183
+ | `clientId` | Provider Client ID | Exact string match |
184
+ | `redirectUri` | Provider Redirect URIs | Exact URL match (including path, no trailing slash difference) |
185
+ | `authorizePath` | Discovered or manual | `https://<host>/application/o/authorize/` |
186
+ | `tokenEndpoint` | Discovered or manual | `https://<host>/application/o/token/` |
187
+ | `userinfoEndpoint` | Discovered or manual | `https://<host>/application/o/userinfo/` |
188
+ | `endSessionEndpoint` | Discovered or manual | `https://<host>/application/o/<app-slug>/end-session/` |
189
+ | `revocationEndpoint` | Discovered or manual | `https://<host>/application/o/<app-slug>/revoke/` |
190
+ | Provider slug in relay | Source slug | e.g. `google` maps to Source with slug `google` |
191
+
192
+ > **Tip:** Use `discoverEndpoints()` to automatically resolve all endpoint URLs from the `.well-known/openid-configuration` document. This avoids manual URL construction errors.
193
+
194
+ ---
195
+
196
+ ## Environment Variables
197
+
198
+ The package reads configuration from environment variables or direct config objects. Here are the required variables for server-side usage:
199
+
200
+ | Variable | Description | Example |
201
+ |----------|-------------|---------|
202
+ | `AUTHENTIK_ISSUER` | Authentik issuer URL | `https://auth.example.com/application/o/my-app/` |
203
+ | `AUTHENTIK_CLIENT_ID` | OAuth2 provider Client ID | `abc123def456` |
204
+ | `AUTHENTIK_REDIRECT_URI` | Callback URL registered in Authentik | `https://app.example.com/auth/callback` |
205
+
206
+ For Supabase provisioning (server-side only):
207
+
208
+ | Variable | Description | Example |
209
+ |----------|-------------|---------|
210
+ | `SUPABASE_URL` | Supabase project URL | `https://xxx.supabase.co` |
211
+ | `SUPABASE_SERVICE_ROLE_KEY` | Supabase service_role key | `eyJ...` (long JWT) |
212
+
213
+ > ⚠️ **Never expose `SUPABASE_SERVICE_ROLE_KEY` to the client.** Provisioning must run server-side only.
214
+
215
+ ---
216
+
217
+ ## Endpoint Discovery
218
+
219
+ Use `discoverEndpoints()` to automatically fetch all OIDC endpoints from Authentik:
220
+
221
+ ```ts
222
+ import { discoverEndpoints } from "@edcalderon/auth/authentik";
223
+
224
+ const endpoints = await discoverEndpoints(
225
+ "https://auth.example.com/application/o/my-app/"
226
+ );
227
+
228
+ // endpoints.authorization → "https://auth.example.com/application/o/authorize/"
229
+ // endpoints.token → "https://auth.example.com/application/o/token/"
230
+ // endpoints.userinfo → "https://auth.example.com/application/o/userinfo/"
231
+ // endpoints.revocation → "https://auth.example.com/application/o/<app>/revoke/"
232
+ // endpoints.endSession → "https://auth.example.com/application/o/<app>/end-session/"
233
+ ```
234
+
235
+ This fetches the `.well-known/openid-configuration` document from the issuer and extracts the endpoint URLs. It is recommended to call this once at startup and cache the result.
236
+
237
+ ---
238
+
239
+ ## Config Validation
240
+
241
+ Run config validation at startup to catch misconfigurations before the first user attempts to log in:
242
+
243
+ ```ts
244
+ import {
245
+ validateAuthentikConfig,
246
+ validateSupabaseSyncConfig,
247
+ validateFullConfig,
248
+ } from "@edcalderon/auth/authentik";
249
+
250
+ // Validate Authentik OIDC config only
251
+ const authentikResult = validateAuthentikConfig({
252
+ issuer: process.env.AUTHENTIK_ISSUER,
253
+ clientId: process.env.AUTHENTIK_CLIENT_ID,
254
+ redirectUri: process.env.AUTHENTIK_REDIRECT_URI,
255
+ tokenEndpoint: endpoints.token,
256
+ userinfoEndpoint: endpoints.userinfo,
257
+ });
258
+
259
+ if (!authentikResult.valid) {
260
+ console.error("Authentik config errors:", authentikResult.checks.filter(c => !c.passed));
261
+ process.exit(1);
262
+ }
263
+
264
+ // Validate both Authentik + Supabase config together
265
+ const fullResult = validateFullConfig(
266
+ {
267
+ issuer: process.env.AUTHENTIK_ISSUER,
268
+ clientId: process.env.AUTHENTIK_CLIENT_ID,
269
+ redirectUri: process.env.AUTHENTIK_REDIRECT_URI,
270
+ tokenEndpoint: endpoints.token,
271
+ userinfoEndpoint: endpoints.userinfo,
272
+ },
273
+ {
274
+ supabaseUrl: process.env.SUPABASE_URL,
275
+ supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
276
+ },
277
+ );
278
+
279
+ if (!fullResult.valid) {
280
+ // Checks will include "supabase_not_configured" error code when
281
+ // Supabase URL or service role key is missing
282
+ console.error("Config validation failed:", fullResult.checks.filter(c => !c.passed));
283
+ }
284
+ ```
285
+
286
+ The `supabase_not_configured` error code is emitted when Supabase URL or service role key is missing — this matches the CIG convention for detecting unconfigured environments at deploy time.
@@ -0,0 +1,118 @@
1
+ # CIG Reference Implementation Map
2
+
3
+ > **Package:** `@edcalderon/auth/authentik` (v1.4.0+)
4
+
5
+ This document maps each module in `@edcalderon/auth/authentik` back to its origin in the [ComputeIntelligenceGraph (CIG)](https://github.com/edwardcalderon/ComputeIntelligenceGraph) codebase. Use these links to trace design decisions and understand the production context that shaped each module.
6
+
7
+ ---
8
+
9
+ ## Architecture Reference
10
+
11
+ | Document | Link | Description |
12
+ |----------|------|-------------|
13
+ | CIG Auth Architecture | [docs/authentication/README.md](https://github.com/edwardcalderon/ComputeIntelligenceGraph/blob/main/docs/authentication/README.md) | Top-level auth architecture document covering the federated OAuth strategy, provider model, and session management design |
14
+ | CIG Auth Package | [packages/auth/](https://github.com/edwardcalderon/ComputeIntelligenceGraph/tree/main/packages/auth) | The original monorepo auth package that `@edcalderon/auth` generalizes |
15
+
16
+ ---
17
+
18
+ ## Module-to-CIG Origin Map
19
+
20
+ ### Relay (`relay.ts`)
21
+
22
+ | Package Module | CIG Origin |
23
+ |---------------|------------|
24
+ | `createRelayPageHtml()` | [`apps/dashboard/app/auth/login/[provider]/route.ts`](https://github.com/edwardcalderon/ComputeIntelligenceGraph/blob/main/apps/dashboard/app/auth/login/%5Bprovider%5D/route.ts) |
25
+ | `parseRelayParams()` | Same route — query parameter parsing logic |
26
+ | `readRelayStorage()` | [`apps/dashboard/app/auth/callback/page.tsx`](https://github.com/edwardcalderon/ComputeIntelligenceGraph/blob/main/apps/dashboard/app/auth/callback/page.tsx) — sessionStorage read on callback |
27
+ | `clearRelayStorage()` | Same callback page — cleanup after successful exchange |
28
+
29
+ **Design context:** The CIG dashboard lives on a different origin than the CIG landing page. The relay pattern was created to bridge PKCE sessionStorage across origins without requiring a shared backend session.
30
+
31
+ ### Callback (`callback.ts`)
32
+
33
+ | Package Module | CIG Origin |
34
+ |---------------|------------|
35
+ | `exchangeCode()` | [`apps/dashboard/app/auth/callback/page.tsx`](https://github.com/edwardcalderon/ComputeIntelligenceGraph/blob/main/apps/dashboard/app/auth/callback/page.tsx) — token exchange logic |
36
+ | `fetchClaims()` | Same callback page — userinfo fetch after token exchange |
37
+ | `processCallback()` | Combines callback + [`apps/dashboard/app/api/auth/sync/route.ts`](https://github.com/edwardcalderon/ComputeIntelligenceGraph/blob/main/apps/dashboard/app/api/auth/sync/route.ts) — the full exchange + sync pipeline |
38
+
39
+ **Design context:** In CIG, the callback page exchanges the code client-side, then calls the sync API route server-side. The package's `processCallback()` unifies this into a single server-safe function with an optional provisioning gate.
40
+
41
+ ### Logout (`logout.ts`)
42
+
43
+ | Package Module | CIG Origin |
44
+ |---------------|------------|
45
+ | `revokeToken()` | [`apps/landing/components/AuthProvider.tsx`](https://github.com/edwardcalderon/ComputeIntelligenceGraph/blob/main/apps/landing/components/AuthProvider.tsx) — `signOut()` method, token revocation step |
46
+ | `buildEndSessionUrl()` | Same AuthProvider — end-session URL construction |
47
+ | `orchestrateLogout()` | Same AuthProvider — the combined revoke + redirect sequence |
48
+
49
+ **Design context:** The CIG landing page handles logout for users who authenticated via Authentik. The package extracts this into a framework-agnostic orchestrator that works in any server or client context.
50
+
51
+ ### Provisioning (`provisioning.ts`)
52
+
53
+ | Package Module | CIG Origin |
54
+ |---------------|------------|
55
+ | `SupabaseSyncAdapter` | [`apps/dashboard/app/api/auth/sync/route.ts`](https://github.com/edwardcalderon/ComputeIntelligenceGraph/blob/main/apps/dashboard/app/api/auth/sync/route.ts) + [`apps/dashboard/lib/authSync.ts`](https://github.com/edwardcalderon/ComputeIntelligenceGraph/blob/main/apps/dashboard/lib/authSync.ts) |
56
+ | `normalizePayload()` | `authSync.ts` — payload normalization before Supabase upsert |
57
+ | `findShadowAuthUser()` (internal) | `authSync.ts` — paginated identity-first user search |
58
+ | `ensureShadowAuthUser()` (internal) | `authSync.ts` — shadow auth.users create/update logic |
59
+ | `NoopProvisioningAdapter` | New in package — no CIG equivalent (CIG always syncs) |
60
+ | `createProvisioningAdapter()` | New in package — factory pattern for custom adapters |
61
+ | `createSupabaseSyncAdapter()` | New in package — convenience wrapper around `SupabaseSyncAdapter` |
62
+
63
+ **Design context:** The CIG sync route is the most battle-tested part of the auth system. It handles the identity-first matching strategy, shadow auth.users creation, rollback on failure, and the `link_shadow_auth_user()` RPC call. The package preserves all these behaviors.
64
+
65
+ ### Config Validation (`config.ts`)
66
+
67
+ | Package Module | CIG Origin |
68
+ |---------------|------------|
69
+ | `validateAuthentikConfig()` | CIG startup checks (distributed across components) |
70
+ | `validateSupabaseSyncConfig()` | [`apps/dashboard/app/api/auth/sync/route.ts`](https://github.com/edwardcalderon/ComputeIntelligenceGraph/blob/main/apps/dashboard/app/api/auth/sync/route.ts) — `supabase_not_configured` error handling |
71
+ | `validateFullConfig()` | New in package — combines both validations |
72
+ | `discoverEndpoints()` | CIG deployment scripts — `.well-known/openid-configuration` fetching |
73
+
74
+ **Design context:** The `supabase_not_configured` error code comes directly from CIG's sync route, where a missing Supabase configuration causes the sync to fail immediately with a clear diagnostic.
75
+
76
+ ### Safe Redirect (`redirect.ts`)
77
+
78
+ | Package Module | CIG Origin |
79
+ |---------------|------------|
80
+ | `resolveSafeRedirect()` | CIG callback + logout redirect patterns — origin allowlist validation |
81
+
82
+ **Design context:** Both the CIG callback page and logout flow validate redirect targets against an allowlist to prevent open-redirect attacks. The package extracts this into a reusable utility.
83
+
84
+ ### Types (`types.ts`)
85
+
86
+ | Package Type | CIG Origin |
87
+ |-------------|------------|
88
+ | `AuthentikProvider` | CIG provider slug constants |
89
+ | `AuthentikEndpoints` | CIG OIDC endpoint configuration |
90
+ | `AuthentikRelayConfig` | CIG relay route configuration |
91
+ | `AuthentikCallbackConfig` | CIG callback page configuration |
92
+ | `AuthentikLogoutConfig` | CIG AuthProvider logout configuration |
93
+ | `ProvisioningPayload` | CIG `OidcSyncPayload` from `authSync.ts` |
94
+ | `ProvisioningResult` | CIG sync response shape |
95
+ | `SupabaseSyncConfig` | CIG sync route environment configuration |
96
+
97
+ ### SQL Migrations
98
+
99
+ | Migration | CIG Origin |
100
+ |-----------|------------|
101
+ | `001_create_app_users.sql` | CIG Supabase migration — `public.users` table + `upsert_oidc_user()` RPC |
102
+ | `002_sync_auth_users_to_app_users.sql` | CIG Supabase migration — `auth.users` → `public.users` sync trigger |
103
+ | `003_authentik_shadow_auth_users.sql` | CIG `authSync.ts` — shadow linkage columns + `link_shadow_auth_user()` RPC |
104
+
105
+ ---
106
+
107
+ ## Summary
108
+
109
+ The `@edcalderon/auth/authentik` package is a direct generalization of the CIG production Authentik integration. Every module preserves the battle-tested behavior from CIG while making it configurable and reusable across projects.
110
+
111
+ Key design decisions traced back to CIG:
112
+
113
+ 1. **Fail-closed provisioning** — users cannot access the app until sync succeeds (CIG sync route)
114
+ 2. **Identity-first matching** — prevents email-based account takeover (CIG `authSync.ts`)
115
+ 3. **Shadow auth.users** — enables Supabase RLS without Supabase Auth signup (CIG sync route)
116
+ 4. **Rollback on failure** — prevents orphaned shadow records (CIG sync route)
117
+ 5. **Cross-origin PKCE relay** — bridges sessionStorage across origins (CIG landing → dashboard)
118
+ 6. **Direct social login** — one button per provider via Authentik Sources (CIG landing page)