@edcalderon/auth 1.2.2 → 1.4.0

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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0] - 2026-03-23
4
+
5
+ ### Added
6
+
7
+ - ✨ **Authentik flow + provisioning kit** (`@edcalderon/auth/authentik`) — a reusable set of helpers generalised from the production CIG Authentik implementation.
8
+ - 🔀 **Cross-origin PKCE relay** — `createRelayPageHtml()`, `parseRelayParams()`, `readRelayStorage()`, `clearRelayStorage()` for apps where login UI and callback handler live on different origins.
9
+ - 🔄 **Enhanced callback handler** — `exchangeCode()`, `fetchClaims()`, `processCallback()` with blocking provisioning gate that prevents redirect until user sync completes.
10
+ - 🚪 **Logout orchestrator** — `revokeToken()`, `buildEndSessionUrl()`, `orchestrateLogout()` implementing the full RP-initiated logout flow.
11
+ - 🔌 **Provisioning adapter layer** — pluggable adapters: `NoopProvisioningAdapter`, `createProvisioningAdapter()`, `SupabaseSyncAdapter` with identity-first matching and rollback on failure.
12
+ - 🏥 **Config validation / doctor** — `validateAuthentikConfig()`, `validateSupabaseSyncConfig()`, `validateFullConfig()` for startup / deploy-time validation (detects `supabase_not_configured`).
13
+ - 🛡️ **Safe redirect resolver** — `resolveSafeRedirect()` with origin allowlist to prevent open-redirect vulnerabilities.
14
+ - 📦 **New subpath export** — `@edcalderon/auth/authentik` barrel export for all Authentik-specific modules.
15
+ - 🗄️ **SQL migration 003** — `003_authentik_shadow_auth_users.sql` adds shadow auth user linkage columns and `link_shadow_auth_user()` RPC.
16
+ - 🧪 **96 tests** across 6 test suites covering relay, callback, logout, provisioning (incl. paginated page-2 lookups, shadow linkage RPC, rollback), config validation (incl. endpoint discovery), and redirect safety.
17
+
18
+ ## [1.3.0] - 2026-03-19
19
+
20
+ ### Added
21
+
22
+ - Added canonical `AuthentikOidcClient` browser helpers with PKCE-only OAuth flow utilities (`isAuthentikConfigured`, `startAuthentikOAuthFlow`, `handleAuthentikCallback`, `readOidcSession`, `clearOidcSession`, `hasPendingAuthentikCallback`, `OIDC_INITIAL_SEARCH`).
23
+ - Added exported Authentik OIDC types: `OidcClaims`, `OidcSession`, `OidcProvider`.
24
+ - Added README guidance for Authentik setup and the known Authentik `2026.2.1` social re-link bug workaround.
25
+
3
26
  ## [1.2.2] - 2026-03-19
4
27
 
5
28
  ### Added
package/README.md CHANGED
@@ -11,16 +11,20 @@ Swap between Supabase, Firebase, Hybrid, or any custom provider without changing
11
11
 
12
12
  ---
13
13
 
14
- ## 📋 Latest Changes (v1.2.2)
14
+ ## 📋 Latest Changes (v1.4.0)
15
15
 
16
16
  ### Added
17
17
 
18
- - Added `packages/auth/supabase/` SQL templates for a vendor-independent `public.users` table and optional Supabase Auth sync trigger.
19
-
20
- ### Fixed
21
-
22
- - Hardened the OIDC upsert migration so identity writes require a trusted server-side caller instead of the `anon` role.
23
- - Preserved existing user profile fields when optional claims are omitted during upserts or Supabase sync updates.
18
+ - **Authentik flow + provisioning kit** (`@edcalderon/auth/authentik`) a reusable set of helpers generalised from the production CIG Authentik implementation.
19
+ - 🔀 **Cross-origin PKCE relay** — `createRelayPageHtml()`, `parseRelayParams()`, `readRelayStorage()`, `clearRelayStorage()` for apps where login UI and callback handler live on different origins.
20
+ - 🔄 **Enhanced callback handler** — `exchangeCode()`, `fetchClaims()`, `processCallback()` with blocking provisioning gate that prevents redirect until user sync completes.
21
+ - 🚪 **Logout orchestrator** — `revokeToken()`, `buildEndSessionUrl()`, `orchestrateLogout()` implementing the full RP-initiated logout flow.
22
+ - 🔌 **Provisioning adapter layer** pluggable adapters: `NoopProvisioningAdapter`, `createProvisioningAdapter()`, `SupabaseSyncAdapter` with identity-first matching and rollback on failure.
23
+ - 🏥 **Config validation / doctor** `validateAuthentikConfig()`, `validateSupabaseSyncConfig()`, `validateFullConfig()` for startup / deploy-time validation (detects `supabase_not_configured`).
24
+ - 🛡️ **Safe redirect resolver** — `resolveSafeRedirect()` with origin allowlist to prevent open-redirect vulnerabilities.
25
+ - 📦 **New subpath export** — `@edcalderon/auth/authentik` barrel export for all Authentik-specific modules.
26
+ - 🗄️ **SQL migration 003** — `003_authentik_shadow_auth_users.sql` adds shadow auth user linkage columns and `link_shadow_auth_user()` RPC.
27
+ - 🧪 **96 tests** across 6 test suites covering relay, callback, logout, provisioning (incl. paginated page-2 lookups, shadow linkage RPC, rollback), config validation (incl. endpoint discovery), and redirect safety.
24
28
 
25
29
  For full version history, see [CHANGELOG.md](./CHANGELOG.md) and [GitHub releases](https://github.com/edcalderon/my-second-brain/releases)
26
30
 
@@ -79,6 +83,68 @@ If you want an application-owned user table instead of coupling your identity mo
79
83
  - `001_create_app_users.sql`: vendor-independent `public.users` table plus secure server-side OIDC upsert RPC
80
84
  - `002_sync_auth_users_to_app_users.sql`: optional trigger and backfill for projects using Supabase Auth
81
85
 
86
+ ### Authentik OIDC Client (Canonical)
87
+
88
+ `@edcalderon/auth` exports a browser-first Authentik OIDC helper that is decoupled from Supabase and can be used with any backend session strategy.
89
+
90
+ ```ts
91
+ import {
92
+ isAuthentikConfigured,
93
+ startAuthentikOAuthFlow,
94
+ handleAuthentikCallback,
95
+ readOidcSession,
96
+ clearOidcSession,
97
+ hasPendingAuthentikCallback,
98
+ } from "@edcalderon/auth";
99
+
100
+ if (isAuthentikConfigured()) {
101
+ await startAuthentikOAuthFlow("google", {
102
+ providerSourceSlugs: {
103
+ google: "google",
104
+ discord: "discord",
105
+ },
106
+ });
107
+ }
108
+
109
+ if (hasPendingAuthentikCallback(window.location.search)) {
110
+ const session = await handleAuthentikCallback(window.location.search, {
111
+ onSessionReady: async (claims, tokens) => {
112
+ // Optional hook for API upsert/session handoff.
113
+ console.log(claims.sub, tokens.accessToken);
114
+ },
115
+ });
116
+
117
+ console.log("OIDC session", session);
118
+ }
119
+
120
+ const existing = readOidcSession();
121
+ if (!existing) {
122
+ clearOidcSession();
123
+ }
124
+ ```
125
+
126
+ Required env vars (defaults):
127
+
128
+ | Var | Description |
129
+ | --- | --- |
130
+ | `EXPO_PUBLIC_AUTHENTIK_ISSUER` | `https://<host>/application/o/<app-slug>/` |
131
+ | `EXPO_PUBLIC_AUTHENTIK_CLIENT_ID` | OAuth2 provider client ID |
132
+ | `EXPO_PUBLIC_AUTHENTIK_REDIRECT_URI` | App redirect URI registered in Authentik |
133
+
134
+ You can override env key names with `envKeys` and pass direct values with `issuer`, `clientId`, and `redirectUri`.
135
+
136
+ Authentik setup checklist:
137
+
138
+ 1. Configure an OAuth2/OIDC provider in Authentik with PKCE enabled.
139
+ 2. Ensure redirect URIs match your app origin/path exactly.
140
+ 3. Configure source login slugs (`providerSourceSlugs`) for each social provider.
141
+ 4. Use `onSessionReady` to hand off claims/tokens to your backend session flow.
142
+
143
+ Known Authentik `2026.2.1` bug workaround:
144
+
145
+ - A production hot-patch may be needed in Authentik `flow_manager.py` around `handle_existing_link` to avoid duplicate `(user_id, source_id)` writes when re-linking existing social identities.
146
+ - Track the upstream Authentik issue and re-apply the patch after container upgrades until a fixed release is available.
147
+
82
148
  ---
83
149
 
84
150
  ## Subpath Exports (Crucial for RN/Next.js compatibility)
@@ -0,0 +1,58 @@
1
+ export type OidcProvider = "google" | "discord" | string;
2
+ export interface OidcClaims {
3
+ sub: string;
4
+ iss: string;
5
+ email?: string;
6
+ email_verified?: boolean;
7
+ name?: string;
8
+ picture?: string;
9
+ preferred_username?: string;
10
+ [key: string]: unknown;
11
+ }
12
+ export interface OidcTokens {
13
+ accessToken: string;
14
+ tokenType?: string;
15
+ refreshToken?: string;
16
+ idToken?: string;
17
+ expiresIn?: number;
18
+ expiresAt?: number;
19
+ scope?: string;
20
+ }
21
+ export interface OidcSession {
22
+ provider: OidcProvider;
23
+ issuer: string;
24
+ clientId: string;
25
+ claims: OidcClaims;
26
+ tokens: OidcTokens;
27
+ createdAt: number;
28
+ }
29
+ export interface AuthentikEnvKeys {
30
+ issuer: string;
31
+ clientId: string;
32
+ redirectUri: string;
33
+ }
34
+ export interface AuthentikOidcConfig {
35
+ issuer?: string;
36
+ clientId?: string;
37
+ redirectUri?: string;
38
+ scope?: string;
39
+ env?: Record<string, string | undefined>;
40
+ envKeys?: Partial<AuthentikEnvKeys>;
41
+ providerSourceSlugs?: Record<string, string>;
42
+ authorizePath?: string;
43
+ tokenPath?: string;
44
+ userinfoPath?: string;
45
+ onSessionReady?: (claims: OidcClaims, tokens: OidcTokens, session: OidcSession) => void | Promise<void>;
46
+ storageKey?: string;
47
+ pendingStorageKey?: string;
48
+ sessionStorage?: Storage;
49
+ localStorage?: Storage;
50
+ fetchFn?: typeof fetch;
51
+ }
52
+ export declare const OIDC_INITIAL_SEARCH = "authentik:oidc:initial-search";
53
+ export declare function isAuthentikConfigured(config?: AuthentikOidcConfig): boolean;
54
+ export declare function hasPendingAuthentikCallback(searchString?: string): boolean;
55
+ export declare function readOidcSession(config?: AuthentikOidcConfig): OidcSession | null;
56
+ export declare function clearOidcSession(config?: AuthentikOidcConfig): void;
57
+ export declare function startAuthentikOAuthFlow(provider: OidcProvider, config?: AuthentikOidcConfig): Promise<void>;
58
+ export declare function handleAuthentikCallback(searchString?: string, config?: AuthentikOidcConfig): Promise<OidcSession>;
@@ -0,0 +1,284 @@
1
+ const DEFAULT_ENV_KEYS = {
2
+ issuer: "EXPO_PUBLIC_AUTHENTIK_ISSUER",
3
+ clientId: "EXPO_PUBLIC_AUTHENTIK_CLIENT_ID",
4
+ redirectUri: "EXPO_PUBLIC_AUTHENTIK_REDIRECT_URI"
5
+ };
6
+ const DEFAULT_SCOPE = "openid profile email";
7
+ const DEFAULT_STORAGE_KEY = "authentik:oidc:session";
8
+ const DEFAULT_PENDING_STORAGE_KEY = "authentik:oidc:pending";
9
+ export const OIDC_INITIAL_SEARCH = "authentik:oidc:initial-search";
10
+ function isBrowserRuntime() {
11
+ return typeof window !== "undefined";
12
+ }
13
+ function getProcessEnv() {
14
+ const maybeProcess = globalThis.process;
15
+ return maybeProcess?.env || {};
16
+ }
17
+ function resolveEnvValue(config, key) {
18
+ const envKeys = { ...DEFAULT_ENV_KEYS, ...(config.envKeys || {}) };
19
+ const explicit = key === "issuer" ? config.issuer : key === "clientId" ? config.clientId : config.redirectUri;
20
+ if (explicit && explicit.trim()) {
21
+ return explicit.trim();
22
+ }
23
+ const envSource = config.env || getProcessEnv();
24
+ const envKey = envKeys[key];
25
+ return envSource[envKey]?.trim();
26
+ }
27
+ function ensurePathSuffix(pathname) {
28
+ return pathname.endsWith("/") ? pathname : `${pathname}/`;
29
+ }
30
+ function resolveEndpoint(issuer, explicitPath, fallbackPath) {
31
+ const issuerUrl = new URL(issuer);
32
+ if (explicitPath) {
33
+ return new URL(explicitPath, `${issuerUrl.origin}/`).toString();
34
+ }
35
+ // Authentik OAuth endpoints (authorize, token, userinfo) live at the parent
36
+ // of the issuer path: issuer = /application/o/<slug>/, endpoint = /application/o/<ep>/.
37
+ const base = ensurePathSuffix(issuer);
38
+ return new URL(`../${fallbackPath}`, base).toString();
39
+ }
40
+ function getSessionStorage(config) {
41
+ if (config.sessionStorage) {
42
+ return config.sessionStorage;
43
+ }
44
+ if (!isBrowserRuntime() || !window.sessionStorage) {
45
+ throw new Error("CONFIG_ERROR: sessionStorage is unavailable in this runtime");
46
+ }
47
+ return window.sessionStorage;
48
+ }
49
+ function getLocalStorage(config) {
50
+ if (config.localStorage) {
51
+ return config.localStorage;
52
+ }
53
+ if (!isBrowserRuntime() || !window.localStorage) {
54
+ throw new Error("CONFIG_ERROR: localStorage is unavailable in this runtime");
55
+ }
56
+ return window.localStorage;
57
+ }
58
+ function getFetch(config) {
59
+ if (config.fetchFn) {
60
+ return config.fetchFn;
61
+ }
62
+ if (typeof fetch === "undefined") {
63
+ throw new Error("CONFIG_ERROR: fetch is unavailable in this runtime");
64
+ }
65
+ return fetch;
66
+ }
67
+ function resolveConfig(config = {}) {
68
+ const issuer = resolveEnvValue(config, "issuer");
69
+ const clientId = resolveEnvValue(config, "clientId");
70
+ const redirectUri = resolveEnvValue(config, "redirectUri");
71
+ if (!issuer || !clientId || !redirectUri) {
72
+ throw new Error("CONFIG_ERROR: Missing Authentik issuer, clientId, or redirectUri");
73
+ }
74
+ return {
75
+ issuer,
76
+ clientId,
77
+ redirectUri,
78
+ scope: config.scope || DEFAULT_SCOPE,
79
+ authorizePath: resolveEndpoint(issuer, config.authorizePath, "authorize/"),
80
+ tokenPath: resolveEndpoint(issuer, config.tokenPath, "token/"),
81
+ userinfoPath: resolveEndpoint(issuer, config.userinfoPath, "userinfo/"),
82
+ storageKey: config.storageKey || DEFAULT_STORAGE_KEY,
83
+ pendingStorageKey: config.pendingStorageKey || DEFAULT_PENDING_STORAGE_KEY,
84
+ providerSourceSlugs: config.providerSourceSlugs || {},
85
+ sessionStorage: getSessionStorage(config),
86
+ localStorage: getLocalStorage(config),
87
+ fetchFn: getFetch(config),
88
+ onSessionReady: config.onSessionReady
89
+ };
90
+ }
91
+ function encodeBase64Url(bytes) {
92
+ let binary = "";
93
+ for (const byte of bytes) {
94
+ binary += String.fromCharCode(byte);
95
+ }
96
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
97
+ }
98
+ function randomString(length) {
99
+ const bytes = new Uint8Array(length);
100
+ crypto.getRandomValues(bytes);
101
+ return encodeBase64Url(bytes);
102
+ }
103
+ async function sha256(input) {
104
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
105
+ return new Uint8Array(digest);
106
+ }
107
+ async function buildPkcePair() {
108
+ const verifier = randomString(64);
109
+ const challenge = encodeBase64Url(await sha256(verifier));
110
+ return { verifier, challenge };
111
+ }
112
+ function parsePendingState(rawValue) {
113
+ if (!rawValue) {
114
+ return null;
115
+ }
116
+ try {
117
+ return JSON.parse(rawValue);
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ function getSourceSlug(provider, config) {
124
+ return config.providerSourceSlugs[provider] || provider;
125
+ }
126
+ export function isAuthentikConfigured(config = {}) {
127
+ try {
128
+ const issuer = resolveEnvValue(config, "issuer");
129
+ const clientId = resolveEnvValue(config, "clientId");
130
+ const redirectUri = resolveEnvValue(config, "redirectUri");
131
+ return Boolean(issuer && clientId && redirectUri);
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ export function hasPendingAuthentikCallback(searchString) {
138
+ const rawSearch = typeof searchString === "string"
139
+ ? searchString
140
+ : isBrowserRuntime()
141
+ ? window.location.search
142
+ : "";
143
+ const params = new URLSearchParams(rawSearch.startsWith("?") ? rawSearch.slice(1) : rawSearch);
144
+ return Boolean(params.get("code") && params.get("state"));
145
+ }
146
+ export function readOidcSession(config = {}) {
147
+ const storage = config.localStorage || (isBrowserRuntime() ? window.localStorage : null);
148
+ if (!storage) {
149
+ return null;
150
+ }
151
+ const key = config.storageKey || DEFAULT_STORAGE_KEY;
152
+ const rawValue = storage.getItem(key);
153
+ if (!rawValue) {
154
+ return null;
155
+ }
156
+ try {
157
+ return JSON.parse(rawValue);
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ export function clearOidcSession(config = {}) {
164
+ const localStorageRef = config.localStorage || (isBrowserRuntime() ? window.localStorage : null);
165
+ const sessionStorageRef = config.sessionStorage || (isBrowserRuntime() ? window.sessionStorage : null);
166
+ const sessionKey = config.storageKey || DEFAULT_STORAGE_KEY;
167
+ const pendingKey = config.pendingStorageKey || DEFAULT_PENDING_STORAGE_KEY;
168
+ localStorageRef?.removeItem(sessionKey);
169
+ sessionStorageRef?.removeItem(pendingKey);
170
+ sessionStorageRef?.removeItem(OIDC_INITIAL_SEARCH);
171
+ }
172
+ export async function startAuthentikOAuthFlow(provider, config = {}) {
173
+ if (!isBrowserRuntime()) {
174
+ throw new Error("CONFIG_ERROR: startAuthentikOAuthFlow requires a browser runtime");
175
+ }
176
+ const resolved = resolveConfig(config);
177
+ const { verifier, challenge } = await buildPkcePair();
178
+ const state = randomString(32);
179
+ const pendingState = {
180
+ state,
181
+ provider,
182
+ codeVerifier: verifier,
183
+ createdAt: Date.now()
184
+ };
185
+ resolved.sessionStorage.setItem(resolved.pendingStorageKey, JSON.stringify(pendingState));
186
+ resolved.sessionStorage.setItem(OIDC_INITIAL_SEARCH, window.location.search || "");
187
+ const authorizeUrl = new URL(resolved.authorizePath);
188
+ authorizeUrl.searchParams.set("response_type", "code");
189
+ authorizeUrl.searchParams.set("client_id", resolved.clientId);
190
+ authorizeUrl.searchParams.set("redirect_uri", resolved.redirectUri);
191
+ authorizeUrl.searchParams.set("scope", resolved.scope);
192
+ authorizeUrl.searchParams.set("state", state);
193
+ authorizeUrl.searchParams.set("code_challenge", challenge);
194
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
195
+ const loginUrl = new URL(`/source/oauth/login/${encodeURIComponent(getSourceSlug(provider, resolved))}/`, new URL(resolved.issuer).origin);
196
+ loginUrl.searchParams.set("next", authorizeUrl.toString());
197
+ window.location.assign(loginUrl.toString());
198
+ }
199
+ export async function handleAuthentikCallback(searchString, config = {}) {
200
+ const resolved = resolveConfig(config);
201
+ const rawSearch = typeof searchString === "string"
202
+ ? searchString
203
+ : isBrowserRuntime()
204
+ ? window.location.search
205
+ : "";
206
+ const params = new URLSearchParams(rawSearch.startsWith("?") ? rawSearch.slice(1) : rawSearch);
207
+ const error = params.get("error");
208
+ if (error) {
209
+ const description = params.get("error_description") || "OAuth callback returned an error";
210
+ throw new Error(`PROVIDER_ERROR: ${error} (${description})`);
211
+ }
212
+ const code = params.get("code");
213
+ const state = params.get("state");
214
+ if (!code || !state) {
215
+ throw new Error("SESSION_ERROR: Missing code or state in Authentik callback");
216
+ }
217
+ const pending = parsePendingState(resolved.sessionStorage.getItem(resolved.pendingStorageKey));
218
+ if (!pending) {
219
+ throw new Error("SESSION_ERROR: Missing pending Authentik state in sessionStorage");
220
+ }
221
+ if (pending.state !== state) {
222
+ throw new Error("SESSION_ERROR: Invalid Authentik callback state");
223
+ }
224
+ const tokenPayload = new URLSearchParams({
225
+ grant_type: "authorization_code",
226
+ code,
227
+ redirect_uri: resolved.redirectUri,
228
+ client_id: resolved.clientId,
229
+ code_verifier: pending.codeVerifier
230
+ });
231
+ const tokenResponse = await resolved.fetchFn(resolved.tokenPath, {
232
+ method: "POST",
233
+ headers: {
234
+ "Content-Type": "application/x-www-form-urlencoded"
235
+ },
236
+ body: tokenPayload
237
+ });
238
+ if (!tokenResponse.ok) {
239
+ throw new Error(`NETWORK_ERROR: Token exchange failed with status ${tokenResponse.status}`);
240
+ }
241
+ const tokenJson = (await tokenResponse.json());
242
+ if (!tokenJson.access_token) {
243
+ throw new Error("SESSION_ERROR: Token response missing access_token");
244
+ }
245
+ const userinfoResponse = await resolved.fetchFn(resolved.userinfoPath, {
246
+ method: "GET",
247
+ headers: {
248
+ Authorization: `Bearer ${tokenJson.access_token}`
249
+ }
250
+ });
251
+ if (!userinfoResponse.ok) {
252
+ throw new Error(`NETWORK_ERROR: Userinfo request failed with status ${userinfoResponse.status}`);
253
+ }
254
+ const claims = (await userinfoResponse.json());
255
+ if (!claims.sub || !claims.iss) {
256
+ throw new Error("SESSION_ERROR: Userinfo response missing required claims (sub, iss)");
257
+ }
258
+ const now = Date.now();
259
+ const expiresAt = tokenJson.expires_in ? now + tokenJson.expires_in * 1000 : undefined;
260
+ const tokens = {
261
+ accessToken: tokenJson.access_token,
262
+ tokenType: tokenJson.token_type,
263
+ refreshToken: tokenJson.refresh_token,
264
+ idToken: tokenJson.id_token,
265
+ expiresIn: tokenJson.expires_in,
266
+ expiresAt,
267
+ scope: tokenJson.scope
268
+ };
269
+ const session = {
270
+ provider: pending.provider,
271
+ issuer: resolved.issuer,
272
+ clientId: resolved.clientId,
273
+ claims,
274
+ tokens,
275
+ createdAt: now
276
+ };
277
+ resolved.localStorage.setItem(resolved.storageKey, JSON.stringify(session));
278
+ resolved.sessionStorage.removeItem(resolved.pendingStorageKey);
279
+ if (resolved.onSessionReady) {
280
+ await resolved.onSessionReady(claims, tokens, session);
281
+ }
282
+ return session;
283
+ }
284
+ //# sourceMappingURL=AuthentikOidcClient.js.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Enhanced Authentik callback handler with blocking provisioning support.
3
+ *
4
+ * This module handles the OIDC authorization code exchange and optionally
5
+ * calls a provisioning adapter **before** allowing the app to redirect
6
+ * into the protected product.
7
+ *
8
+ * Reference: CIG apps/dashboard/app/auth/callback/page.tsx
9
+ */
10
+ import type { AuthentikCallbackConfig, AuthentikCallbackResult, AuthentikTokenResponse, AuthentikClaims, ProvisioningAdapter, ProvisioningResult } from "./types";
11
+ /**
12
+ * Exchange an authorization code for tokens using PKCE.
13
+ *
14
+ * This is a pure server-safe function — it does not touch sessionStorage.
15
+ */
16
+ export declare function exchangeCode(config: AuthentikCallbackConfig, code: string, codeVerifier: string): Promise<AuthentikTokenResponse>;
17
+ /**
18
+ * Fetch OIDC claims from the Authentik userinfo endpoint.
19
+ */
20
+ export declare function fetchClaims(config: AuthentikCallbackConfig, accessToken: string): Promise<AuthentikClaims>;
21
+ /**
22
+ * Options for the full callback handler.
23
+ */
24
+ export interface ProcessCallbackOptions {
25
+ /** Callback config (issuer, clientId, redirectUri, etc.). */
26
+ config: AuthentikCallbackConfig;
27
+ /** The authorization code from the callback query string. */
28
+ code: string;
29
+ /** The PKCE code verifier stored by the relay or startOAuthFlow. */
30
+ codeVerifier: string;
31
+ /** The state token from the callback query string. */
32
+ state: string;
33
+ /** The expected state token (from sessionStorage / relay). */
34
+ expectedState: string;
35
+ /** The social-login provider that initiated the flow. */
36
+ provider: string;
37
+ /**
38
+ * Optional provisioning adapter.
39
+ * When provided the callback will **block** until provisioning succeeds.
40
+ * If provisioning fails the result will contain the error.
41
+ */
42
+ provisioningAdapter?: ProvisioningAdapter;
43
+ }
44
+ /**
45
+ * Result of the full callback flow.
46
+ */
47
+ export interface ProcessCallbackResult {
48
+ /** Whether the entire flow (exchange + provisioning) succeeded. */
49
+ success: boolean;
50
+ /** Tokens + claims from the exchange step. */
51
+ callbackResult?: AuthentikCallbackResult;
52
+ /** Provisioning result (only present when an adapter was provided). */
53
+ provisioningResult?: ProvisioningResult;
54
+ /** Error message on failure. */
55
+ error?: string;
56
+ /** Machine-readable error code on failure. */
57
+ errorCode?: string;
58
+ }
59
+ /**
60
+ * Process an Authentik OIDC callback end-to-end:
61
+ *
62
+ * 1. Validate state matches
63
+ * 2. Exchange the authorization code for tokens
64
+ * 3. Fetch OIDC claims from userinfo
65
+ * 4. (Optional) Run the provisioning adapter — blocks until complete
66
+ * 5. Return the combined result
67
+ *
68
+ * If any step fails the function returns `{ success: false, error }`.
69
+ * It does **not** throw so that callers can present structured error UI.
70
+ */
71
+ export declare function processCallback(options: ProcessCallbackOptions): Promise<ProcessCallbackResult>;