@eminuckan/spine 0.3.1 → 0.3.2
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 +6 -0
- package/SECURITY.md +5 -15
- package/dist/access/index.mjs +2 -0
- package/dist/{access-CGrsDMwu.js → access-BHy1v5sr.mjs} +6 -8
- package/dist/access-BHy1v5sr.mjs.map +1 -0
- package/dist/api-client/index.mjs +3 -0
- package/dist/api-client/server.mjs +4 -0
- package/dist/{api-client-CgZUMukO.js → api-client-CB7XZaMg.mjs} +8 -10
- package/dist/api-client-CB7XZaMg.mjs.map +1 -0
- package/dist/auth/auth.server.d.ts +1 -0
- package/dist/auth/auth.server.d.ts.map +1 -1
- package/dist/auth/index.mjs +1 -0
- package/dist/auth/route-protection.server.d.ts.map +1 -1
- package/dist/auth/server.mjs +3 -0
- package/dist/{identity-C5un-xAs.js → identity/index.mjs} +24 -21
- package/dist/identity/index.mjs.map +1 -0
- package/dist/{server-Dxsww9v9.js → identity/server.mjs} +7 -8
- package/dist/identity/server.mjs.map +1 -0
- package/dist/{index.js → index.mjs} +11 -13
- package/dist/{logging-DviiAkLx.js → logging/index.mjs} +22 -24
- package/dist/logging/index.mjs.map +1 -0
- package/dist/{permission-service-CvGpwI0d.js → permission-service-DUyKomU3.mjs} +6 -10
- package/dist/permission-service-DUyKomU3.mjs.map +1 -0
- package/dist/permissions/index.mjs +3 -0
- package/dist/{permissions-_n1hIG7Q.js → permissions-C5dTP9y4.mjs} +23 -15
- package/dist/permissions-C5dTP9y4.mjs.map +1 -0
- package/dist/query-client/index.mjs +2 -0
- package/dist/{query-client-Bh3TyB8W.js → query-client-_xB300gc.mjs} +49 -15
- package/dist/query-client-_xB300gc.mjs.map +1 -0
- package/dist/react-router/{index.js → index.mjs} +11 -13
- package/dist/react-router/server.mjs +17 -0
- package/dist/{redis-session-storage.server-v2VcpFsQ.js → redis-session-storage.server-DGgQcJ9B.mjs} +30 -42
- package/dist/redis-session-storage.server-DGgQcJ9B.mjs.map +1 -0
- package/dist/{server-C96NMFBy.js → server-ByScGRXA.mjs} +39 -36
- package/dist/server-ByScGRXA.mjs.map +1 -0
- package/dist/{auth.server-D5oiz9aF.js → server-CqX2yCa4.mjs} +388 -119
- package/dist/server-CqX2yCa4.mjs.map +1 -0
- package/dist/server.mjs +90 -0
- package/dist/server.mjs.map +1 -0
- package/dist/signalr/index.mjs +2 -0
- package/dist/{signalr-k-70VHKo.js → signalr-C-TA8nEE.mjs} +11 -6
- package/dist/signalr-C-TA8nEE.mjs.map +1 -0
- package/dist/{tenant-DfLemb6n.js → tenant/index.mjs} +19 -12
- package/dist/tenant/index.mjs.map +1 -0
- package/dist/{server-Ca-3DJf2.js → tenant/server.mjs} +12 -12
- package/dist/tenant/server.mjs.map +1 -0
- package/dist/{types-BaaE6wjY.js → types-DqCXinAk.mjs} +3 -3
- package/dist/types-DqCXinAk.mjs.map +1 -0
- package/docs/codex-for-oss-application.md +4 -0
- package/package.json +14 -2
- package/dist/access/index.js +0 -4
- package/dist/access-CGrsDMwu.js.map +0 -1
- package/dist/api-client/index.js +0 -5
- package/dist/api-client/server.js +0 -6
- package/dist/api-client-CgZUMukO.js.map +0 -1
- package/dist/auth/index.js +0 -0
- package/dist/auth/server.js +0 -6
- package/dist/auth.server-D5oiz9aF.js.map +0 -1
- package/dist/auth.server-lC94lmlx.js +0 -5
- package/dist/identity/index.js +0 -3
- package/dist/identity/server.js +0 -5
- package/dist/identity-C5un-xAs.js.map +0 -1
- package/dist/logging/index.js +0 -3
- package/dist/logging-DviiAkLx.js.map +0 -1
- package/dist/permission-service-CvGpwI0d.js.map +0 -1
- package/dist/permissions/index.js +0 -4
- package/dist/permissions-_n1hIG7Q.js.map +0 -1
- package/dist/query-client/index.js +0 -3
- package/dist/query-client-Bh3TyB8W.js.map +0 -1
- package/dist/react-router/server.js +0 -20
- package/dist/redis-session-storage.server-v2VcpFsQ.js.map +0 -1
- package/dist/server-B6PCxc-T.js +0 -304
- package/dist/server-B6PCxc-T.js.map +0 -1
- package/dist/server-C96NMFBy.js.map +0 -1
- package/dist/server-Ca-3DJf2.js.map +0 -1
- package/dist/server-Dxsww9v9.js.map +0 -1
- package/dist/server-hGrElJMG.js +0 -78
- package/dist/server-hGrElJMG.js.map +0 -1
- package/dist/server.js +0 -20
- package/dist/signalr/index.js +0 -3
- package/dist/signalr-k-70VHKo.js.map +0 -1
- package/dist/src-B9rDUgSb.js +0 -0
- package/dist/tenant/index.js +0 -3
- package/dist/tenant/server.js +0 -5
- package/dist/tenant-DfLemb6n.js.map +0 -1
- package/dist/types-BaaE6wjY.js.map +0 -1
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { logger } from "./logging
|
|
2
|
-
import {
|
|
1
|
+
import { logger } from "./logging/index.mjs";
|
|
2
|
+
import { S as updateAuthSession, a as createOAuthState, d as destroyAuthSessionsBySid, f as destroyAuthSessionsForUser, g as getOAuthState, i as createAuthSession, l as destroyAuthSessionsByIdentitySession, o as deleteOAuthState, p as getAuthSession, s as destroyAuthSession, t as AUTH_ERROR_COOKIE_PREFIX, v as listAuthSessionDataForUser } from "./redis-session-storage.server-DGgQcJ9B.mjs";
|
|
3
3
|
import * as oidc from "openid-client";
|
|
4
4
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
5
|
-
|
|
6
5
|
//#region src/http/response.ts
|
|
7
6
|
/**
|
|
8
7
|
* Create a redirect response without relying on a router/framework helper.
|
|
@@ -15,10 +14,9 @@ function createRedirectResponse(location, init = {}) {
|
|
|
15
14
|
headers
|
|
16
15
|
});
|
|
17
16
|
}
|
|
18
|
-
|
|
19
17
|
//#endregion
|
|
20
18
|
//#region src/auth/authorization-parameters.ts
|
|
21
|
-
const RESERVED_AUTHORIZATION_PARAMETER_NAMES = new Set([
|
|
19
|
+
const RESERVED_AUTHORIZATION_PARAMETER_NAMES = /* @__PURE__ */ new Set([
|
|
22
20
|
"client_id",
|
|
23
21
|
"code_challenge",
|
|
24
22
|
"code_challenge_method",
|
|
@@ -31,7 +29,7 @@ const RESERVED_AUTHORIZATION_PARAMETER_NAMES = new Set([
|
|
|
31
29
|
"scope",
|
|
32
30
|
"state"
|
|
33
31
|
]);
|
|
34
|
-
const DEFAULT_EXTRA_AUTHORIZATION_PARAMETER_NAMES = new Set([
|
|
32
|
+
const DEFAULT_EXTRA_AUTHORIZATION_PARAMETER_NAMES = /* @__PURE__ */ new Set([
|
|
35
33
|
"acr_values",
|
|
36
34
|
"kc_idp_hint",
|
|
37
35
|
"login_hint",
|
|
@@ -43,7 +41,7 @@ function isAllowedExtraAuthorizationParameter(key, allowedNames, allowedPrefixes
|
|
|
43
41
|
}
|
|
44
42
|
function sanitizeExtraAuthorizationParameters(params, options = {}) {
|
|
45
43
|
if (!params) return {};
|
|
46
|
-
const allowedNames = new Set([...DEFAULT_EXTRA_AUTHORIZATION_PARAMETER_NAMES, ...(options.extraAuthParamNames ?? []).map((name) => name.trim().toLowerCase()).filter(Boolean)]);
|
|
44
|
+
const allowedNames = /* @__PURE__ */ new Set([...DEFAULT_EXTRA_AUTHORIZATION_PARAMETER_NAMES, ...(options.extraAuthParamNames ?? []).map((name) => name.trim().toLowerCase()).filter(Boolean)]);
|
|
47
45
|
const allowedPrefixes = (options.extraAuthParamPrefixes ?? []).map((prefix) => prefix.trim().toLowerCase()).filter(Boolean);
|
|
48
46
|
const sanitized = {};
|
|
49
47
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -72,10 +70,19 @@ function buildAuthorizationState(baseState, publicStateContext) {
|
|
|
72
70
|
if (!normalizedContext) return baseState;
|
|
73
71
|
return `${baseState}.${Buffer.from(JSON.stringify(normalizedContext), "utf8").toString("base64url")}`;
|
|
74
72
|
}
|
|
75
|
-
|
|
76
73
|
//#endregion
|
|
77
74
|
//#region src/auth/auth.server.ts
|
|
78
75
|
/**
|
|
76
|
+
* OpenID Connect Authentication Server Module
|
|
77
|
+
*
|
|
78
|
+
* Handles:
|
|
79
|
+
* - Login flow (PKCE)
|
|
80
|
+
* - Token exchange
|
|
81
|
+
* - Token refresh
|
|
82
|
+
* - Logout
|
|
83
|
+
* - Session management
|
|
84
|
+
*/
|
|
85
|
+
/**
|
|
79
86
|
* Get effective hasLandingPage setting based on config
|
|
80
87
|
*/
|
|
81
88
|
function getEffectiveHasLandingPage(config) {
|
|
@@ -119,8 +126,7 @@ function createAuthConfig() {
|
|
|
119
126
|
};
|
|
120
127
|
}
|
|
121
128
|
function parseOidcClientAuthMethod(value, fallback) {
|
|
122
|
-
|
|
123
|
-
switch (normalized) {
|
|
129
|
+
switch (value?.trim().toLowerCase()) {
|
|
124
130
|
case void 0:
|
|
125
131
|
case "": return fallback;
|
|
126
132
|
case "none": return "none";
|
|
@@ -137,8 +143,8 @@ function getAuthConfig() {
|
|
|
137
143
|
return OIDC_CONFIG;
|
|
138
144
|
}
|
|
139
145
|
let authServerCache = null;
|
|
140
|
-
const CACHE_TTL =
|
|
141
|
-
const jwksCache = new Map();
|
|
146
|
+
const CACHE_TTL = 3600 * 1e3;
|
|
147
|
+
const jwksCache = /* @__PURE__ */ new Map();
|
|
142
148
|
function normalizeIssuer(value) {
|
|
143
149
|
const normalized = value?.trim().replace(/\/+$/, "");
|
|
144
150
|
return normalized && normalized.length > 0 ? normalized : null;
|
|
@@ -188,10 +194,16 @@ function appendApplicationInitiatedActionResult(returnUrl, action, status) {
|
|
|
188
194
|
const params = new URLSearchParams();
|
|
189
195
|
if (action) params.set("kc_action", action);
|
|
190
196
|
if (status) params.set("kc_action_status", status);
|
|
191
|
-
|
|
192
|
-
return `${returnUrl}${separator}${params.toString()}`;
|
|
197
|
+
return `${returnUrl}${returnUrl.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
193
198
|
}
|
|
194
199
|
}
|
|
200
|
+
function createAuthorizationResponseUrl(requestUrl, redirectUri) {
|
|
201
|
+
const incomingUrl = new URL(requestUrl);
|
|
202
|
+
const authorizationResponseUrl = new URL(redirectUri);
|
|
203
|
+
authorizationResponseUrl.search = incomingUrl.search;
|
|
204
|
+
authorizationResponseUrl.hash = "";
|
|
205
|
+
return authorizationResponseUrl;
|
|
206
|
+
}
|
|
195
207
|
function isAuthLoopPath(path) {
|
|
196
208
|
const normalized = path.split("#")[0].split("?")[0].replace(/\/+$/, "") || "/";
|
|
197
209
|
return normalized === "/auth/logout" || normalized === "/auth/callback";
|
|
@@ -215,8 +227,7 @@ function shouldAllowInsecureOidcRequests(authority) {
|
|
|
215
227
|
return isLocalInsecureIssuer(authority);
|
|
216
228
|
}
|
|
217
229
|
function getDiscoveryOptions() {
|
|
218
|
-
|
|
219
|
-
return shouldAllowInsecureOidcRequests(config.authority) ? {
|
|
230
|
+
return shouldAllowInsecureOidcRequests(getAuthConfig().authority) ? {
|
|
220
231
|
algorithm: "oidc",
|
|
221
232
|
execute: [oidc.allowInsecureRequests]
|
|
222
233
|
} : { algorithm: "oidc" };
|
|
@@ -232,7 +243,6 @@ function getClientAuthentication(config) {
|
|
|
232
243
|
case "client_secret_post":
|
|
233
244
|
if (!config.clientSecret) throw new Error("OIDC_CLIENT_SECRET is required for client_secret_post authentication");
|
|
234
245
|
return oidc.ClientSecretPost(config.clientSecret);
|
|
235
|
-
case "none":
|
|
236
246
|
default: return oidc.None();
|
|
237
247
|
}
|
|
238
248
|
}
|
|
@@ -247,8 +257,7 @@ function shouldLogoutAfterOAuthError(error) {
|
|
|
247
257
|
return message.includes("invalid_grant") || message.includes("invalid_token") || message.includes("unauthorized");
|
|
248
258
|
}
|
|
249
259
|
function getLogoutScope(url) {
|
|
250
|
-
const
|
|
251
|
-
const normalized = requestedScope?.trim().toLowerCase();
|
|
260
|
+
const normalized = (url.searchParams.get("logout") ?? url.searchParams.get("logout_scope") ?? url.searchParams.get("scope"))?.trim().toLowerCase();
|
|
252
261
|
if (normalized === "local" || normalized === "application" || normalized === "app" || normalized === "current-device") return "local";
|
|
253
262
|
if (normalized === "all" || normalized === "all-local" || normalized === "all-devices" || normalized === "everywhere") return "all";
|
|
254
263
|
if (url.searchParams.get("local_only") === "true" || url.searchParams.get("client_id_only") === "true") return "local";
|
|
@@ -332,23 +341,19 @@ function getJwks(jwksUri) {
|
|
|
332
341
|
return jwks;
|
|
333
342
|
}
|
|
334
343
|
async function getLogoutTokenFromRequest(request) {
|
|
335
|
-
|
|
336
|
-
if (contentType.includes("application/json")) {
|
|
344
|
+
if ((request.headers.get("content-type") ?? "").includes("application/json")) {
|
|
337
345
|
const body = await request.json().catch(() => null);
|
|
338
346
|
return typeof body?.logout_token === "string" ? body.logout_token : null;
|
|
339
347
|
}
|
|
340
|
-
const
|
|
341
|
-
const logoutToken = formData?.get("logout_token");
|
|
348
|
+
const logoutToken = (await request.formData().catch(() => null))?.get("logout_token");
|
|
342
349
|
return typeof logoutToken === "string" ? logoutToken : null;
|
|
343
350
|
}
|
|
344
351
|
function validateBackChannelLogoutClaims(payload, clientId) {
|
|
345
352
|
const events = payload.events;
|
|
346
|
-
|
|
347
|
-
if (!hasBackChannelEvent) throw new Error("Invalid back-channel logout token: missing back-channel logout event");
|
|
353
|
+
if (!(events && typeof events === "object" && !Array.isArray(events) && "http://schemas.openid.net/event/backchannel-logout" in events)) throw new Error("Invalid back-channel logout token: missing back-channel logout event");
|
|
348
354
|
if ("nonce" in payload) throw new Error("Invalid back-channel logout token: nonce is not allowed");
|
|
349
355
|
const audience = payload.aud;
|
|
350
|
-
|
|
351
|
-
if (!hasClientAudience) throw new Error("Invalid back-channel logout token: audience mismatch");
|
|
356
|
+
if (!(Array.isArray(audience) ? audience.includes(clientId) : audience === clientId)) throw new Error("Invalid back-channel logout token: audience mismatch");
|
|
352
357
|
const subject = typeof payload.sub === "string" ? payload.sub : void 0;
|
|
353
358
|
const sid = typeof payload.sid === "string" ? payload.sid : void 0;
|
|
354
359
|
if (!subject && !sid) throw new Error("Invalid back-channel logout token: subject or sid is required");
|
|
@@ -430,7 +435,6 @@ function getStringClaim(claims, claimKeys) {
|
|
|
430
435
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
431
436
|
}
|
|
432
437
|
if (typeof value === "number" || typeof value === "boolean") return `${value}`;
|
|
433
|
-
return void 0;
|
|
434
438
|
}
|
|
435
439
|
function getNumberClaim(claims, claimKeys) {
|
|
436
440
|
const { value } = getFirstClaimValue(claims, claimKeys);
|
|
@@ -439,7 +443,6 @@ function getNumberClaim(claims, claimKeys) {
|
|
|
439
443
|
const parsed = Number(value);
|
|
440
444
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
441
445
|
}
|
|
442
|
-
return void 0;
|
|
443
446
|
}
|
|
444
447
|
function getBooleanClaim(claims, claimKeys, defaultValue = false) {
|
|
445
448
|
const { value } = getFirstClaimValue(claims, claimKeys);
|
|
@@ -577,10 +580,9 @@ function getAuthError(request) {
|
|
|
577
580
|
if (!error) return null;
|
|
578
581
|
const descRegex = new RegExp(`${AUTH_ERROR_DESC_COOKIE}=([^;]+)`);
|
|
579
582
|
const descMatch = cookies.match(descRegex);
|
|
580
|
-
const description = descMatch ? decodeURIComponent(descMatch[1]) : void 0;
|
|
581
583
|
return {
|
|
582
584
|
error,
|
|
583
|
-
description
|
|
585
|
+
description: descMatch ? decodeURIComponent(descMatch[1]) : void 0
|
|
584
586
|
};
|
|
585
587
|
}
|
|
586
588
|
/**
|
|
@@ -598,8 +600,7 @@ function clearAuthErrorHeaders() {
|
|
|
598
600
|
* Check if request has auth error (quick check without parsing)
|
|
599
601
|
*/
|
|
600
602
|
function hasAuthError(request) {
|
|
601
|
-
|
|
602
|
-
return cookies?.includes(`${AUTH_ERROR_COOKIE}=`) ?? false;
|
|
603
|
+
return request.headers.get("Cookie")?.includes(`${AUTH_ERROR_COOKIE}=`) ?? false;
|
|
603
604
|
}
|
|
604
605
|
/**
|
|
605
606
|
* Initiate OAuth login flow
|
|
@@ -611,15 +612,12 @@ async function login(request, returnUrlOrOptions) {
|
|
|
611
612
|
try {
|
|
612
613
|
const config = getAuthConfig();
|
|
613
614
|
const options = typeof returnUrlOrOptions === "string" ? { returnUrl: returnUrlOrOptions } : returnUrlOrOptions || {};
|
|
614
|
-
|
|
615
|
-
const hasError = cookies?.includes(`${AUTH_ERROR_COOKIE}=`);
|
|
616
|
-
if (hasError) {
|
|
615
|
+
if (request.headers.get("Cookie")?.includes(`${AUTH_ERROR_COOKIE}=`)) {
|
|
617
616
|
logger.warn("Auth error cookie detected, preventing redirect loop");
|
|
618
|
-
const headers
|
|
619
|
-
headers
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
return createRedirectResponse(baseUrl, { headers: headers$1 });
|
|
617
|
+
const headers = new Headers();
|
|
618
|
+
headers.append("Set-Cookie", `${AUTH_ERROR_COOKIE}=; Path=/; Max-Age=0`);
|
|
619
|
+
const baseUrl = new URL(config.redirectUri).origin;
|
|
620
|
+
return createRedirectResponse(baseUrl, { headers });
|
|
623
621
|
}
|
|
624
622
|
const oidcConfig = await getOAuthConfig();
|
|
625
623
|
const codeVerifier = oidc.randomPKCECodeVerifier();
|
|
@@ -706,25 +704,24 @@ async function handleCallback(request) {
|
|
|
706
704
|
});
|
|
707
705
|
if (error) {
|
|
708
706
|
if (kcActionStatus && state) {
|
|
709
|
-
const cookies
|
|
710
|
-
const oauthStateRegex
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const redirectUrl$1 = appendApplicationInitiatedActionResult(oauthState$1.returnUrl || "/", kcAction || oauthState$1.kcAction, kcActionStatus);
|
|
707
|
+
const cookies = request.headers.get("Cookie");
|
|
708
|
+
const oauthStateRegex = new RegExp(`${OAUTH_STATE_COOKIE}=([^;]+)`);
|
|
709
|
+
const stateId = (cookies?.match(oauthStateRegex))?.[1];
|
|
710
|
+
if (stateId) {
|
|
711
|
+
const oauthState = await getOAuthState(stateId);
|
|
712
|
+
const stateAge = oauthState ? Date.now() - oauthState.createdAt : Number.POSITIVE_INFINITY;
|
|
713
|
+
if (oauthState && oauthState.state === state && stateAge <= 600 * 1e3) {
|
|
714
|
+
await deleteOAuthState(stateId);
|
|
715
|
+
const headers = new Headers();
|
|
716
|
+
headers.append("Set-Cookie", `${OAUTH_STATE_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`);
|
|
717
|
+
const redirectUrl = appendApplicationInitiatedActionResult(oauthState.returnUrl || "/", kcAction || oauthState.kcAction, kcActionStatus);
|
|
721
718
|
logger.info("Application initiated action returned with OAuth error", {
|
|
722
719
|
error,
|
|
723
720
|
errorDescription,
|
|
724
|
-
kcAction: kcAction || oauthState
|
|
721
|
+
kcAction: kcAction || oauthState.kcAction,
|
|
725
722
|
kcActionStatus
|
|
726
723
|
});
|
|
727
|
-
return createRedirectResponse(redirectUrl
|
|
724
|
+
return createRedirectResponse(redirectUrl, { headers });
|
|
728
725
|
}
|
|
729
726
|
}
|
|
730
727
|
}
|
|
@@ -737,82 +734,74 @@ async function handleCallback(request) {
|
|
|
737
734
|
});
|
|
738
735
|
if (error === "access_denied") {
|
|
739
736
|
logger.warn("Access denied for this application", { clientId: config.clientId });
|
|
740
|
-
const headers
|
|
741
|
-
headers
|
|
742
|
-
headers
|
|
743
|
-
const
|
|
744
|
-
const baseUrl$1 = redirectUri$1.origin;
|
|
737
|
+
const headers = await destroyAuthSession(request);
|
|
738
|
+
headers.append("Set-Cookie", `${AUTH_ERROR_COOKIE}=access_denied; Path=/; HttpOnly; SameSite=Lax; Max-Age=300`);
|
|
739
|
+
headers.append("Set-Cookie", `${AUTH_ERROR_DESC_COOKIE}=${encodeURIComponent(errorDescription || "You do not have access to this application.")}; Path=/; SameSite=Lax; Max-Age=300`);
|
|
740
|
+
const baseUrl = new URL(config.redirectUri).origin;
|
|
745
741
|
logger.info("Redirecting to /auth/login to show access_denied error", {
|
|
746
742
|
hasLandingPage,
|
|
747
|
-
baseUrl
|
|
743
|
+
baseUrl,
|
|
748
744
|
configRedirectUri: config.redirectUri
|
|
749
745
|
});
|
|
750
|
-
return createRedirectResponse(`${baseUrl
|
|
746
|
+
return createRedirectResponse(`${baseUrl}/auth/login`, { headers });
|
|
751
747
|
}
|
|
752
748
|
if (error === "login_required") {
|
|
753
749
|
logger.info("Login required, user session may have expired");
|
|
754
|
-
const headers
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
return createRedirectResponse(baseUrl$1, { headers: headers$2 });
|
|
750
|
+
const headers = await destroyAuthSession(request);
|
|
751
|
+
const baseUrl = new URL(config.redirectUri).origin;
|
|
752
|
+
return createRedirectResponse(baseUrl, { headers });
|
|
758
753
|
}
|
|
759
|
-
const headers
|
|
760
|
-
headers
|
|
761
|
-
if (errorDescription) headers
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
return createRedirectResponse(baseUrl, { headers: headers$1 });
|
|
754
|
+
const headers = await destroyAuthSession(request);
|
|
755
|
+
headers.append("Set-Cookie", `${AUTH_ERROR_COOKIE}=${encodeURIComponent(error)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=300`);
|
|
756
|
+
if (errorDescription) headers.append("Set-Cookie", `${AUTH_ERROR_DESC_COOKIE}=${encodeURIComponent(errorDescription)}; Path=/; SameSite=Lax; Max-Age=300`);
|
|
757
|
+
const baseUrl = new URL(config.redirectUri).origin;
|
|
758
|
+
return createRedirectResponse(baseUrl, { headers });
|
|
765
759
|
}
|
|
766
760
|
const cookies = request.headers.get("Cookie");
|
|
767
761
|
const oauthStateRegex = new RegExp(`${OAUTH_STATE_COOKIE}=([^;]+)`);
|
|
768
|
-
const
|
|
769
|
-
const stateId = cookieMatch?.[1];
|
|
762
|
+
const stateId = (cookies?.match(oauthStateRegex))?.[1];
|
|
770
763
|
if (!stateId) throw new Error("OAuth state ID not found in cookie");
|
|
771
764
|
const oauthState = await getOAuthState(stateId);
|
|
772
765
|
if (!oauthState) throw new Error("No OAuth state found - state may have expired");
|
|
773
766
|
if (!state) throw new Error("Missing required callback state parameter");
|
|
774
767
|
if (oauthState.state !== state) throw new Error(`OAuth state mismatch`);
|
|
775
|
-
|
|
776
|
-
if (stateAge > 10 * 60 * 1e3) throw new Error("OAuth state expired");
|
|
768
|
+
if (Date.now() - oauthState.createdAt > 600 * 1e3) throw new Error("OAuth state expired");
|
|
777
769
|
if (!code) {
|
|
778
770
|
if (kcActionStatus) {
|
|
779
771
|
await deleteOAuthState(stateId);
|
|
780
|
-
const headers
|
|
781
|
-
headers
|
|
782
|
-
const redirectUrl
|
|
772
|
+
const headers = new Headers();
|
|
773
|
+
headers.append("Set-Cookie", `${OAUTH_STATE_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`);
|
|
774
|
+
const redirectUrl = appendApplicationInitiatedActionResult(oauthState.returnUrl || "/", kcAction || oauthState.kcAction, kcActionStatus);
|
|
783
775
|
logger.info("Application initiated action returned without authorization code", {
|
|
784
776
|
kcAction: kcAction || oauthState.kcAction,
|
|
785
777
|
kcActionStatus
|
|
786
778
|
});
|
|
787
|
-
return createRedirectResponse(redirectUrl
|
|
779
|
+
return createRedirectResponse(redirectUrl, { headers });
|
|
788
780
|
}
|
|
789
781
|
throw new Error("Missing required callback code parameter");
|
|
790
782
|
}
|
|
791
783
|
logger.info("Initiating token exchange");
|
|
792
784
|
const oidcConfig = await getOAuthConfig();
|
|
793
|
-
const
|
|
785
|
+
const authorizationResponseUrl = createAuthorizationResponseUrl(request.url, config.redirectUri);
|
|
786
|
+
const tokenResult = await oidc.authorizationCodeGrant(oidcConfig, authorizationResponseUrl, {
|
|
794
787
|
pkceCodeVerifier: oauthState.codeVerifier,
|
|
795
788
|
expectedState: oauthState.state,
|
|
796
789
|
expectedNonce: oauthState.nonce,
|
|
797
790
|
idTokenExpected: config.scope.split(/\s+/).includes("openid")
|
|
798
791
|
});
|
|
799
792
|
logger.info("Token exchange completed successfully");
|
|
800
|
-
const
|
|
801
|
-
const newSessionData = await createSessionData(tokenResult, claims);
|
|
802
|
-
const sessionHeaders = await createAuthSession(request, newSessionData);
|
|
793
|
+
const sessionHeaders = await createAuthSession(request, await createSessionData(tokenResult, getClaimsFromTokenResponse(tokenResult)));
|
|
803
794
|
await deleteOAuthState(stateId);
|
|
804
795
|
const headers = new Headers(sessionHeaders);
|
|
805
796
|
headers.append("Set-Cookie", `${OAUTH_STATE_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`);
|
|
806
797
|
logger.info("OAuth callback processed successfully");
|
|
807
|
-
|
|
808
|
-
return createRedirectResponse(redirectUrl, { headers });
|
|
798
|
+
return createRedirectResponse(appendApplicationInitiatedActionResult(oauthState.returnUrl || "/", kcAction || oauthState.kcAction, kcActionStatus), { headers });
|
|
809
799
|
} catch (error) {
|
|
810
800
|
logger.error("OAuth callback failed", error instanceof Error ? error : void 0);
|
|
811
801
|
try {
|
|
812
802
|
const cookies = request.headers.get("Cookie");
|
|
813
803
|
const oauthStateRegex = new RegExp(`${OAUTH_STATE_COOKIE}=([^;]+)`);
|
|
814
|
-
const
|
|
815
|
-
const stateId = cookieMatch?.[1];
|
|
804
|
+
const stateId = (cookies?.match(oauthStateRegex))?.[1];
|
|
816
805
|
if (stateId) await deleteOAuthState(stateId);
|
|
817
806
|
} catch {}
|
|
818
807
|
await destroyAuthSession(request);
|
|
@@ -840,23 +829,20 @@ async function logout(request) {
|
|
|
840
829
|
const sessionData = await getAuthSession(request);
|
|
841
830
|
const idToken = sessionData.idToken;
|
|
842
831
|
if (logoutScope === "local") {
|
|
843
|
-
const headers
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
headers$1.append("Set-Cookie", "logout_return_url=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");
|
|
832
|
+
const headers = await destroyAuthSession(request);
|
|
833
|
+
const absoluteRedirectUrl = getAbsoluteRedirectUrl(request, returnUrl || "/");
|
|
834
|
+
headers.append("Set-Cookie", "logout_return_url=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");
|
|
847
835
|
logger.info("Local application logout completed", { redirectUrl: absoluteRedirectUrl });
|
|
848
|
-
return createRedirectResponse(absoluteRedirectUrl, { headers
|
|
836
|
+
return createRedirectResponse(absoluteRedirectUrl, { headers });
|
|
849
837
|
}
|
|
850
838
|
const sessionHeaders = new Headers();
|
|
851
839
|
if (logoutScope === "all" && sessionData.userId) {
|
|
852
840
|
await revokeAndDestroyAllUserSessionsBestEffort(sessionData.userId);
|
|
853
841
|
sessionHeaders.append("Set-Cookie", "logout_return_url=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");
|
|
854
|
-
|
|
855
|
-
expiredHeaders.forEach((value, key) => sessionHeaders.append(key, value));
|
|
842
|
+
(await destroyAuthSession(request)).forEach((value, key) => sessionHeaders.append(key, value));
|
|
856
843
|
} else {
|
|
857
844
|
await revokeSessionTokensBestEffort(sessionData);
|
|
858
|
-
|
|
859
|
-
expiredHeaders.forEach((value, key) => sessionHeaders.append(key, value));
|
|
845
|
+
(await destroyAuthSession(request)).forEach((value, key) => sessionHeaders.append(key, value));
|
|
860
846
|
}
|
|
861
847
|
const headers = new Headers(sessionHeaders);
|
|
862
848
|
if (returnUrl) headers.append("Set-Cookie", `logout_return_url=${encodeURIComponent(returnUrl)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=300`);
|
|
@@ -882,14 +868,12 @@ async function logout(request) {
|
|
|
882
868
|
} catch (error) {
|
|
883
869
|
logger.warn("OIDC end_session endpoint unavailable, falling back to local redirect", { error: getOAuthErrorMessage(error) });
|
|
884
870
|
}
|
|
885
|
-
const
|
|
886
|
-
const finalRedirectUrl = getAbsoluteRedirectUrl(request, registeredPath);
|
|
871
|
+
const finalRedirectUrl = getAbsoluteRedirectUrl(request, config.postLogoutRedirectUri || "/");
|
|
887
872
|
logger.info("Logout completed", { redirectUrl: finalRedirectUrl });
|
|
888
873
|
return createRedirectResponse(finalRedirectUrl, { headers });
|
|
889
874
|
} catch (error) {
|
|
890
875
|
logger.error("Logout failed", error instanceof Error ? error : void 0);
|
|
891
|
-
|
|
892
|
-
return createRedirectResponse("/", { headers });
|
|
876
|
+
return createRedirectResponse("/", { headers: await destroyAuthSession(request) });
|
|
893
877
|
}
|
|
894
878
|
}
|
|
895
879
|
/**
|
|
@@ -904,8 +888,7 @@ async function handleBackChannelLogout(request) {
|
|
|
904
888
|
if (request.method !== "POST") return Response.json({ error: "method_not_allowed" }, { status: 405 });
|
|
905
889
|
try {
|
|
906
890
|
const config = getAuthConfig();
|
|
907
|
-
const
|
|
908
|
-
const serverMetadata = oidcConfig.serverMetadata();
|
|
891
|
+
const serverMetadata = (await getOAuthConfig()).serverMetadata();
|
|
909
892
|
const logoutToken = await getLogoutTokenFromRequest(request);
|
|
910
893
|
if (!logoutToken) return Response.json({ error: "missing_logout_token" }, { status: 400 });
|
|
911
894
|
if (!serverMetadata.jwks_uri) throw new Error("OIDC provider does not expose jwks_uri");
|
|
@@ -915,12 +898,11 @@ async function handleBackChannelLogout(request) {
|
|
|
915
898
|
clockTolerance: 60
|
|
916
899
|
});
|
|
917
900
|
const claims = validateBackChannelLogoutClaims(payload, config.clientId);
|
|
918
|
-
const destroyedSessions = await destroyAuthSessionsByIdentitySession({
|
|
919
|
-
sid: claims.sid,
|
|
920
|
-
userId: claims.subject
|
|
921
|
-
});
|
|
922
901
|
const result = {
|
|
923
|
-
destroyedSessions
|
|
902
|
+
destroyedSessions: await destroyAuthSessionsByIdentitySession({
|
|
903
|
+
sid: claims.sid,
|
|
904
|
+
userId: claims.subject
|
|
905
|
+
}),
|
|
924
906
|
issuer: claims.issuer,
|
|
925
907
|
subject: claims.subject,
|
|
926
908
|
sid: claims.sid
|
|
@@ -947,16 +929,14 @@ async function handleBackChannelLogout(request) {
|
|
|
947
929
|
async function handleFrontChannelLogout(request) {
|
|
948
930
|
try {
|
|
949
931
|
const config = getAuthConfig();
|
|
950
|
-
const
|
|
951
|
-
const serverMetadata = oidcConfig.serverMetadata();
|
|
932
|
+
const serverMetadata = (await getOAuthConfig()).serverMetadata();
|
|
952
933
|
const url = new URL(request.url);
|
|
953
934
|
const issuer = url.searchParams.get("iss");
|
|
954
935
|
const sid = url.searchParams.get("sid");
|
|
955
936
|
const expectedIssuer = normalizeIssuer(serverMetadata.issuer ?? config.authority);
|
|
956
937
|
if (!issuer || normalizeIssuer(issuer) !== expectedIssuer) return new Response(null, { status: 204 });
|
|
957
|
-
const destroyedSessions = sid ? await destroyAuthSessionsBySid(sid) : 0;
|
|
958
938
|
const result = {
|
|
959
|
-
destroyedSessions,
|
|
939
|
+
destroyedSessions: sid ? await destroyAuthSessionsBySid(sid) : 0,
|
|
960
940
|
issuer,
|
|
961
941
|
sid: sid ?? void 0
|
|
962
942
|
};
|
|
@@ -975,8 +955,7 @@ async function handleFrontChannelLogout(request) {
|
|
|
975
955
|
*/
|
|
976
956
|
async function getUser(request) {
|
|
977
957
|
try {
|
|
978
|
-
|
|
979
|
-
return sessionData.user || null;
|
|
958
|
+
return (await getAuthSession(request)).user || null;
|
|
980
959
|
} catch (error) {
|
|
981
960
|
logger.error("Failed to get user", error instanceof Error ? error : void 0);
|
|
982
961
|
return null;
|
|
@@ -1000,8 +979,7 @@ async function requireAuth(request) {
|
|
|
1000
979
|
*/
|
|
1001
980
|
async function getAccessToken(request) {
|
|
1002
981
|
try {
|
|
1003
|
-
|
|
1004
|
-
return sessionData.accessToken || null;
|
|
982
|
+
return (await getAuthSession(request)).accessToken || null;
|
|
1005
983
|
} catch (error) {
|
|
1006
984
|
logger.error("Failed to get access token", error instanceof Error ? error : void 0);
|
|
1007
985
|
return null;
|
|
@@ -1060,7 +1038,298 @@ function clearAuthServerCache() {
|
|
|
1060
1038
|
authServerCache = null;
|
|
1061
1039
|
jwksCache.clear();
|
|
1062
1040
|
}
|
|
1063
|
-
|
|
1064
1041
|
//#endregion
|
|
1065
|
-
|
|
1066
|
-
|
|
1042
|
+
//#region src/auth/route-protection.server.ts
|
|
1043
|
+
/**
|
|
1044
|
+
* Route Protection Server Module
|
|
1045
|
+
*
|
|
1046
|
+
* Provides route protection utilities for different authentication levels.
|
|
1047
|
+
*/
|
|
1048
|
+
const REFRESH_THRESHOLD = resolveRefreshThreshold();
|
|
1049
|
+
function resolveRefreshThreshold() {
|
|
1050
|
+
const configured = Number(process.env.OIDC_REFRESH_THRESHOLD_MS);
|
|
1051
|
+
return Number.isFinite(configured) && configured > 0 ? configured : 6e4;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Check if token needs refresh
|
|
1055
|
+
*/
|
|
1056
|
+
async function shouldRefreshToken(request) {
|
|
1057
|
+
try {
|
|
1058
|
+
const sessionData = await getAuthSession(request);
|
|
1059
|
+
if (!sessionData.refreshToken || sessionData.refreshToken.length === 0) {
|
|
1060
|
+
if (sessionData.expiresAt && Date.now() >= sessionData.expiresAt) {
|
|
1061
|
+
logger.info("Token expired and no refresh token, forcing local application logout");
|
|
1062
|
+
throw await logout(await createAutomaticLogoutRequest(request, "expired-no-refresh-token"));
|
|
1063
|
+
}
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
if (!sessionData.expiresAt || !sessionData.accessToken) return false;
|
|
1067
|
+
const now = Date.now();
|
|
1068
|
+
let jwtExpiry = null;
|
|
1069
|
+
try {
|
|
1070
|
+
const tokenParts = sessionData.accessToken.split(".");
|
|
1071
|
+
if (tokenParts.length === 3) {
|
|
1072
|
+
const payload = JSON.parse(Buffer.from(tokenParts[1], "base64").toString());
|
|
1073
|
+
if (payload.exp) jwtExpiry = payload.exp * 1e3;
|
|
1074
|
+
}
|
|
1075
|
+
} catch {}
|
|
1076
|
+
return (jwtExpiry && jwtExpiry < sessionData.expiresAt ? jwtExpiry : sessionData.expiresAt) - now <= REFRESH_THRESHOLD;
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
logger.error("Error checking token refresh", error instanceof Error ? error : void 0);
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Auto-refresh tokens if needed
|
|
1084
|
+
*/
|
|
1085
|
+
async function autoRefreshTokens(request) {
|
|
1086
|
+
try {
|
|
1087
|
+
if (await shouldRefreshToken(request)) {
|
|
1088
|
+
logger.info("Auto-refreshing tokens due to expiry");
|
|
1089
|
+
const result = await refreshTokens(request);
|
|
1090
|
+
if (result.success) logger.info("Auto-refresh completed successfully");
|
|
1091
|
+
else {
|
|
1092
|
+
logger.error("Auto-refresh failed", void 0, { error: result.error });
|
|
1093
|
+
if (result.shouldLogout) {
|
|
1094
|
+
logger.error("Refresh token invalid, forcing local application logout");
|
|
1095
|
+
throw await logout(await createAutomaticLogoutRequest(request, "refresh-failed"));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
let protectionConfig = {};
|
|
1105
|
+
/**
|
|
1106
|
+
* Configure route protection with identity, tenant, and app policy checkers
|
|
1107
|
+
*/
|
|
1108
|
+
function configureRouteProtection(config) {
|
|
1109
|
+
protectionConfig = config;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Reset route protection to its built-in defaults.
|
|
1113
|
+
*/
|
|
1114
|
+
function resetRouteProtectionConfig() {
|
|
1115
|
+
protectionConfig = {};
|
|
1116
|
+
}
|
|
1117
|
+
function createRedirectFromConfig(redirect) {
|
|
1118
|
+
return createRedirectResponse(redirect.location, {
|
|
1119
|
+
headers: redirect.headers,
|
|
1120
|
+
status: redirect.status
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
function getLoginReturnUrl(request, currentPath, protection) {
|
|
1124
|
+
if (protectionConfig.getLoginReturnUrl) return protectionConfig.getLoginReturnUrl({
|
|
1125
|
+
request,
|
|
1126
|
+
currentPath,
|
|
1127
|
+
protection
|
|
1128
|
+
});
|
|
1129
|
+
switch (protection) {
|
|
1130
|
+
default: {
|
|
1131
|
+
const url = new URL(request.url);
|
|
1132
|
+
return url.pathname === "/" ? "/" : `${url.pathname}${url.search}`;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
async function createAutomaticLogoutRequest(request, reason) {
|
|
1137
|
+
if (protectionConfig.createAutomaticLogoutRequest) return protectionConfig.createAutomaticLogoutRequest({
|
|
1138
|
+
request,
|
|
1139
|
+
reason
|
|
1140
|
+
});
|
|
1141
|
+
const logoutUrl = new URL(request.url);
|
|
1142
|
+
logoutUrl.pathname = "/auth/logout";
|
|
1143
|
+
logoutUrl.searchParams.set("logout", "local");
|
|
1144
|
+
return new Request(logoutUrl.toString(), { headers: request.headers });
|
|
1145
|
+
}
|
|
1146
|
+
function createRouteResolverContext(request, user, currentPath, protection) {
|
|
1147
|
+
let identityContextPromise = null;
|
|
1148
|
+
let currentTenantPromise = null;
|
|
1149
|
+
let initializeTenantPromise = null;
|
|
1150
|
+
return {
|
|
1151
|
+
request,
|
|
1152
|
+
user,
|
|
1153
|
+
currentPath,
|
|
1154
|
+
protection,
|
|
1155
|
+
getIdentityContext: async (forceRefresh = false) => {
|
|
1156
|
+
if (!protectionConfig.identityChecker) return null;
|
|
1157
|
+
if (forceRefresh) return protectionConfig.identityChecker.getIdentityContext(request, user.sub, true);
|
|
1158
|
+
if (!identityContextPromise) identityContextPromise = protectionConfig.identityChecker.getIdentityContext(request, user.sub);
|
|
1159
|
+
return identityContextPromise;
|
|
1160
|
+
},
|
|
1161
|
+
getCurrentTenant: async () => {
|
|
1162
|
+
if (!protectionConfig.tenantChecker) return null;
|
|
1163
|
+
if (!currentTenantPromise) currentTenantPromise = protectionConfig.tenantChecker.getCurrentTenant(request);
|
|
1164
|
+
return currentTenantPromise;
|
|
1165
|
+
},
|
|
1166
|
+
initializeTenant: async () => {
|
|
1167
|
+
if (!protectionConfig.tenantChecker) return null;
|
|
1168
|
+
if (!initializeTenantPromise) initializeTenantPromise = protectionConfig.tenantChecker.initializeTenant(request);
|
|
1169
|
+
return initializeTenantPromise;
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
async function applyConfiguredRouteResolution(request, user, currentPath, protection) {
|
|
1174
|
+
if (!protectionConfig.resolveRoute) return false;
|
|
1175
|
+
const context = createRouteResolverContext(request, user, currentPath, protection);
|
|
1176
|
+
try {
|
|
1177
|
+
const result = await protectionConfig.resolveRoute(context);
|
|
1178
|
+
if (result) throw createRedirectFromConfig(result);
|
|
1179
|
+
return true;
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
|
|
1182
|
+
if (!protectionConfig.handleRouteError) throw error;
|
|
1183
|
+
const recovery = await protectionConfig.handleRouteError({
|
|
1184
|
+
...context,
|
|
1185
|
+
error
|
|
1186
|
+
});
|
|
1187
|
+
if (recovery) throw createRedirectFromConfig(recovery);
|
|
1188
|
+
throw error;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Main route protection function
|
|
1193
|
+
*/
|
|
1194
|
+
async function protectRoute(request, protection, loaderFn) {
|
|
1195
|
+
const url = new URL(request.url);
|
|
1196
|
+
switch (protection) {
|
|
1197
|
+
case "public": try {
|
|
1198
|
+
await autoRefreshTokens(request);
|
|
1199
|
+
return loaderFn(await getUser(request) ?? void 0);
|
|
1200
|
+
} catch {
|
|
1201
|
+
return loaderFn(void 0);
|
|
1202
|
+
}
|
|
1203
|
+
case "auth":
|
|
1204
|
+
case "policy": try {
|
|
1205
|
+
await autoRefreshTokens(request);
|
|
1206
|
+
const user = await requireAuth(request);
|
|
1207
|
+
await applyConfiguredRouteResolution(request, user, url.pathname, protection);
|
|
1208
|
+
return loaderFn(user);
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
|
|
1211
|
+
throw await login(request, getLoginReturnUrl(request, url.pathname, protection));
|
|
1212
|
+
}
|
|
1213
|
+
case "onboarding-required": try {
|
|
1214
|
+
await autoRefreshTokens(request);
|
|
1215
|
+
const user = await requireAuth(request);
|
|
1216
|
+
await applyConfiguredRouteResolution(request, user, url.pathname, "onboarding-required");
|
|
1217
|
+
return loaderFn(user);
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
|
|
1220
|
+
throw await login(request, getLoginReturnUrl(request, url.pathname, "onboarding-required"));
|
|
1221
|
+
}
|
|
1222
|
+
case "subscription-required": try {
|
|
1223
|
+
await autoRefreshTokens(request);
|
|
1224
|
+
const user = await requireAuth(request);
|
|
1225
|
+
await applyConfiguredRouteResolution(request, user, url.pathname, "subscription-required");
|
|
1226
|
+
if (protectionConfig.tenantChecker) {
|
|
1227
|
+
if (!await protectionConfig.tenantChecker.getCurrentTenant(request)) await protectionConfig.tenantChecker.initializeTenant(request);
|
|
1228
|
+
}
|
|
1229
|
+
return loaderFn(user);
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
if (error instanceof Response && error.status >= 300 && error.status < 400) throw error;
|
|
1232
|
+
throw await login(request, getLoginReturnUrl(request, url.pathname, "subscription-required"));
|
|
1233
|
+
}
|
|
1234
|
+
default: throw new Error(`Unknown protection level: ${protection}`);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Public route (optional auth)
|
|
1239
|
+
*/
|
|
1240
|
+
const publicRoute = (request, loaderFn) => protectRoute(request, "public", loaderFn);
|
|
1241
|
+
/**
|
|
1242
|
+
* Auth-protected route
|
|
1243
|
+
*/
|
|
1244
|
+
const authRoute = (request, loaderFn) => protectRoute(request, "auth", loaderFn);
|
|
1245
|
+
/**
|
|
1246
|
+
* Generic policy-protected route. Apps define policy in configureRouteProtection.
|
|
1247
|
+
*/
|
|
1248
|
+
const policyRoute = (request, loaderFn) => protectRoute(request, "policy", loaderFn);
|
|
1249
|
+
/**
|
|
1250
|
+
* Alias for auth-protected routes.
|
|
1251
|
+
*/
|
|
1252
|
+
const protectedRoute = authRoute;
|
|
1253
|
+
/**
|
|
1254
|
+
* Setup policy route
|
|
1255
|
+
*
|
|
1256
|
+
* @deprecated Use policyRoute with configureRouteProtection instead.
|
|
1257
|
+
*/
|
|
1258
|
+
const onboardingRoute = (request, loaderFn) => protectRoute(request, "onboarding-required", loaderFn);
|
|
1259
|
+
/**
|
|
1260
|
+
* Entitlement policy route
|
|
1261
|
+
*
|
|
1262
|
+
* @deprecated Use policyRoute with configureRouteProtection instead.
|
|
1263
|
+
*/
|
|
1264
|
+
const subscriptionRoute = (request, loaderFn) => protectRoute(request, "subscription-required", loaderFn);
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region src/auth/token-refresh.server.ts
|
|
1267
|
+
/**
|
|
1268
|
+
* Token Refresh Server Module
|
|
1269
|
+
*/
|
|
1270
|
+
/**
|
|
1271
|
+
* Attempt to refresh the access token
|
|
1272
|
+
*/
|
|
1273
|
+
async function attemptTokenRefresh(request) {
|
|
1274
|
+
try {
|
|
1275
|
+
logger.info("Attempting token refresh due to 401 response");
|
|
1276
|
+
const sessionData = await getAuthSession(request);
|
|
1277
|
+
if (!sessionData.refreshToken) {
|
|
1278
|
+
logger.warn("No refresh token available in session");
|
|
1279
|
+
return {
|
|
1280
|
+
success: false,
|
|
1281
|
+
error: "No refresh token available",
|
|
1282
|
+
shouldLogout: true
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
const refreshResult = await refreshTokens(request, sessionData.refreshToken);
|
|
1286
|
+
if (!refreshResult.success) {
|
|
1287
|
+
logger.error("Token refresh failed", void 0, {
|
|
1288
|
+
error: refreshResult.error,
|
|
1289
|
+
shouldLogout: refreshResult.shouldLogout
|
|
1290
|
+
});
|
|
1291
|
+
return {
|
|
1292
|
+
success: false,
|
|
1293
|
+
error: refreshResult.error,
|
|
1294
|
+
shouldLogout: refreshResult.shouldLogout
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
const tokens = refreshResult.tokens;
|
|
1298
|
+
if (!tokens || !tokens.access_token) {
|
|
1299
|
+
logger.error("Token refresh succeeded but no access token returned");
|
|
1300
|
+
return {
|
|
1301
|
+
success: false,
|
|
1302
|
+
error: "No access token in refresh response",
|
|
1303
|
+
shouldLogout: true
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
logger.info("Token refresh successful");
|
|
1307
|
+
return {
|
|
1308
|
+
success: true,
|
|
1309
|
+
newAccessToken: tokens.access_token
|
|
1310
|
+
};
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
logger.error("Token refresh attempt failed", error instanceof Error ? error : void 0);
|
|
1313
|
+
return {
|
|
1314
|
+
success: false,
|
|
1315
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1316
|
+
shouldLogout: true
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Check if a response indicates token expiration
|
|
1322
|
+
*/
|
|
1323
|
+
function isTokenExpiredResponse(response) {
|
|
1324
|
+
return response.status === 401;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Check if token refresh should be attempted
|
|
1328
|
+
*/
|
|
1329
|
+
function shouldAttemptTokenRefresh(response, hasAuth) {
|
|
1330
|
+
return hasAuth && isTokenExpiredResponse(response);
|
|
1331
|
+
}
|
|
1332
|
+
//#endregion
|
|
1333
|
+
export { resetAuthClaimMapping as A, handleCallback as C, logout as D, login as E, refreshTokens as O, handleBackChannelLogout as S, hasAuthError as T, createAuthorizationResponseUrl as _, configureRouteProtection as a, getAuthError as b, protectRoute as c, resetRouteProtectionConfig as d, shouldRefreshToken as f, configureAuthClaimMapping as g, clearAuthServerCache as h, authRoute as i, requireAuth as k, protectedRoute as l, clearAuthErrorHeaders as m, isTokenExpiredResponse as n, onboardingRoute as o, subscriptionRoute as p, shouldAttemptTokenRefresh as r, policyRoute as s, attemptTokenRefresh as t, publicRoute as u, extractUserInfo as v, handleFrontChannelLogout as w, getUser as x, getAccessToken as y };
|
|
1334
|
+
|
|
1335
|
+
//# sourceMappingURL=server-CqX2yCa4.mjs.map
|