@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 +23 -0
- package/README.md +73 -7
- package/dist/AuthentikOidcClient.d.ts +58 -0
- package/dist/AuthentikOidcClient.js +284 -0
- 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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +19 -1
- package/supabase/migrations/003_authentik_shadow_auth_users.sql +81 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-origin PKCE relay handler for Authentik social login.
|
|
3
|
+
*
|
|
4
|
+
* When the login UI lives on a different origin than the callback handler,
|
|
5
|
+
* sessionStorage is origin-scoped. The relay stores the PKCE verifier and
|
|
6
|
+
* state on the callback origin before navigating to Authentik.
|
|
7
|
+
*
|
|
8
|
+
* Reference: CIG apps/dashboard/app/auth/login/[provider]/route.ts
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_SCOPE = "openid profile email";
|
|
11
|
+
const DEFAULT_STORAGE_KEY_PREFIX = "authentik_relay";
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the Authentik flow URL for a provider.
|
|
14
|
+
*
|
|
15
|
+
* If `providerFlowSlugs` maps the provider to a custom flow, the URL is:
|
|
16
|
+
* `{issuerOrigin}/if/flow/{flowSlug}/?next={authorizeUrl}`
|
|
17
|
+
*
|
|
18
|
+
* Otherwise falls back to the Authentik source-based social login:
|
|
19
|
+
* `{issuerOrigin}/source/oauth/login/{provider}/?next={authorizeUrl}`
|
|
20
|
+
*/
|
|
21
|
+
function buildFlowUrl(config, provider, authorizeUrl) {
|
|
22
|
+
const issuerOrigin = new URL(config.issuer).origin;
|
|
23
|
+
const flowSlug = config.providerFlowSlugs?.[provider];
|
|
24
|
+
if (flowSlug) {
|
|
25
|
+
const url = new URL(`/if/flow/${encodeURIComponent(flowSlug)}/`, issuerOrigin);
|
|
26
|
+
url.searchParams.set("next", authorizeUrl);
|
|
27
|
+
return url.toString();
|
|
28
|
+
}
|
|
29
|
+
const url = new URL(`/source/oauth/login/${encodeURIComponent(provider)}/`, issuerOrigin);
|
|
30
|
+
url.searchParams.set("next", authorizeUrl);
|
|
31
|
+
return url.toString();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the OIDC authorize URL that Authentik will redirect to after the
|
|
35
|
+
* social-provider flow completes.
|
|
36
|
+
*/
|
|
37
|
+
function buildAuthorizeUrl(config, params) {
|
|
38
|
+
const url = new URL(config.authorizePath);
|
|
39
|
+
url.searchParams.set("response_type", "code");
|
|
40
|
+
url.searchParams.set("client_id", config.clientId);
|
|
41
|
+
url.searchParams.set("redirect_uri", config.redirectUri);
|
|
42
|
+
url.searchParams.set("scope", config.scope || DEFAULT_SCOPE);
|
|
43
|
+
url.searchParams.set("state", params.state);
|
|
44
|
+
url.searchParams.set("code_challenge", params.codeChallenge);
|
|
45
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
46
|
+
return url.toString();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Generate the minimal HTML page that the relay route should serve.
|
|
50
|
+
*
|
|
51
|
+
* This page:
|
|
52
|
+
* 1. Stores PKCE params in the callback origin's sessionStorage
|
|
53
|
+
* 2. Redirects the browser to the Authentik social login flow
|
|
54
|
+
*
|
|
55
|
+
* The HTML is self-contained and does **not** load any external scripts.
|
|
56
|
+
*/
|
|
57
|
+
export function createRelayPageHtml(config, params) {
|
|
58
|
+
const authorizeUrl = buildAuthorizeUrl(config, params);
|
|
59
|
+
const flowUrl = buildFlowUrl(config, params.provider, authorizeUrl);
|
|
60
|
+
const prefix = config.storageKeyPrefix || DEFAULT_STORAGE_KEY_PREFIX;
|
|
61
|
+
// Escape for safe embedding in a <script> block
|
|
62
|
+
const safeJson = (v) => JSON.stringify(v).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
|
|
63
|
+
const html = [
|
|
64
|
+
"<!DOCTYPE html>",
|
|
65
|
+
"<html><head><meta charset=\"utf-8\"><title>Redirecting…</title></head>",
|
|
66
|
+
"<body>",
|
|
67
|
+
"<p>Redirecting to login…</p>",
|
|
68
|
+
"<script>",
|
|
69
|
+
"(function(){",
|
|
70
|
+
` try{`,
|
|
71
|
+
` var s=window.sessionStorage;`,
|
|
72
|
+
` s.setItem(${safeJson(`${prefix}:verifier`)},${safeJson(params.codeVerifier)});`,
|
|
73
|
+
` s.setItem(${safeJson(`${prefix}:state`)},${safeJson(params.state)});`,
|
|
74
|
+
` s.setItem(${safeJson(`${prefix}:provider`)},${safeJson(params.provider)});`,
|
|
75
|
+
params.next
|
|
76
|
+
? ` s.setItem(${safeJson(`${prefix}:next`)},${safeJson(params.next)});`
|
|
77
|
+
: "",
|
|
78
|
+
` }catch(e){console.error("relay storage error",e);}`,
|
|
79
|
+
` window.location.replace(${safeJson(flowUrl)});`,
|
|
80
|
+
"})();",
|
|
81
|
+
"</script>",
|
|
82
|
+
"</body></html>",
|
|
83
|
+
]
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join("\n");
|
|
86
|
+
return { html };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Parse the query parameters that the login origin sends to the relay.
|
|
90
|
+
*
|
|
91
|
+
* Expected query params:
|
|
92
|
+
* - `code_verifier` — PKCE verifier generated on the login origin
|
|
93
|
+
* - `code_challenge` — PKCE challenge (SHA-256 of verifier, base64url)
|
|
94
|
+
* - `state` — CSRF state token
|
|
95
|
+
* - `next` — (optional) post-login redirect target
|
|
96
|
+
*
|
|
97
|
+
* Returns `null` if required params are missing.
|
|
98
|
+
*/
|
|
99
|
+
export function parseRelayParams(searchParams) {
|
|
100
|
+
const get = (key) => {
|
|
101
|
+
if (searchParams instanceof URLSearchParams) {
|
|
102
|
+
return searchParams.get(key) ?? undefined;
|
|
103
|
+
}
|
|
104
|
+
return searchParams[key];
|
|
105
|
+
};
|
|
106
|
+
const codeVerifier = get("code_verifier");
|
|
107
|
+
const codeChallenge = get("code_challenge");
|
|
108
|
+
const state = get("state");
|
|
109
|
+
const provider = get("provider");
|
|
110
|
+
if (!codeVerifier || !codeChallenge || !state || !provider) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
provider,
|
|
115
|
+
codeVerifier,
|
|
116
|
+
codeChallenge,
|
|
117
|
+
state,
|
|
118
|
+
next: get("next"),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Read PKCE params back from sessionStorage on the callback origin.
|
|
123
|
+
*
|
|
124
|
+
* This is called by the callback handler after Authentik redirects back
|
|
125
|
+
* with `?code=&state=`.
|
|
126
|
+
*/
|
|
127
|
+
export function readRelayStorage(storage, prefix = DEFAULT_STORAGE_KEY_PREFIX) {
|
|
128
|
+
const codeVerifier = storage.getItem(`${prefix}:verifier`);
|
|
129
|
+
const state = storage.getItem(`${prefix}:state`);
|
|
130
|
+
const provider = storage.getItem(`${prefix}:provider`);
|
|
131
|
+
if (!codeVerifier || !state || !provider) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const next = storage.getItem(`${prefix}:next`) ?? undefined;
|
|
135
|
+
return { codeVerifier, state, provider, next };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Clean up relay storage after a successful callback exchange.
|
|
139
|
+
*/
|
|
140
|
+
export function clearRelayStorage(storage, prefix = DEFAULT_STORAGE_KEY_PREFIX) {
|
|
141
|
+
storage.removeItem(`${prefix}:verifier`);
|
|
142
|
+
storage.removeItem(`${prefix}:state`);
|
|
143
|
+
storage.removeItem(`${prefix}:provider`);
|
|
144
|
+
storage.removeItem(`${prefix}:next`);
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=relay.js.map
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @edcalderon/auth — Authentik flow + provisioning kit types.
|
|
3
|
+
*
|
|
4
|
+
* Generalised from the production CIG implementation:
|
|
5
|
+
* https://github.com/edwardcalderon/ComputeIntelligenceGraph/tree/main/packages/auth
|
|
6
|
+
*/
|
|
7
|
+
/** Well-known social-login provider slugs, plus any custom string. */
|
|
8
|
+
export type AuthentikProvider = "google" | "github" | "discord" | (string & {});
|
|
9
|
+
/**
|
|
10
|
+
* Resolved OIDC endpoint URLs for an Authentik provider.
|
|
11
|
+
*
|
|
12
|
+
* These can be obtained from `.well-known/openid-configuration`
|
|
13
|
+
* via `discoverEndpoints()`, or supplied manually.
|
|
14
|
+
*
|
|
15
|
+
* **Important:** Authentik places most OIDC endpoints at the
|
|
16
|
+
* `/application/o/` level, *not* under the per-app issuer path.
|
|
17
|
+
* E.g. with issuer `https://auth.example.com/application/o/my-app/`,
|
|
18
|
+
* the token endpoint is `https://auth.example.com/application/o/token/`.
|
|
19
|
+
* Always use explicit URLs or discovery — do not guess from the issuer.
|
|
20
|
+
*/
|
|
21
|
+
export interface AuthentikEndpoints {
|
|
22
|
+
/** Authorization endpoint (full URL). */
|
|
23
|
+
authorization: string;
|
|
24
|
+
/** Token endpoint (full URL). */
|
|
25
|
+
token: string;
|
|
26
|
+
/** Userinfo endpoint (full URL). */
|
|
27
|
+
userinfo: string;
|
|
28
|
+
/** Token revocation endpoint (full URL). */
|
|
29
|
+
revocation?: string;
|
|
30
|
+
/** RP-initiated logout / end-session endpoint (full URL). */
|
|
31
|
+
endSession?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Configuration for the cross-origin relay handler. */
|
|
34
|
+
export interface AuthentikRelayConfig {
|
|
35
|
+
/** Authentik issuer base URL (e.g. "https://auth.example.com"). */
|
|
36
|
+
issuer: string;
|
|
37
|
+
/** OIDC Application client_id registered in Authentik. */
|
|
38
|
+
clientId: string;
|
|
39
|
+
/** The callback URL that will receive the authorization code. */
|
|
40
|
+
redirectUri: string;
|
|
41
|
+
/** OIDC scopes (default: "openid profile email"). */
|
|
42
|
+
scope?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Explicit OIDC authorize endpoint URL.
|
|
45
|
+
* Required — use `discoverEndpoints()` or supply manually.
|
|
46
|
+
*/
|
|
47
|
+
authorizePath: string;
|
|
48
|
+
/**
|
|
49
|
+
* Map of provider slug → Authentik flow slug.
|
|
50
|
+
* E.g. `{ google: "my-app-google-login", github: "my-app-github-login" }`.
|
|
51
|
+
* When a provider is not in the map the provider string is used as-is.
|
|
52
|
+
*/
|
|
53
|
+
providerFlowSlugs?: Record<string, string>;
|
|
54
|
+
/**
|
|
55
|
+
* Storage key prefix used for PKCE state in sessionStorage.
|
|
56
|
+
* Default: "authentik_relay".
|
|
57
|
+
*/
|
|
58
|
+
storageKeyPrefix?: string;
|
|
59
|
+
}
|
|
60
|
+
/** Parsed query params forwarded from the login origin to the relay. */
|
|
61
|
+
export interface RelayIncomingParams {
|
|
62
|
+
provider: string;
|
|
63
|
+
codeVerifier: string;
|
|
64
|
+
codeChallenge: string;
|
|
65
|
+
state: string;
|
|
66
|
+
/** Optional post-login redirect target within the app. */
|
|
67
|
+
next?: string;
|
|
68
|
+
}
|
|
69
|
+
/** Result of the relay handler — an HTML snippet or a redirect URL. */
|
|
70
|
+
export interface RelayHandlerResult {
|
|
71
|
+
/** Minimal HTML page that stores PKCE params and redirects to Authentik. */
|
|
72
|
+
html: string;
|
|
73
|
+
}
|
|
74
|
+
/** Configuration for the enhanced callback handler. */
|
|
75
|
+
export interface AuthentikCallbackConfig {
|
|
76
|
+
/** Authentik issuer base URL. */
|
|
77
|
+
issuer: string;
|
|
78
|
+
/** OIDC Application client_id. */
|
|
79
|
+
clientId: string;
|
|
80
|
+
/** The callback URL that received the authorization code. */
|
|
81
|
+
redirectUri: string;
|
|
82
|
+
/** OIDC scopes (default: "openid profile email"). */
|
|
83
|
+
scope?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Explicit token endpoint URL.
|
|
86
|
+
* Required — use `discoverEndpoints()` or supply manually.
|
|
87
|
+
*/
|
|
88
|
+
tokenEndpoint: string;
|
|
89
|
+
/**
|
|
90
|
+
* Explicit userinfo endpoint URL.
|
|
91
|
+
* Required — use `discoverEndpoints()` or supply manually.
|
|
92
|
+
*/
|
|
93
|
+
userinfoEndpoint: string;
|
|
94
|
+
/** Fetch implementation override (for testing or server runtimes). */
|
|
95
|
+
fetchFn?: typeof fetch;
|
|
96
|
+
}
|
|
97
|
+
/** The raw OIDC token response from Authentik. */
|
|
98
|
+
export interface AuthentikTokenResponse {
|
|
99
|
+
access_token: string;
|
|
100
|
+
token_type?: string;
|
|
101
|
+
refresh_token?: string;
|
|
102
|
+
id_token?: string;
|
|
103
|
+
expires_in?: number;
|
|
104
|
+
scope?: string;
|
|
105
|
+
}
|
|
106
|
+
/** Parsed OIDC claims from the userinfo endpoint. */
|
|
107
|
+
export interface AuthentikClaims {
|
|
108
|
+
sub: string;
|
|
109
|
+
iss: string;
|
|
110
|
+
email?: string;
|
|
111
|
+
email_verified?: boolean;
|
|
112
|
+
name?: string;
|
|
113
|
+
preferred_username?: string;
|
|
114
|
+
picture?: string;
|
|
115
|
+
[key: string]: unknown;
|
|
116
|
+
}
|
|
117
|
+
/** Tokens + claims obtained after a successful callback exchange. */
|
|
118
|
+
export interface AuthentikCallbackResult {
|
|
119
|
+
tokens: AuthentikTokenResponse;
|
|
120
|
+
claims: AuthentikClaims;
|
|
121
|
+
provider: string;
|
|
122
|
+
}
|
|
123
|
+
/** Configuration for the logout orchestrator. */
|
|
124
|
+
export interface AuthentikLogoutConfig {
|
|
125
|
+
/** Authentik issuer base URL. */
|
|
126
|
+
issuer: string;
|
|
127
|
+
/** URL the browser should land on after Authentik clears its session. */
|
|
128
|
+
postLogoutRedirectUri: string;
|
|
129
|
+
/**
|
|
130
|
+
* Explicit RP-initiated end-session endpoint URL.
|
|
131
|
+
* Required — use `discoverEndpoints()` or supply manually.
|
|
132
|
+
*/
|
|
133
|
+
endSessionEndpoint: string;
|
|
134
|
+
/**
|
|
135
|
+
* Explicit token revocation endpoint URL.
|
|
136
|
+
* Optional — when omitted, revocation is skipped.
|
|
137
|
+
*/
|
|
138
|
+
revocationEndpoint?: string;
|
|
139
|
+
/** OIDC Application client_id (used in token revocation). */
|
|
140
|
+
clientId?: string;
|
|
141
|
+
/** Fetch implementation override. */
|
|
142
|
+
fetchFn?: typeof fetch;
|
|
143
|
+
}
|
|
144
|
+
/** Result of a logout orchestration. */
|
|
145
|
+
export interface AuthentikLogoutResult {
|
|
146
|
+
/** The full RP-initiated logout URL the browser should navigate to. */
|
|
147
|
+
endSessionUrl: string;
|
|
148
|
+
/** Whether the access-token revocation succeeded. */
|
|
149
|
+
tokenRevoked: boolean;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Payload sent to the provisioning adapter after a successful callback.
|
|
153
|
+
* Modelled after CIG's OidcSyncPayload.
|
|
154
|
+
*/
|
|
155
|
+
export interface ProvisioningPayload {
|
|
156
|
+
/** OIDC subject identifier. */
|
|
157
|
+
sub: string;
|
|
158
|
+
/** OIDC issuer URL. */
|
|
159
|
+
iss: string;
|
|
160
|
+
/** User email (normalised to lowercase). */
|
|
161
|
+
email: string;
|
|
162
|
+
/** Whether the email is verified by the OIDC provider. */
|
|
163
|
+
emailVerified?: boolean;
|
|
164
|
+
/** Display name. */
|
|
165
|
+
name?: string;
|
|
166
|
+
/** Avatar / profile picture URL. */
|
|
167
|
+
picture?: string;
|
|
168
|
+
/** Upstream social provider slug (e.g. "google", "github", "authentik"). */
|
|
169
|
+
provider?: string;
|
|
170
|
+
/** The full set of OIDC claims as received from Authentik. */
|
|
171
|
+
rawClaims?: Record<string, unknown>;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Result returned by a provisioning adapter.
|
|
175
|
+
* The `synced` flag **must** be `true` for the callback to proceed with redirect.
|
|
176
|
+
*/
|
|
177
|
+
export interface ProvisioningResult {
|
|
178
|
+
/** Whether the sync completed successfully. */
|
|
179
|
+
synced: boolean;
|
|
180
|
+
/** The app-level user ID (from public.users or equivalent). */
|
|
181
|
+
appUserId?: string;
|
|
182
|
+
/** Supabase auth.users ID when shadow-user mode is used. */
|
|
183
|
+
authUserId?: string;
|
|
184
|
+
/** Whether a new shadow auth.users row was created (for rollback tracking). */
|
|
185
|
+
authUserCreated?: boolean;
|
|
186
|
+
/** Whether an existing shadow auth.users row was updated. */
|
|
187
|
+
authUserUpdated?: boolean;
|
|
188
|
+
/** Human-readable diagnostic on failure. */
|
|
189
|
+
error?: string;
|
|
190
|
+
/** Machine-readable error code. */
|
|
191
|
+
errorCode?: string;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* A provisioning adapter is a server-side function that ensures the
|
|
195
|
+
* Authentik-authenticated user exists in the application's local user store.
|
|
196
|
+
*
|
|
197
|
+
* Adapters must be **idempotent** and **fail-closed**: if provisioning fails
|
|
198
|
+
* the callback must not redirect the user into the protected app.
|
|
199
|
+
*/
|
|
200
|
+
export interface ProvisioningAdapter {
|
|
201
|
+
/**
|
|
202
|
+
* Sync the Authentik identity into the local user store.
|
|
203
|
+
* Must return `{ synced: true }` for the callback flow to succeed.
|
|
204
|
+
*/
|
|
205
|
+
sync(payload: ProvisioningPayload): Promise<ProvisioningResult>;
|
|
206
|
+
}
|
|
207
|
+
/** Config for the Authentik ↔ Supabase integrated sync adapter. */
|
|
208
|
+
export interface SupabaseSyncConfig {
|
|
209
|
+
/** Supabase project URL. */
|
|
210
|
+
supabaseUrl: string;
|
|
211
|
+
/** Supabase service_role key (server-side only). */
|
|
212
|
+
supabaseServiceRoleKey: string;
|
|
213
|
+
/**
|
|
214
|
+
* Name of the Supabase RPC function that upserts into public.users.
|
|
215
|
+
* Default: "upsert_oidc_user".
|
|
216
|
+
*/
|
|
217
|
+
upsertRpcName?: string;
|
|
218
|
+
/**
|
|
219
|
+
* Name of the Supabase RPC function that links the shadow auth.users
|
|
220
|
+
* ID to the public.users row.
|
|
221
|
+
* Default: "link_shadow_auth_user".
|
|
222
|
+
*/
|
|
223
|
+
linkShadowRpcName?: string;
|
|
224
|
+
/**
|
|
225
|
+
* Whether to create a shadow user in auth.users.
|
|
226
|
+
* Default: true (recommended for CIG-style setups).
|
|
227
|
+
*/
|
|
228
|
+
createShadowAuthUser?: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* Default OIDC issuer to use when payload.iss is not provided.
|
|
231
|
+
* Falls back to the Authentik issuer configured elsewhere.
|
|
232
|
+
*/
|
|
233
|
+
defaultIssuer?: string;
|
|
234
|
+
/**
|
|
235
|
+
* Whether to rollback newly created shadow auth.users rows
|
|
236
|
+
* when downstream public.users sync fails.
|
|
237
|
+
* Default: true.
|
|
238
|
+
*/
|
|
239
|
+
rollbackOnFailure?: boolean;
|
|
240
|
+
}
|
|
241
|
+
/** Diagnostic result from the config-validation doctor helper. */
|
|
242
|
+
export interface ConfigValidationResult {
|
|
243
|
+
/** Whether the configuration is valid. */
|
|
244
|
+
valid: boolean;
|
|
245
|
+
/** Individual check results. */
|
|
246
|
+
checks: ConfigCheck[];
|
|
247
|
+
}
|
|
248
|
+
export interface ConfigCheck {
|
|
249
|
+
/** Short name of the check. */
|
|
250
|
+
name: string;
|
|
251
|
+
/** Whether this check passed. */
|
|
252
|
+
passed: boolean;
|
|
253
|
+
/** Human-readable message. */
|
|
254
|
+
message: string;
|
|
255
|
+
/** Severity when check fails. */
|
|
256
|
+
severity: "error" | "warning";
|
|
257
|
+
}
|
|
258
|
+
/** Configuration for the safe redirect resolver. */
|
|
259
|
+
export interface SafeRedirectConfig {
|
|
260
|
+
/** Allowed origin(s) for redirects. */
|
|
261
|
+
allowedOrigins: string[];
|
|
262
|
+
/** Fallback URL when the requested redirect is not safe. */
|
|
263
|
+
fallbackUrl: string;
|
|
264
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @edcalderon/auth — Authentik flow + provisioning kit types.
|
|
3
|
+
*
|
|
4
|
+
* Generalised from the production CIG implementation:
|
|
5
|
+
* https://github.com/edwardcalderon/ComputeIntelligenceGraph/tree/main/packages/auth
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=types.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export * from "./providers/FirebaseWebClient";
|
|
|
5
5
|
export { FirebaseWebClient as FirebaseClient } from "./providers/FirebaseWebClient";
|
|
6
6
|
export * from "./providers/HybridWebClient";
|
|
7
7
|
export { HybridWebClient as HybridClient } from "./providers/HybridWebClient";
|
|
8
|
+
export * from "./AuthentikOidcClient";
|
package/dist/index.js
CHANGED
|
@@ -7,4 +7,5 @@ export * from "./providers/FirebaseWebClient";
|
|
|
7
7
|
export { FirebaseWebClient as FirebaseClient } from "./providers/FirebaseWebClient";
|
|
8
8
|
export * from "./providers/HybridWebClient";
|
|
9
9
|
export { HybridWebClient as HybridClient } from "./providers/HybridWebClient";
|
|
10
|
+
export * from "./AuthentikOidcClient";
|
|
10
11
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edcalderon/auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A universal, provider-agnostic authentication package (Web + Next.js + Expo/React Native)",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
"require": "./dist/index.js",
|
|
10
10
|
"react-native": "./dist/index.js"
|
|
11
11
|
},
|
|
12
|
+
"./authentik": {
|
|
13
|
+
"types": "./dist/authentik/index.d.ts",
|
|
14
|
+
"import": "./dist/authentik/index.js",
|
|
15
|
+
"require": "./dist/authentik/index.js"
|
|
16
|
+
},
|
|
12
17
|
"./supabase": {
|
|
13
18
|
"types": "./dist/providers/SupabaseClient.d.ts",
|
|
14
19
|
"import": "./dist/providers/SupabaseClient.js",
|
|
@@ -42,6 +47,9 @@
|
|
|
42
47
|
"scripts": {
|
|
43
48
|
"build": "tsc",
|
|
44
49
|
"dev": "tsc --watch",
|
|
50
|
+
"test": "jest",
|
|
51
|
+
"test:watch": "jest --watch",
|
|
52
|
+
"test:coverage": "jest --coverage",
|
|
45
53
|
"prepublishOnly": "npm run build",
|
|
46
54
|
"update-readme": "versioning update-readme"
|
|
47
55
|
},
|
|
@@ -90,10 +98,20 @@
|
|
|
90
98
|
}
|
|
91
99
|
},
|
|
92
100
|
"devDependencies": {
|
|
101
|
+
"@types/jest": "^29.5.12",
|
|
93
102
|
"@types/react": "^19.2.7",
|
|
103
|
+
"jest": "^29.7.0",
|
|
104
|
+
"ts-jest": "^29.1.2",
|
|
94
105
|
"typescript": "^5.9.3"
|
|
95
106
|
},
|
|
96
107
|
"engines": {
|
|
97
108
|
"node": ">=18.0.0"
|
|
109
|
+
},
|
|
110
|
+
"jest": {
|
|
111
|
+
"testEnvironment": "node",
|
|
112
|
+
"testMatch": ["**/__tests__/**/*.test.ts"],
|
|
113
|
+
"transform": {
|
|
114
|
+
"^.+\\.ts$": "ts-jest"
|
|
115
|
+
}
|
|
98
116
|
}
|
|
99
117
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 003_authentik_shadow_auth_users.sql
|
|
3
|
+
-- Optional migration for projects using the Authentik ↔ Supabase integrated
|
|
4
|
+
-- sync mode in @edcalderon/auth.
|
|
5
|
+
--
|
|
6
|
+
-- This migration adds app_metadata columns and an index to support the
|
|
7
|
+
-- shadow auth.users pattern where Authentik-authenticated users are mirrored
|
|
8
|
+
-- into Supabase auth.users with identity-first matching.
|
|
9
|
+
--
|
|
10
|
+
-- The public.users table is already created by 001_create_app_users.sql.
|
|
11
|
+
-- This migration adds columns to support the shadow-user linkage and
|
|
12
|
+
-- rollback-safe provisioning.
|
|
13
|
+
--
|
|
14
|
+
-- Reference: CIG apps/dashboard/lib/authSync.ts
|
|
15
|
+
-- ============================================================================
|
|
16
|
+
|
|
17
|
+
-- Add optional column to track the linked Supabase auth.users shadow ID
|
|
18
|
+
alter table public.users
|
|
19
|
+
add column if not exists shadow_auth_user_id uuid;
|
|
20
|
+
|
|
21
|
+
-- Add optional column to record when the shadow link was established
|
|
22
|
+
alter table public.users
|
|
23
|
+
add column if not exists shadow_linked_at timestamptz;
|
|
24
|
+
|
|
25
|
+
-- Index for quick lookup by shadow auth user ID
|
|
26
|
+
create index if not exists users_shadow_auth_user_id_idx
|
|
27
|
+
on public.users (shadow_auth_user_id)
|
|
28
|
+
where shadow_auth_user_id is not null;
|
|
29
|
+
|
|
30
|
+
-- ============================================================================
|
|
31
|
+
-- Helper RPC: link a public.users row to a Supabase auth.users shadow record.
|
|
32
|
+
-- Called by the SupabaseSyncAdapter after creating/finding the shadow user.
|
|
33
|
+
--
|
|
34
|
+
-- This is an additive helper — the core upsert_oidc_user RPC from migration
|
|
35
|
+
-- 001 continues to work unchanged. The raw_claims payload can optionally
|
|
36
|
+
-- carry shadow_supabase_auth_user_id to record the linkage.
|
|
37
|
+
-- ============================================================================
|
|
38
|
+
|
|
39
|
+
create or replace function public.link_shadow_auth_user(
|
|
40
|
+
p_sub text,
|
|
41
|
+
p_iss text,
|
|
42
|
+
p_shadow_auth_user_id uuid
|
|
43
|
+
)
|
|
44
|
+
returns public.users
|
|
45
|
+
language plpgsql
|
|
46
|
+
security definer
|
|
47
|
+
set search_path = public
|
|
48
|
+
as $$
|
|
49
|
+
declare
|
|
50
|
+
v_claims jsonb := coalesce(
|
|
51
|
+
nullif(current_setting('request.jwt.claims', true), ''),
|
|
52
|
+
'{}'
|
|
53
|
+
)::jsonb;
|
|
54
|
+
v_role text := coalesce(nullif(v_claims ->> 'role', ''), 'unknown');
|
|
55
|
+
v_user public.users;
|
|
56
|
+
begin
|
|
57
|
+
if v_role <> 'service_role' then
|
|
58
|
+
raise exception 'link_shadow_auth_user() requires a trusted server-side caller';
|
|
59
|
+
end if;
|
|
60
|
+
|
|
61
|
+
update public.users
|
|
62
|
+
set shadow_auth_user_id = p_shadow_auth_user_id,
|
|
63
|
+
shadow_linked_at = timezone('utc', now()),
|
|
64
|
+
updated_at = timezone('utc', now())
|
|
65
|
+
where sub = p_sub
|
|
66
|
+
and iss = p_iss
|
|
67
|
+
returning * into v_user;
|
|
68
|
+
|
|
69
|
+
if not found then
|
|
70
|
+
raise exception 'No public.users row found for sub=% iss=%', p_sub, p_iss;
|
|
71
|
+
end if;
|
|
72
|
+
|
|
73
|
+
return v_user;
|
|
74
|
+
end;
|
|
75
|
+
$$;
|
|
76
|
+
|
|
77
|
+
revoke all on function public.link_shadow_auth_user(text, text, uuid)
|
|
78
|
+
from public, anon, authenticated;
|
|
79
|
+
|
|
80
|
+
grant execute on function public.link_shadow_auth_user(text, text, uuid)
|
|
81
|
+
to service_role;
|