@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.1] - 2026-03-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- ๐ **Documentation**: Added `packages/auth/docs/` with five guides: `authentik-integration-guide.md`, `provisioning-model.md`, `upgrade-migration.md`, `nextjs-examples.md`, `cig-reference-map.md`.
|
|
8
|
+
- Updated README with documentation table and `@edcalderon/auth/authentik` subpath listing.
|
|
9
|
+
|
|
10
|
+
## [1.4.0] - 2026-03-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- โจ **Authentik flow + provisioning kit** (`@edcalderon/auth/authentik`) โ a reusable set of helpers generalised from the production CIG Authentik implementation.
|
|
15
|
+
- ๐ **Cross-origin PKCE relay** โ `createRelayPageHtml()`, `parseRelayParams()`, `readRelayStorage()`, `clearRelayStorage()` for apps where login UI and callback handler live on different origins.
|
|
16
|
+
- ๐ **Enhanced callback handler** โ `exchangeCode()`, `fetchClaims()`, `processCallback()` with blocking provisioning gate that prevents redirect until user sync completes.
|
|
17
|
+
- ๐ช **Logout orchestrator** โ `revokeToken()`, `buildEndSessionUrl()`, `orchestrateLogout()` implementing the full RP-initiated logout flow.
|
|
18
|
+
- ๐ **Provisioning adapter layer** โ pluggable adapters: `NoopProvisioningAdapter`, `createProvisioningAdapter()`, `SupabaseSyncAdapter` with identity-first matching and rollback on failure.
|
|
19
|
+
- ๐ฅ **Config validation / doctor** โ `validateAuthentikConfig()`, `validateSupabaseSyncConfig()`, `validateFullConfig()` for startup / deploy-time validation (detects `supabase_not_configured`).
|
|
20
|
+
- ๐ก๏ธ **Safe redirect resolver** โ `resolveSafeRedirect()` with origin allowlist to prevent open-redirect vulnerabilities.
|
|
21
|
+
- ๐ฆ **New subpath export** โ `@edcalderon/auth/authentik` barrel export for all Authentik-specific modules.
|
|
22
|
+
- ๐๏ธ **SQL migration 003** โ `003_authentik_shadow_auth_users.sql` adds shadow auth user linkage columns and `link_shadow_auth_user()` RPC.
|
|
23
|
+
- ๐งช **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
|
+
|
|
3
25
|
## [1.3.0] - 2026-03-19
|
|
4
26
|
|
|
5
27
|
### Added
|
package/README.md
CHANGED
|
@@ -11,13 +11,12 @@ Swap between Supabase, Firebase, Hybrid, or any custom provider without changing
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
## ๐ Latest Changes (v1.
|
|
14
|
+
## ๐ Latest Changes (v1.4.1)
|
|
15
15
|
|
|
16
16
|
### Added
|
|
17
17
|
|
|
18
|
-
- Added
|
|
19
|
-
-
|
|
20
|
-
- Added README guidance for Authentik setup and the known Authentik `2026.2.1` social re-link bug workaround.
|
|
18
|
+
- ๐ **Documentation**: Added `packages/auth/docs/` with five guides: `authentik-integration-guide.md`, `provisioning-model.md`, `upgrade-migration.md`, `nextjs-examples.md`, `cig-reference-map.md`.
|
|
19
|
+
- Updated README with documentation table and `@edcalderon/auth/authentik` subpath listing.
|
|
21
20
|
|
|
22
21
|
For full version history, see [CHANGELOG.md](./CHANGELOG.md) and [GitHub releases](https://github.com/edcalderon/my-second-brain/releases)
|
|
23
22
|
|
|
@@ -150,6 +149,7 @@ The package avoids bleeding `window` or `document` objects into Expo bundles or
|
|
|
150
149
|
- `@edcalderon/auth/firebase-native`
|
|
151
150
|
- `@edcalderon/auth/hybrid-web`
|
|
152
151
|
- `@edcalderon/auth/hybrid-native`
|
|
152
|
+
- `@edcalderon/auth/authentik` (Authentik flow + provisioning kit โ [docs](./docs/authentik-integration-guide.md))
|
|
153
153
|
|
|
154
154
|
---
|
|
155
155
|
|
|
@@ -32,8 +32,10 @@ function resolveEndpoint(issuer, explicitPath, fallbackPath) {
|
|
|
32
32
|
if (explicitPath) {
|
|
33
33
|
return new URL(explicitPath, `${issuerUrl.origin}/`).toString();
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
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();
|
|
37
39
|
}
|
|
38
40
|
function getSessionStorage(config) {
|
|
39
41
|
if (config.sessionStorage) {
|
|
@@ -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>;
|
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
/* ------------------------------------------------------------------ */
|
|
11
|
+
/* Token exchange */
|
|
12
|
+
/* ------------------------------------------------------------------ */
|
|
13
|
+
/**
|
|
14
|
+
* Exchange an authorization code for tokens using PKCE.
|
|
15
|
+
*
|
|
16
|
+
* This is a pure server-safe function โ it does not touch sessionStorage.
|
|
17
|
+
*/
|
|
18
|
+
export async function exchangeCode(config, code, codeVerifier) {
|
|
19
|
+
const tokenUrl = config.tokenEndpoint;
|
|
20
|
+
const fetchFn = config.fetchFn || fetch;
|
|
21
|
+
const body = new URLSearchParams({
|
|
22
|
+
grant_type: "authorization_code",
|
|
23
|
+
code,
|
|
24
|
+
redirect_uri: config.redirectUri,
|
|
25
|
+
client_id: config.clientId,
|
|
26
|
+
code_verifier: codeVerifier,
|
|
27
|
+
});
|
|
28
|
+
const response = await fetchFn(tokenUrl, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
31
|
+
body,
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`NETWORK_ERROR: Token exchange failed (HTTP ${response.status})`);
|
|
35
|
+
}
|
|
36
|
+
const json = (await response.json());
|
|
37
|
+
if (!json.access_token || typeof json.access_token !== "string") {
|
|
38
|
+
throw new Error("SESSION_ERROR: Token response missing access_token");
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
access_token: json.access_token,
|
|
42
|
+
token_type: json.token_type ?? undefined,
|
|
43
|
+
refresh_token: json.refresh_token ?? undefined,
|
|
44
|
+
id_token: json.id_token ?? undefined,
|
|
45
|
+
expires_in: typeof json.expires_in === "number" ? json.expires_in : undefined,
|
|
46
|
+
scope: json.scope ?? undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/* ------------------------------------------------------------------ */
|
|
50
|
+
/* Userinfo */
|
|
51
|
+
/* ------------------------------------------------------------------ */
|
|
52
|
+
/**
|
|
53
|
+
* Fetch OIDC claims from the Authentik userinfo endpoint.
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchClaims(config, accessToken) {
|
|
56
|
+
const userinfoUrl = config.userinfoEndpoint;
|
|
57
|
+
const fetchFn = config.fetchFn || fetch;
|
|
58
|
+
const response = await fetchFn(userinfoUrl, {
|
|
59
|
+
method: "GET",
|
|
60
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`NETWORK_ERROR: Userinfo request failed (HTTP ${response.status})`);
|
|
64
|
+
}
|
|
65
|
+
const claims = (await response.json());
|
|
66
|
+
if (!claims.sub || !claims.iss) {
|
|
67
|
+
throw new Error("SESSION_ERROR: Userinfo response missing required claims (sub, iss)");
|
|
68
|
+
}
|
|
69
|
+
return claims;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Process an Authentik OIDC callback end-to-end:
|
|
73
|
+
*
|
|
74
|
+
* 1. Validate state matches
|
|
75
|
+
* 2. Exchange the authorization code for tokens
|
|
76
|
+
* 3. Fetch OIDC claims from userinfo
|
|
77
|
+
* 4. (Optional) Run the provisioning adapter โ blocks until complete
|
|
78
|
+
* 5. Return the combined result
|
|
79
|
+
*
|
|
80
|
+
* If any step fails the function returns `{ success: false, error }`.
|
|
81
|
+
* It does **not** throw so that callers can present structured error UI.
|
|
82
|
+
*/
|
|
83
|
+
export async function processCallback(options) {
|
|
84
|
+
// 1. State validation
|
|
85
|
+
if (options.state !== options.expectedState) {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
error: "Invalid callback state โ possible CSRF or expired session",
|
|
89
|
+
errorCode: "state_mismatch",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// 2. Token exchange
|
|
93
|
+
let tokens;
|
|
94
|
+
try {
|
|
95
|
+
tokens = await exchangeCode(options.config, options.code, options.codeVerifier);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
error: err instanceof Error ? err.message : "Token exchange failed",
|
|
101
|
+
errorCode: "token_exchange_failed",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// 3. Fetch claims
|
|
105
|
+
let claims;
|
|
106
|
+
try {
|
|
107
|
+
claims = await fetchClaims(options.config, tokens.access_token);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
error: err instanceof Error ? err.message : "Userinfo fetch failed",
|
|
113
|
+
errorCode: "userinfo_failed",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const callbackResult = {
|
|
117
|
+
tokens,
|
|
118
|
+
claims,
|
|
119
|
+
provider: options.provider,
|
|
120
|
+
};
|
|
121
|
+
// 4. Provisioning gate
|
|
122
|
+
if (options.provisioningAdapter) {
|
|
123
|
+
const payload = {
|
|
124
|
+
sub: claims.sub,
|
|
125
|
+
iss: claims.iss,
|
|
126
|
+
email: (claims.email || "").toLowerCase(),
|
|
127
|
+
emailVerified: claims.email_verified,
|
|
128
|
+
name: claims.name || claims.preferred_username,
|
|
129
|
+
picture: claims.picture,
|
|
130
|
+
provider: options.provider,
|
|
131
|
+
rawClaims: claims,
|
|
132
|
+
};
|
|
133
|
+
let provisioningResult;
|
|
134
|
+
try {
|
|
135
|
+
provisioningResult = await options.provisioningAdapter.sync(payload);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
callbackResult,
|
|
141
|
+
error: err instanceof Error ? err.message : "Provisioning failed",
|
|
142
|
+
errorCode: "provisioning_error",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (!provisioningResult.synced) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
callbackResult,
|
|
149
|
+
provisioningResult,
|
|
150
|
+
error: provisioningResult.error || "User provisioning did not complete",
|
|
151
|
+
errorCode: provisioningResult.errorCode || "provisioning_failed",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
callbackResult,
|
|
157
|
+
provisioningResult,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// No adapter โ exchange-only mode
|
|
161
|
+
return { success: true, callbackResult };
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=callback.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration validation / doctor helpers.
|
|
3
|
+
*
|
|
4
|
+
* These helpers detect missing or invalid configuration **before** the
|
|
5
|
+
* first real user login, so misconfigured environments fail fast.
|
|
6
|
+
*
|
|
7
|
+
* Reference: CIG `supabase_not_configured` callback error code.
|
|
8
|
+
*/
|
|
9
|
+
import type { ConfigValidationResult, AuthentikCallbackConfig, AuthentikEndpoints, SupabaseSyncConfig } from "./types";
|
|
10
|
+
/**
|
|
11
|
+
* Validate that the core Authentik OIDC configuration is present and
|
|
12
|
+
* syntactically correct.
|
|
13
|
+
*
|
|
14
|
+
* This does **not** make network requests โ it only checks that the
|
|
15
|
+
* required values are set and look reasonable.
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateAuthentikConfig(config: Partial<AuthentikCallbackConfig>): ConfigValidationResult;
|
|
18
|
+
/**
|
|
19
|
+
* Validate the server-side Supabase sync configuration.
|
|
20
|
+
*
|
|
21
|
+
* Returns a diagnostic result with the exact error code
|
|
22
|
+
* `supabase_not_configured` when the Supabase URL or service role key
|
|
23
|
+
* is missing, matching the CIG convention.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateSupabaseSyncConfig(config: Partial<SupabaseSyncConfig>): ConfigValidationResult;
|
|
26
|
+
/**
|
|
27
|
+
* Validate both Authentik and Supabase sync configuration in one call.
|
|
28
|
+
*
|
|
29
|
+
* Useful as a startup / health-check / deploy-time validation gate.
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateFullConfig(authentikConfig: Partial<AuthentikCallbackConfig>, supabaseConfig: Partial<SupabaseSyncConfig>): ConfigValidationResult;
|
|
32
|
+
/**
|
|
33
|
+
* Discover OIDC endpoint URLs from an Authentik issuer's
|
|
34
|
+
* `.well-known/openid-configuration`.
|
|
35
|
+
*
|
|
36
|
+
* **Important:** Authentik places most OIDC endpoints (token, userinfo,
|
|
37
|
+
* authorize, revocation) at the `/application/o/` level, *not* under the
|
|
38
|
+
* per-app issuer path. For example, with:
|
|
39
|
+
* issuer = `https://auth.example.com/application/o/my-app/`
|
|
40
|
+
* the token endpoint is:
|
|
41
|
+
* `https://auth.example.com/application/o/token/`
|
|
42
|
+
*
|
|
43
|
+
* This function fetches the correct endpoint URLs from the well-known
|
|
44
|
+
* document so callers never need to guess.
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* const endpoints = await discoverEndpoints("https://auth.example.com/application/o/my-app/");
|
|
48
|
+
* // endpoints.token โ "https://auth.example.com/application/o/token/"
|
|
49
|
+
* // endpoints.userinfo โ "https://auth.example.com/application/o/userinfo/"
|
|
50
|
+
* // endpoints.endSession โ "https://auth.example.com/application/o/my-app/end-session/"
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function discoverEndpoints(issuer: string, fetchFn?: typeof fetch): Promise<AuthentikEndpoints>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration validation / doctor helpers.
|
|
3
|
+
*
|
|
4
|
+
* These helpers detect missing or invalid configuration **before** the
|
|
5
|
+
* first real user login, so misconfigured environments fail fast.
|
|
6
|
+
*
|
|
7
|
+
* Reference: CIG `supabase_not_configured` callback error code.
|
|
8
|
+
*/
|
|
9
|
+
/* ------------------------------------------------------------------ */
|
|
10
|
+
/* Authentik config validation */
|
|
11
|
+
/* ------------------------------------------------------------------ */
|
|
12
|
+
/**
|
|
13
|
+
* Validate that the core Authentik OIDC configuration is present and
|
|
14
|
+
* syntactically correct.
|
|
15
|
+
*
|
|
16
|
+
* This does **not** make network requests โ it only checks that the
|
|
17
|
+
* required values are set and look reasonable.
|
|
18
|
+
*/
|
|
19
|
+
export function validateAuthentikConfig(config) {
|
|
20
|
+
const checks = [];
|
|
21
|
+
// issuer
|
|
22
|
+
checks.push(config.issuer
|
|
23
|
+
? isValidUrl(config.issuer)
|
|
24
|
+
? { name: "issuer", passed: true, message: "Issuer URL is valid", severity: "error" }
|
|
25
|
+
: { name: "issuer", passed: false, message: `Issuer is not a valid URL: ${config.issuer}`, severity: "error" }
|
|
26
|
+
: { name: "issuer", passed: false, message: "Authentik issuer URL is required", severity: "error" });
|
|
27
|
+
// clientId
|
|
28
|
+
checks.push(config.clientId
|
|
29
|
+
? { name: "clientId", passed: true, message: "Client ID is set", severity: "error" }
|
|
30
|
+
: { name: "clientId", passed: false, message: "Authentik client_id is required", severity: "error" });
|
|
31
|
+
// redirectUri
|
|
32
|
+
checks.push(config.redirectUri
|
|
33
|
+
? isValidUrl(config.redirectUri)
|
|
34
|
+
? { name: "redirectUri", passed: true, message: "Redirect URI is valid", severity: "error" }
|
|
35
|
+
: { name: "redirectUri", passed: false, message: `Redirect URI is not a valid URL: ${config.redirectUri}`, severity: "error" }
|
|
36
|
+
: { name: "redirectUri", passed: false, message: "Redirect URI is required", severity: "error" });
|
|
37
|
+
// tokenEndpoint
|
|
38
|
+
checks.push(config.tokenEndpoint
|
|
39
|
+
? isValidUrl(config.tokenEndpoint)
|
|
40
|
+
? { name: "tokenEndpoint", passed: true, message: "Token endpoint URL is valid", severity: "error" }
|
|
41
|
+
: { name: "tokenEndpoint", passed: false, message: `Token endpoint is not a valid URL: ${config.tokenEndpoint}`, severity: "error" }
|
|
42
|
+
: { name: "tokenEndpoint", passed: false, message: "Token endpoint URL is required โ use discoverEndpoints() or supply manually", severity: "error" });
|
|
43
|
+
// userinfoEndpoint
|
|
44
|
+
checks.push(config.userinfoEndpoint
|
|
45
|
+
? isValidUrl(config.userinfoEndpoint)
|
|
46
|
+
? { name: "userinfoEndpoint", passed: true, message: "Userinfo endpoint URL is valid", severity: "error" }
|
|
47
|
+
: { name: "userinfoEndpoint", passed: false, message: `Userinfo endpoint is not a valid URL: ${config.userinfoEndpoint}`, severity: "error" }
|
|
48
|
+
: { name: "userinfoEndpoint", passed: false, message: "Userinfo endpoint URL is required โ use discoverEndpoints() or supply manually", severity: "error" });
|
|
49
|
+
return {
|
|
50
|
+
valid: checks.every((c) => c.passed),
|
|
51
|
+
checks,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/* ------------------------------------------------------------------ */
|
|
55
|
+
/* Supabase sync config validation */
|
|
56
|
+
/* ------------------------------------------------------------------ */
|
|
57
|
+
/**
|
|
58
|
+
* Validate the server-side Supabase sync configuration.
|
|
59
|
+
*
|
|
60
|
+
* Returns a diagnostic result with the exact error code
|
|
61
|
+
* `supabase_not_configured` when the Supabase URL or service role key
|
|
62
|
+
* is missing, matching the CIG convention.
|
|
63
|
+
*/
|
|
64
|
+
export function validateSupabaseSyncConfig(config) {
|
|
65
|
+
const checks = [];
|
|
66
|
+
// supabaseUrl
|
|
67
|
+
checks.push(config.supabaseUrl
|
|
68
|
+
? isValidUrl(config.supabaseUrl)
|
|
69
|
+
? { name: "supabaseUrl", passed: true, message: "Supabase URL is valid", severity: "error" }
|
|
70
|
+
: { name: "supabaseUrl", passed: false, message: `Supabase URL is not a valid URL: ${config.supabaseUrl}`, severity: "error" }
|
|
71
|
+
: { name: "supabaseUrl", passed: false, message: "supabase_not_configured: Supabase URL is required for sync", severity: "error" });
|
|
72
|
+
// supabaseServiceRoleKey
|
|
73
|
+
checks.push(config.supabaseServiceRoleKey
|
|
74
|
+
? config.supabaseServiceRoleKey.length >= 20
|
|
75
|
+
? { name: "supabaseServiceRoleKey", passed: true, message: "Service role key is set", severity: "error" }
|
|
76
|
+
: { name: "supabaseServiceRoleKey", passed: false, message: "Service role key appears too short", severity: "warning" }
|
|
77
|
+
: { name: "supabaseServiceRoleKey", passed: false, message: "supabase_not_configured: Supabase service_role key is required for sync", severity: "error" });
|
|
78
|
+
// upsertRpcName (optional, warn if customised)
|
|
79
|
+
if (config.upsertRpcName && config.upsertRpcName !== "upsert_oidc_user") {
|
|
80
|
+
checks.push({
|
|
81
|
+
name: "upsertRpcName",
|
|
82
|
+
passed: true,
|
|
83
|
+
message: `Custom RPC name: ${config.upsertRpcName}`,
|
|
84
|
+
severity: "warning",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
valid: checks.filter((c) => c.severity === "error").every((c) => c.passed),
|
|
89
|
+
checks,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/* ------------------------------------------------------------------ */
|
|
93
|
+
/* Combined validation */
|
|
94
|
+
/* ------------------------------------------------------------------ */
|
|
95
|
+
/**
|
|
96
|
+
* Validate both Authentik and Supabase sync configuration in one call.
|
|
97
|
+
*
|
|
98
|
+
* Useful as a startup / health-check / deploy-time validation gate.
|
|
99
|
+
*/
|
|
100
|
+
export function validateFullConfig(authentikConfig, supabaseConfig) {
|
|
101
|
+
const authentik = validateAuthentikConfig(authentikConfig);
|
|
102
|
+
const supabase = validateSupabaseSyncConfig(supabaseConfig);
|
|
103
|
+
const checks = [...authentik.checks, ...supabase.checks];
|
|
104
|
+
return {
|
|
105
|
+
valid: checks.filter((c) => c.severity === "error").every((c) => c.passed),
|
|
106
|
+
checks,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/* ------------------------------------------------------------------ */
|
|
110
|
+
/* Helpers */
|
|
111
|
+
/* ------------------------------------------------------------------ */
|
|
112
|
+
function isValidUrl(value) {
|
|
113
|
+
try {
|
|
114
|
+
new URL(value);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/* ------------------------------------------------------------------ */
|
|
122
|
+
/* Endpoint discovery */
|
|
123
|
+
/* ------------------------------------------------------------------ */
|
|
124
|
+
/**
|
|
125
|
+
* Discover OIDC endpoint URLs from an Authentik issuer's
|
|
126
|
+
* `.well-known/openid-configuration`.
|
|
127
|
+
*
|
|
128
|
+
* **Important:** Authentik places most OIDC endpoints (token, userinfo,
|
|
129
|
+
* authorize, revocation) at the `/application/o/` level, *not* under the
|
|
130
|
+
* per-app issuer path. For example, with:
|
|
131
|
+
* issuer = `https://auth.example.com/application/o/my-app/`
|
|
132
|
+
* the token endpoint is:
|
|
133
|
+
* `https://auth.example.com/application/o/token/`
|
|
134
|
+
*
|
|
135
|
+
* This function fetches the correct endpoint URLs from the well-known
|
|
136
|
+
* document so callers never need to guess.
|
|
137
|
+
*
|
|
138
|
+
* ```ts
|
|
139
|
+
* const endpoints = await discoverEndpoints("https://auth.example.com/application/o/my-app/");
|
|
140
|
+
* // endpoints.token โ "https://auth.example.com/application/o/token/"
|
|
141
|
+
* // endpoints.userinfo โ "https://auth.example.com/application/o/userinfo/"
|
|
142
|
+
* // endpoints.endSession โ "https://auth.example.com/application/o/my-app/end-session/"
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export async function discoverEndpoints(issuer, fetchFn = fetch) {
|
|
146
|
+
const base = issuer.endsWith("/") ? issuer : `${issuer}/`;
|
|
147
|
+
const wellKnownUrl = `${base}.well-known/openid-configuration`;
|
|
148
|
+
const response = await fetchFn(wellKnownUrl);
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
throw new Error(`Failed to fetch .well-known/openid-configuration from ${wellKnownUrl} (HTTP ${response.status})`);
|
|
151
|
+
}
|
|
152
|
+
const doc = (await response.json());
|
|
153
|
+
const authorization = doc.authorization_endpoint;
|
|
154
|
+
const token = doc.token_endpoint;
|
|
155
|
+
const userinfo = doc.userinfo_endpoint;
|
|
156
|
+
if (typeof authorization !== "string" ||
|
|
157
|
+
typeof token !== "string" ||
|
|
158
|
+
typeof userinfo !== "string") {
|
|
159
|
+
throw new Error("Well-known document missing required endpoints (authorization_endpoint, token_endpoint, userinfo_endpoint)");
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
authorization,
|
|
163
|
+
token,
|
|
164
|
+
userinfo,
|
|
165
|
+
revocation: typeof doc.revocation_endpoint === "string" ? doc.revocation_endpoint : undefined,
|
|
166
|
+
endSession: typeof doc.end_session_endpoint === "string" ? doc.end_session_endpoint : undefined,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @edcalderon/auth โ Authentik flow + provisioning kit.
|
|
3
|
+
*
|
|
4
|
+
* Barrel export that assembles all Authentik-specific modules into a
|
|
5
|
+
* single importable surface area.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { processCallback, orchestrateLogout, ... } from "@edcalderon/auth/authentik";
|
|
9
|
+
*/
|
|
10
|
+
export type { AuthentikProvider, AuthentikEndpoints, AuthentikRelayConfig, RelayIncomingParams, RelayHandlerResult, AuthentikCallbackConfig, AuthentikTokenResponse, AuthentikClaims, AuthentikCallbackResult, AuthentikLogoutConfig, AuthentikLogoutResult, ProvisioningPayload, ProvisioningResult, ProvisioningAdapter, SupabaseSyncConfig, ConfigValidationResult, ConfigCheck, SafeRedirectConfig, } from "./types";
|
|
11
|
+
export { createRelayPageHtml, parseRelayParams, readRelayStorage, clearRelayStorage, } from "./relay";
|
|
12
|
+
export { exchangeCode, fetchClaims, processCallback, } from "./callback";
|
|
13
|
+
export type { ProcessCallbackOptions, ProcessCallbackResult, } from "./callback";
|
|
14
|
+
export { revokeToken, buildEndSessionUrl, orchestrateLogout, } from "./logout";
|
|
15
|
+
export { NoopProvisioningAdapter, createProvisioningAdapter, normalizePayload, SupabaseSyncAdapter, createSupabaseSyncAdapter, } from "./provisioning";
|
|
16
|
+
export { validateAuthentikConfig, validateSupabaseSyncConfig, validateFullConfig, discoverEndpoints, } from "./config";
|
|
17
|
+
export { resolveSafeRedirect } from "./redirect";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @edcalderon/auth โ Authentik flow + provisioning kit.
|
|
3
|
+
*
|
|
4
|
+
* Barrel export that assembles all Authentik-specific modules into a
|
|
5
|
+
* single importable surface area.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { processCallback, orchestrateLogout, ... } from "@edcalderon/auth/authentik";
|
|
9
|
+
*/
|
|
10
|
+
// Relay
|
|
11
|
+
export { createRelayPageHtml, parseRelayParams, readRelayStorage, clearRelayStorage, } from "./relay";
|
|
12
|
+
// Callback
|
|
13
|
+
export { exchangeCode, fetchClaims, processCallback, } from "./callback";
|
|
14
|
+
// Logout
|
|
15
|
+
export { revokeToken, buildEndSessionUrl, orchestrateLogout, } from "./logout";
|
|
16
|
+
// Provisioning
|
|
17
|
+
export { NoopProvisioningAdapter, createProvisioningAdapter, normalizePayload, SupabaseSyncAdapter, createSupabaseSyncAdapter, } from "./provisioning";
|
|
18
|
+
// Config validation
|
|
19
|
+
export { validateAuthentikConfig, validateSupabaseSyncConfig, validateFullConfig, discoverEndpoints, } from "./config";
|
|
20
|
+
// Safe redirect
|
|
21
|
+
export { resolveSafeRedirect } from "./redirect";
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentik logout orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* A correct Authentik logout requires all of these steps in sequence:
|
|
5
|
+
* 1. Clear app-local session state
|
|
6
|
+
* 2. Revoke the access token (best-effort)
|
|
7
|
+
* 3. Build the RP-initiated logout URL with id_token_hint
|
|
8
|
+
* 4. Navigate the browser to the end-session URL
|
|
9
|
+
*
|
|
10
|
+
* Reference: CIG apps/landing/components/AuthProvider.tsx signOut()
|
|
11
|
+
*/
|
|
12
|
+
import type { AuthentikLogoutConfig, AuthentikLogoutResult } from "./types";
|
|
13
|
+
/**
|
|
14
|
+
* Revoke an OAuth2 token at the Authentik revocation endpoint.
|
|
15
|
+
*
|
|
16
|
+
* This is a **best-effort** operation โ revocation failures do not
|
|
17
|
+
* prevent logout from completing. Returns `true` if the server
|
|
18
|
+
* responded with 2xx.
|
|
19
|
+
*
|
|
20
|
+
* If `config.revocationEndpoint` is not set, revocation is skipped
|
|
21
|
+
* and the function returns `false`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function revokeToken(config: AuthentikLogoutConfig, accessToken: string): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Build the Authentik RP-initiated logout URL.
|
|
26
|
+
*
|
|
27
|
+
* The resulting URL, when navigated to, will:
|
|
28
|
+
* 1. Clear the Authentik browser session
|
|
29
|
+
* 2. Redirect back to `postLogoutRedirectUri`
|
|
30
|
+
*
|
|
31
|
+
* Requires that the Authentik provider has an **invalidation flow**
|
|
32
|
+
* configured with a "User Logout" stage and a redirect stage.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildEndSessionUrl(config: AuthentikLogoutConfig, idToken?: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Execute the full Authentik logout sequence:
|
|
37
|
+
*
|
|
38
|
+
* 1. Revoke the access token (best-effort)
|
|
39
|
+
* 2. Build the RP-initiated end-session URL
|
|
40
|
+
*
|
|
41
|
+
* The caller is responsible for:
|
|
42
|
+
* - Clearing app-local session state **before** calling this function
|
|
43
|
+
* - Navigating the browser to `result.endSessionUrl` **after** this returns
|
|
44
|
+
*
|
|
45
|
+
* This design keeps the orchestrator framework-agnostic.
|
|
46
|
+
*/
|
|
47
|
+
export declare function orchestrateLogout(config: AuthentikLogoutConfig, tokens: {
|
|
48
|
+
accessToken?: string;
|
|
49
|
+
idToken?: string;
|
|
50
|
+
}): Promise<AuthentikLogoutResult>;
|