tenneo-auth-plugin 1.0.0 → 1.0.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/dist/index.d.mts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +568 -740
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +568 -741
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,64 +21,6 @@ function getAuthRuntime() {
|
|
|
21
21
|
function isLocalhost(hostname) {
|
|
22
22
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.endsWith(".local");
|
|
23
23
|
}
|
|
24
|
-
function extractTenantKeyFromDomain(urlOrHostname, config) {
|
|
25
|
-
try {
|
|
26
|
-
let hostname;
|
|
27
|
-
if (urlOrHostname.startsWith("http://") || urlOrHostname.startsWith("https://")) {
|
|
28
|
-
try {
|
|
29
|
-
const url = new URL(urlOrHostname);
|
|
30
|
-
hostname = url.hostname;
|
|
31
|
-
} catch {
|
|
32
|
-
hostname = urlOrHostname.replace(/^https?:\/\//, "").split("/")[0];
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
35
|
-
hostname = urlOrHostname;
|
|
36
|
-
}
|
|
37
|
-
const parts = hostname.split(".");
|
|
38
|
-
if (parts.length < 2) return config.getClientCode();
|
|
39
|
-
const tenantKey = parts[0];
|
|
40
|
-
if (!tenantKey || tenantKey.trim() === "") return config.getClientCode();
|
|
41
|
-
if (tenantKey.trim().toLowerCase() === "productlms") return config.getClientCode();
|
|
42
|
-
return tenantKey.trim();
|
|
43
|
-
} catch {
|
|
44
|
-
return config.getClientCode();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
function extractTenantKeyFromUrl(url, config, getStorage3, getKey) {
|
|
48
|
-
try {
|
|
49
|
-
const propsConfigKey = getKey ? getKey("props_config") : "tenneo_auth_props_config";
|
|
50
|
-
const propsConfigStr = getStorage3(propsConfigKey);
|
|
51
|
-
if (propsConfigStr) {
|
|
52
|
-
try {
|
|
53
|
-
const propsConfig = JSON.parse(propsConfigStr);
|
|
54
|
-
if (propsConfig.tenantKey?.trim()) return propsConfig.tenantKey.trim();
|
|
55
|
-
} catch {
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const tenantKeyStorageKey = getKey ? getKey("tenant_key") : "tenneo_auth_tenant_key";
|
|
59
|
-
const storedTenantKey = getStorage3(tenantKeyStorageKey);
|
|
60
|
-
if (storedTenantKey?.trim()) return storedTenantKey.trim();
|
|
61
|
-
const urlParams = new URLSearchParams(url.search);
|
|
62
|
-
const tenantKeyFromQuery = urlParams.get("t");
|
|
63
|
-
if (tenantKeyFromQuery?.trim()) {
|
|
64
|
-
const hostnameParts = url.hostname.split(".");
|
|
65
|
-
const baseDomain = hostnameParts.length >= 2 ? hostnameParts.slice(-2).join(".") : url.hostname;
|
|
66
|
-
const simulatedHostname = `${tenantKeyFromQuery.trim()}.${baseDomain}`;
|
|
67
|
-
const key = extractTenantKeyFromDomain(simulatedHostname, config);
|
|
68
|
-
if (key) return key;
|
|
69
|
-
return tenantKeyFromQuery.trim();
|
|
70
|
-
}
|
|
71
|
-
const keyFromHost = extractTenantKeyFromDomain(url.hostname, config);
|
|
72
|
-
if (keyFromHost) return keyFromHost;
|
|
73
|
-
const clientCode = config.getClientCode();
|
|
74
|
-
if (clientCode?.trim()) return clientCode.trim();
|
|
75
|
-
return config.getClientCode();
|
|
76
|
-
} catch {
|
|
77
|
-
const clientCode = config.getClientCode();
|
|
78
|
-
if (clientCode?.trim()) return clientCode.trim();
|
|
79
|
-
return config.getClientCode();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
24
|
function isCallbackUrlFromUrl(url) {
|
|
83
25
|
const params = new URLSearchParams(url.search);
|
|
84
26
|
return params.has("code") && params.has("state");
|
|
@@ -139,8 +81,7 @@ var CONFIG_KEY_SUFFIXES = {
|
|
|
139
81
|
stateMismatch: "state_mismatch",
|
|
140
82
|
bootstrapInProgress: "bootstrap_in_progress",
|
|
141
83
|
bootstrapTimestamp: "bootstrap_timestamp",
|
|
142
|
-
callbackHandled: "callback_handled"
|
|
143
|
-
idToken: "id_token"
|
|
84
|
+
callbackHandled: "callback_handled"
|
|
144
85
|
};
|
|
145
86
|
|
|
146
87
|
// src/infrastructure/storage/context.ts
|
|
@@ -434,10 +375,6 @@ function createCookieStorageAdapter(namespace, options) {
|
|
|
434
375
|
|
|
435
376
|
// src/infrastructure/storage/api.ts
|
|
436
377
|
var KEY_SUFFIXES = {
|
|
437
|
-
authServerUrl: "auth_server_url",
|
|
438
|
-
clientId: "client_id",
|
|
439
|
-
redirectUri: "redirect_uri",
|
|
440
|
-
tenantId: "tenant_id",
|
|
441
378
|
tenantKey: "tenant_key",
|
|
442
379
|
codeVerifier: "code_verifier",
|
|
443
380
|
state: "state",
|
|
@@ -446,18 +383,6 @@ var KEY_SUFFIXES = {
|
|
|
446
383
|
expiresAt: "expires_at"
|
|
447
384
|
};
|
|
448
385
|
var STORAGE_KEYS = {
|
|
449
|
-
get authServerUrl() {
|
|
450
|
-
return getStorageKey(KEY_SUFFIXES.authServerUrl);
|
|
451
|
-
},
|
|
452
|
-
get clientId() {
|
|
453
|
-
return getStorageKey(KEY_SUFFIXES.clientId);
|
|
454
|
-
},
|
|
455
|
-
get redirectUri() {
|
|
456
|
-
return getStorageKey(KEY_SUFFIXES.redirectUri);
|
|
457
|
-
},
|
|
458
|
-
get tenantId() {
|
|
459
|
-
return getStorageKey(KEY_SUFFIXES.tenantId);
|
|
460
|
-
},
|
|
461
386
|
get tenantKey() {
|
|
462
387
|
return getStorageKey(KEY_SUFFIXES.tenantKey);
|
|
463
388
|
},
|
|
@@ -513,22 +438,22 @@ function removeStorage(key) {
|
|
|
513
438
|
}
|
|
514
439
|
}
|
|
515
440
|
function clearAuthStorage() {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
441
|
+
const keysToClear = [
|
|
442
|
+
STORAGE_KEYS.accessToken,
|
|
443
|
+
STORAGE_KEYS.refreshToken,
|
|
444
|
+
STORAGE_KEYS.expiresAt,
|
|
445
|
+
STORAGE_KEYS.codeVerifier,
|
|
446
|
+
STORAGE_KEYS.state,
|
|
447
|
+
getStorageKey("callback_processed"),
|
|
448
|
+
getStorageKey("props_config"),
|
|
449
|
+
getStorageKey("tenant_switched"),
|
|
450
|
+
getStorageKey("new_tenant_key"),
|
|
451
|
+
getStorageKey("preserved_tenant_key")
|
|
452
|
+
];
|
|
453
|
+
for (const key of keysToClear) removeStorage(key);
|
|
523
454
|
}
|
|
524
455
|
function clearAuthSessionStorage() {
|
|
525
|
-
|
|
526
|
-
if (!adapter?.clearSync) return;
|
|
527
|
-
try {
|
|
528
|
-
adapter.clearTokensSync?.();
|
|
529
|
-
adapter.clearSync();
|
|
530
|
-
} catch {
|
|
531
|
-
}
|
|
456
|
+
clearAuthStorage();
|
|
532
457
|
}
|
|
533
458
|
function clearAllCookies() {
|
|
534
459
|
const runtime = getAuthRuntime();
|
|
@@ -537,27 +462,8 @@ function clearAllCookies() {
|
|
|
537
462
|
} catch {
|
|
538
463
|
}
|
|
539
464
|
}
|
|
540
|
-
function clearAllBrowserStorage() {
|
|
541
|
-
if (typeof window === "undefined") return;
|
|
542
|
-
try {
|
|
543
|
-
window.localStorage?.clear();
|
|
544
|
-
} catch {
|
|
545
|
-
}
|
|
546
|
-
try {
|
|
547
|
-
window.sessionStorage?.clear();
|
|
548
|
-
} catch {
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
465
|
function clearAllStorage() {
|
|
552
|
-
const forceLogin = getStorage(getStorageKey("force_login"));
|
|
553
|
-
const logoutInProgress = getStorage(getStorageKey("logout_in_progress"));
|
|
554
|
-
const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
|
|
555
|
-
clearAuthSessionStorage();
|
|
556
466
|
clearAuthStorage();
|
|
557
|
-
clearAllBrowserStorage();
|
|
558
|
-
if (forceLogin === "true") setStorage(getStorageKey("force_login"), "true");
|
|
559
|
-
if (logoutInProgress === "true") setStorage(getStorageKey("logout_in_progress"), "true");
|
|
560
|
-
if (preservedTenantKey) setStorage(getStorageKey("preserved_tenant_key"), preservedTenantKey);
|
|
561
467
|
clearAllCookies();
|
|
562
468
|
}
|
|
563
469
|
function getAccessToken() {
|
|
@@ -593,14 +499,27 @@ function isAccessTokenExpired() {
|
|
|
593
499
|
const buffer = 60 * 1e3;
|
|
594
500
|
return Date.now() >= expiresAt - buffer;
|
|
595
501
|
}
|
|
502
|
+
function readHostConfig() {
|
|
503
|
+
try {
|
|
504
|
+
const raw = getStorage(getStorageKey("host_config"));
|
|
505
|
+
if (!raw) return {};
|
|
506
|
+
const parsed = JSON.parse(raw);
|
|
507
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
508
|
+
} catch {
|
|
509
|
+
return {};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
596
512
|
function getClientId() {
|
|
597
|
-
|
|
513
|
+
const value = readHostConfig().clientId;
|
|
514
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
598
515
|
}
|
|
599
516
|
function getRedirectUri() {
|
|
600
|
-
|
|
517
|
+
const value = readHostConfig().redirectUri;
|
|
518
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
601
519
|
}
|
|
602
520
|
function getTenantId() {
|
|
603
|
-
|
|
521
|
+
const value = readHostConfig().tenantId;
|
|
522
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
604
523
|
}
|
|
605
524
|
function getCodeVerifier() {
|
|
606
525
|
return getStorage(STORAGE_KEYS.codeVerifier);
|
|
@@ -717,10 +636,8 @@ function resolveAuthLifecycleState(input) {
|
|
|
717
636
|
hasRefreshToken,
|
|
718
637
|
isTokenExpired
|
|
719
638
|
} = input;
|
|
720
|
-
const forceLogin = getStorage3("force_login") === "true";
|
|
721
639
|
const callbackProcessed = getStorage3("callback_processed") === "true";
|
|
722
640
|
const tenantSwitched = getStorage3("tenant_switched") === "true";
|
|
723
|
-
if (forceLogin) return "FORCE_LOGIN" /* FORCE_LOGIN */;
|
|
724
641
|
if (isCallbackUrl2(url)) return "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */;
|
|
725
642
|
if (hasValidToken) return "AUTHENTICATED" /* AUTHENTICATED */;
|
|
726
643
|
if (callbackProcessed && !hasToken) return "LOGIN_REQUIRED" /* LOGIN_REQUIRED */;
|
|
@@ -781,30 +698,58 @@ function setFlowFlags() {
|
|
|
781
698
|
setFlowInProgress();
|
|
782
699
|
}
|
|
783
700
|
|
|
784
|
-
// src/
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
701
|
+
// src/application/events/AuthEvents.ts
|
|
702
|
+
var AuthEventNames = {
|
|
703
|
+
LOGIN_STARTED: "LOGIN_STARTED",
|
|
704
|
+
LOGIN_SUCCESS: "LOGIN_SUCCESS",
|
|
705
|
+
LOGIN_FAILED: "LOGIN_FAILED",
|
|
706
|
+
CALLBACK_SUCCESS: "CALLBACK_SUCCESS",
|
|
707
|
+
CALLBACK_FAILED: "CALLBACK_FAILED",
|
|
708
|
+
TOKEN_REFRESH_SUCCESS: "TOKEN_REFRESH_SUCCESS",
|
|
709
|
+
TOKEN_REFRESH_FAILED: "TOKEN_REFRESH_FAILED",
|
|
710
|
+
TENANT_SWITCHED: "TENANT_SWITCHED",
|
|
711
|
+
LOGOUT_COMPLETED: "LOGOUT_COMPLETED",
|
|
712
|
+
BOOTSTRAP_STATE_DETECTED: "BOOTSTRAP_STATE_DETECTED",
|
|
713
|
+
REDIRECT_LOOP_RISK: "REDIRECT_LOOP_RISK"
|
|
714
|
+
};
|
|
715
|
+
var listeners = /* @__PURE__ */ new Map();
|
|
716
|
+
function getHandlers(event) {
|
|
717
|
+
let set = listeners.get(event);
|
|
718
|
+
if (!set) {
|
|
719
|
+
set = /* @__PURE__ */ new Set();
|
|
720
|
+
listeners.set(event, set);
|
|
721
|
+
}
|
|
722
|
+
return set;
|
|
790
723
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
797
|
-
const base64 = btoa(String.fromCharCode(...hashArray));
|
|
798
|
-
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
799
|
-
} catch {
|
|
800
|
-
throw new Error("Failed to generate code challenge");
|
|
724
|
+
function getWildcardHandlers() {
|
|
725
|
+
let set = listeners.get("*");
|
|
726
|
+
if (!set) {
|
|
727
|
+
set = /* @__PURE__ */ new Set();
|
|
728
|
+
listeners.set("*", set);
|
|
801
729
|
}
|
|
730
|
+
return set;
|
|
802
731
|
}
|
|
803
|
-
function
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
const
|
|
807
|
-
|
|
732
|
+
function emitAuthEvent(event, payload) {
|
|
733
|
+
const handlers = getHandlers(event);
|
|
734
|
+
const wildcard = getWildcardHandlers();
|
|
735
|
+
const p = payload ?? {};
|
|
736
|
+
handlers.forEach((fn) => {
|
|
737
|
+
try {
|
|
738
|
+
fn(p);
|
|
739
|
+
} catch {
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
wildcard.forEach((fn) => {
|
|
743
|
+
try {
|
|
744
|
+
fn({ ...p, event });
|
|
745
|
+
} catch {
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
function subscribeAuthEvent(event, handler) {
|
|
750
|
+
const set = event === "*" ? getWildcardHandlers() : getHandlers(event);
|
|
751
|
+
set.add(handler);
|
|
752
|
+
return () => set.delete(handler);
|
|
808
753
|
}
|
|
809
754
|
|
|
810
755
|
// src/config/constants.ts
|
|
@@ -848,12 +793,17 @@ function readHostConfigFromStorage() {
|
|
|
848
793
|
function resolveMerged(overrides) {
|
|
849
794
|
const stored = readHostConfigFromStorage();
|
|
850
795
|
const cfg = { ...stored, ...inMemoryConfig, ...overrides ?? {} };
|
|
796
|
+
if (!cfg.tenantCode && cfg.tenantKey) {
|
|
797
|
+
cfg.tenantCode = cfg.tenantKey;
|
|
798
|
+
}
|
|
851
799
|
const trimmed = (v) => v == null ? "" : String(v).trim();
|
|
852
800
|
return {
|
|
853
801
|
apiBaseUrl: trimmed(cfg.apiBaseUrl),
|
|
854
802
|
authServerUrl: trimmed(cfg.authServerUrl),
|
|
855
803
|
callbackUrl: trimmed(cfg.callbackUrl),
|
|
856
|
-
clientCode: trimmed(cfg.clientCode)
|
|
804
|
+
clientCode: trimmed(cfg.clientCode),
|
|
805
|
+
tenantCode: trimmed(cfg.tenantCode),
|
|
806
|
+
appId: trimmed(cfg.appId)
|
|
857
807
|
};
|
|
858
808
|
}
|
|
859
809
|
function resolveConfig(overrides) {
|
|
@@ -862,6 +812,9 @@ function resolveConfig(overrides) {
|
|
|
862
812
|
function getConfig() {
|
|
863
813
|
return resolveMerged();
|
|
864
814
|
}
|
|
815
|
+
function getAuthConfig() {
|
|
816
|
+
return resolveMerged();
|
|
817
|
+
}
|
|
865
818
|
function setConfig(overrides) {
|
|
866
819
|
inMemoryConfig = { ...inMemoryConfig, ...overrides };
|
|
867
820
|
}
|
|
@@ -892,28 +845,121 @@ var Config = {
|
|
|
892
845
|
}
|
|
893
846
|
};
|
|
894
847
|
|
|
895
|
-
// src/
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
848
|
+
// src/infrastructure/tenant/TenantConfigCache.ts
|
|
849
|
+
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
850
|
+
var TenantConfigCache = class {
|
|
851
|
+
constructor(ttlMs = DEFAULT_TTL_MS) {
|
|
852
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
853
|
+
this.inFlight = /* @__PURE__ */ new Map();
|
|
854
|
+
this.ttlMs = ttlMs;
|
|
855
|
+
}
|
|
856
|
+
get(tenantKey) {
|
|
857
|
+
const entry = this.cache.get(tenantKey);
|
|
858
|
+
if (!entry) return null;
|
|
859
|
+
if (Date.now() >= entry.expiresAt) {
|
|
860
|
+
this.cache.delete(tenantKey);
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
return entry.config;
|
|
864
|
+
}
|
|
865
|
+
set(tenantKey, config) {
|
|
866
|
+
this.cache.set(tenantKey, {
|
|
867
|
+
config,
|
|
868
|
+
expiresAt: Date.now() + this.ttlMs
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
invalidate(tenantKey) {
|
|
872
|
+
if (tenantKey) {
|
|
873
|
+
this.cache.delete(tenantKey);
|
|
874
|
+
this.inFlight.delete(tenantKey);
|
|
875
|
+
} else {
|
|
876
|
+
this.cache.clear();
|
|
877
|
+
this.inFlight.clear();
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
async getOrResolve(tenantKey, resolver) {
|
|
881
|
+
if (!tenantKey || !tenantKey.trim()) return null;
|
|
882
|
+
const key = tenantKey.trim();
|
|
883
|
+
const cached = this.get(key);
|
|
884
|
+
if (cached) {
|
|
885
|
+
logger.debug("Tenant config from cache", { tenantKey: key });
|
|
886
|
+
return cached;
|
|
887
|
+
}
|
|
888
|
+
const existing = this.inFlight.get(key);
|
|
889
|
+
if (existing) {
|
|
890
|
+
logger.debug("Tenant resolution reused (in-flight)", { tenantKey: key });
|
|
891
|
+
return existing;
|
|
892
|
+
}
|
|
893
|
+
const promise = (async () => {
|
|
894
|
+
try {
|
|
895
|
+
const config = await resolver(key);
|
|
896
|
+
if (config) this.set(key, config);
|
|
897
|
+
return config;
|
|
898
|
+
} finally {
|
|
899
|
+
this.inFlight.delete(key);
|
|
900
|
+
}
|
|
901
|
+
})();
|
|
902
|
+
this.inFlight.set(key, promise);
|
|
903
|
+
return promise;
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
// src/infrastructure/tenant/tenantResolution.ts
|
|
908
|
+
var defaultTenantConfigCache = new TenantConfigCache();
|
|
909
|
+
function consumePropsConfig() {
|
|
910
|
+
const propsConfigKey = getStorageKey("props_config");
|
|
911
|
+
try {
|
|
912
|
+
const propsConfigStr = getStorage(propsConfigKey);
|
|
913
|
+
if (!propsConfigStr) return null;
|
|
914
|
+
const parsed = JSON.parse(propsConfigStr);
|
|
915
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
916
|
+
} catch {
|
|
917
|
+
return null;
|
|
918
|
+
} finally {
|
|
919
|
+
removeStorage(propsConfigKey);
|
|
899
920
|
}
|
|
900
|
-
const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
901
|
-
return guidRegex.test(guid.trim());
|
|
902
921
|
}
|
|
903
|
-
function
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
922
|
+
function readHostConfig2() {
|
|
923
|
+
try {
|
|
924
|
+
const raw = getStorage(getStorageKey("host_config"));
|
|
925
|
+
if (!raw) return null;
|
|
926
|
+
const parsed = JSON.parse(raw);
|
|
927
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
928
|
+
} catch {
|
|
929
|
+
return null;
|
|
909
930
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
931
|
+
}
|
|
932
|
+
function validateTenantKey(value) {
|
|
933
|
+
if (typeof value !== "string") return null;
|
|
934
|
+
const trimmed = value.trim();
|
|
935
|
+
return trimmed ? trimmed : null;
|
|
936
|
+
}
|
|
937
|
+
function resolveTenantKey() {
|
|
938
|
+
const propsConfig = consumePropsConfig();
|
|
939
|
+
const tenantFromProps = validateTenantKey(propsConfig?.tenantKey);
|
|
940
|
+
if (tenantFromProps) return tenantFromProps;
|
|
941
|
+
const hostConfig = readHostConfig2();
|
|
942
|
+
if (!hostConfig) {
|
|
943
|
+
logger.warn("Missing host_config while resolving tenant");
|
|
944
|
+
return null;
|
|
915
945
|
}
|
|
916
|
-
|
|
946
|
+
const tenantFromHost = validateTenantKey(hostConfig?.tenantKey);
|
|
947
|
+
if (tenantFromHost) return tenantFromHost;
|
|
948
|
+
if (hostConfig.tenantKey != null) {
|
|
949
|
+
logger.warn("Invalid tenant key in host_config", { tenantKey: hostConfig.tenantKey });
|
|
950
|
+
}
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
async function resolveTenantConfig(tenantKey, tenantResolver) {
|
|
954
|
+
const resolved = await tenantResolver.resolve(tenantKey);
|
|
955
|
+
if (!resolved) return null;
|
|
956
|
+
return { ...resolved, tenantKey };
|
|
957
|
+
}
|
|
958
|
+
async function resolveTenantConfigCached(tenantKey, tenantResolver) {
|
|
959
|
+
return defaultTenantConfigCache.getOrResolve(
|
|
960
|
+
tenantKey,
|
|
961
|
+
(key) => resolveTenantConfig(key, tenantResolver)
|
|
962
|
+
);
|
|
917
963
|
}
|
|
918
964
|
|
|
919
965
|
// src/application/domain-facade.ts
|
|
@@ -923,33 +969,7 @@ function isLocalhost2() {
|
|
|
923
969
|
return isLocalhost(hostname);
|
|
924
970
|
}
|
|
925
971
|
function extractTenantKey() {
|
|
926
|
-
|
|
927
|
-
if (runtime) {
|
|
928
|
-
const url = runtime.urlProvider.getCurrentUrl();
|
|
929
|
-
return extractTenantKeyFromUrl(url, runtime.configProvider, getStorage, getStorageKey);
|
|
930
|
-
}
|
|
931
|
-
return extractTenantKeyLegacy();
|
|
932
|
-
}
|
|
933
|
-
function extractTenantKeyLegacy() {
|
|
934
|
-
if (typeof window === "undefined") return Config.getClientCode();
|
|
935
|
-
try {
|
|
936
|
-
const propsConfigStr = getStorage(getStorageKey("props_config"));
|
|
937
|
-
if (propsConfigStr) {
|
|
938
|
-
const propsConfig = JSON.parse(propsConfigStr);
|
|
939
|
-
if (propsConfig.tenantKey?.trim()) return propsConfig.tenantKey.trim();
|
|
940
|
-
}
|
|
941
|
-
const stored = getStorage(getStorageKey("tenant_key"));
|
|
942
|
-
if (stored?.trim()) return stored.trim();
|
|
943
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
944
|
-
const t = urlParams.get("t");
|
|
945
|
-
if (t?.trim()) return t.trim();
|
|
946
|
-
const hostname = window.location.hostname;
|
|
947
|
-
const parts = hostname.split(".");
|
|
948
|
-
if (parts.length >= 2 && parts[0] && parts[0].toLowerCase() !== "productlms") return parts[0].trim();
|
|
949
|
-
return Config.getClientCode();
|
|
950
|
-
} catch {
|
|
951
|
-
return Config.getClientCode();
|
|
952
|
-
}
|
|
972
|
+
return resolveTenantKey();
|
|
953
973
|
}
|
|
954
974
|
function isCallbackUrl() {
|
|
955
975
|
const runtime = getAuthRuntime();
|
|
@@ -959,219 +979,6 @@ function isCallbackUrl() {
|
|
|
959
979
|
return params.has("code") && params.has("state");
|
|
960
980
|
}
|
|
961
981
|
|
|
962
|
-
// src/application/events/AuthEvents.ts
|
|
963
|
-
var AuthEventNames = {
|
|
964
|
-
LOGIN_STARTED: "LOGIN_STARTED",
|
|
965
|
-
LOGIN_SUCCESS: "LOGIN_SUCCESS",
|
|
966
|
-
LOGIN_FAILED: "LOGIN_FAILED",
|
|
967
|
-
CALLBACK_SUCCESS: "CALLBACK_SUCCESS",
|
|
968
|
-
CALLBACK_FAILED: "CALLBACK_FAILED",
|
|
969
|
-
TOKEN_REFRESH_SUCCESS: "TOKEN_REFRESH_SUCCESS",
|
|
970
|
-
TOKEN_REFRESH_FAILED: "TOKEN_REFRESH_FAILED",
|
|
971
|
-
TENANT_SWITCHED: "TENANT_SWITCHED",
|
|
972
|
-
LOGOUT_COMPLETED: "LOGOUT_COMPLETED",
|
|
973
|
-
BOOTSTRAP_STATE_DETECTED: "BOOTSTRAP_STATE_DETECTED",
|
|
974
|
-
REDIRECT_LOOP_RISK: "REDIRECT_LOOP_RISK"
|
|
975
|
-
};
|
|
976
|
-
var listeners = /* @__PURE__ */ new Map();
|
|
977
|
-
function getHandlers(event) {
|
|
978
|
-
let set = listeners.get(event);
|
|
979
|
-
if (!set) {
|
|
980
|
-
set = /* @__PURE__ */ new Set();
|
|
981
|
-
listeners.set(event, set);
|
|
982
|
-
}
|
|
983
|
-
return set;
|
|
984
|
-
}
|
|
985
|
-
function getWildcardHandlers() {
|
|
986
|
-
let set = listeners.get("*");
|
|
987
|
-
if (!set) {
|
|
988
|
-
set = /* @__PURE__ */ new Set();
|
|
989
|
-
listeners.set("*", set);
|
|
990
|
-
}
|
|
991
|
-
return set;
|
|
992
|
-
}
|
|
993
|
-
function emitAuthEvent(event, payload) {
|
|
994
|
-
const handlers = getHandlers(event);
|
|
995
|
-
const wildcard = getWildcardHandlers();
|
|
996
|
-
const p = payload ?? {};
|
|
997
|
-
handlers.forEach((fn) => {
|
|
998
|
-
try {
|
|
999
|
-
fn(p);
|
|
1000
|
-
} catch {
|
|
1001
|
-
}
|
|
1002
|
-
});
|
|
1003
|
-
wildcard.forEach((fn) => {
|
|
1004
|
-
try {
|
|
1005
|
-
fn({ ...p, event });
|
|
1006
|
-
} catch {
|
|
1007
|
-
}
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
function subscribeAuthEvent(event, handler) {
|
|
1011
|
-
const set = event === "*" ? getWildcardHandlers() : getHandlers(event);
|
|
1012
|
-
set.add(handler);
|
|
1013
|
-
return () => set.delete(handler);
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// src/application/security/redirectLoopGuard.ts
|
|
1017
|
-
var DEFAULT_MAX_REDIRECTS = 5;
|
|
1018
|
-
var WINDOW_MS = 60 * 1e3;
|
|
1019
|
-
var redirectCount = 0;
|
|
1020
|
-
var firstRedirectAt = 0;
|
|
1021
|
-
function resetRedirectLoopGuard() {
|
|
1022
|
-
redirectCount = 0;
|
|
1023
|
-
firstRedirectAt = 0;
|
|
1024
|
-
}
|
|
1025
|
-
function checkAndIncrementRedirect(maxAttempts = DEFAULT_MAX_REDIRECTS) {
|
|
1026
|
-
const now = Date.now();
|
|
1027
|
-
if (now - firstRedirectAt > WINDOW_MS) {
|
|
1028
|
-
redirectCount = 0;
|
|
1029
|
-
firstRedirectAt = now;
|
|
1030
|
-
}
|
|
1031
|
-
if (redirectCount >= maxAttempts) {
|
|
1032
|
-
emitAuthEvent(AuthEventNames.REDIRECT_LOOP_RISK, {
|
|
1033
|
-
message: `Redirect attempt limit (${maxAttempts}) exceeded`
|
|
1034
|
-
});
|
|
1035
|
-
return false;
|
|
1036
|
-
}
|
|
1037
|
-
redirectCount++;
|
|
1038
|
-
return true;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// src/strategies/oauth2-pkce/oauth.ts
|
|
1042
|
-
function storeTenantConfig(config) {
|
|
1043
|
-
const validation = validateTenantId(config.tenantId);
|
|
1044
|
-
if (!validation.isValid) {
|
|
1045
|
-
logger.warn("Tenant ID validation failed", { tenantId: config.tenantId });
|
|
1046
|
-
}
|
|
1047
|
-
const authServerToStore = Config.getAuthServerUrl() || config.authServer;
|
|
1048
|
-
setStorage(STORAGE_KEYS.authServerUrl, authServerToStore);
|
|
1049
|
-
setStorage(STORAGE_KEYS.clientId, config.client.clientId);
|
|
1050
|
-
setStorage(STORAGE_KEYS.redirectUri, Config.getRedirectUri());
|
|
1051
|
-
setStorage(STORAGE_KEYS.tenantId, config.tenantId);
|
|
1052
|
-
if (config.tenantKey) {
|
|
1053
|
-
setStorage(STORAGE_KEYS.tenantKey, config.tenantKey);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
async function initiateAuthFlow(config) {
|
|
1057
|
-
try {
|
|
1058
|
-
logger.info("Initiating OAuth authorization flow");
|
|
1059
|
-
const authServerFromProps = Config.getAuthServerUrl();
|
|
1060
|
-
const authServerEffective = (authServerFromProps || config?.authServer || "").replace(/\/+$/, "");
|
|
1061
|
-
const redirectUriEffective = Config.getRedirectUri();
|
|
1062
|
-
const storedTenantId = getStorage(STORAGE_KEYS.tenantId);
|
|
1063
|
-
const storedClientId = getStorage(STORAGE_KEYS.clientId);
|
|
1064
|
-
const tenantIdEffective = storedTenantId || config?.tenantId || "";
|
|
1065
|
-
const clientIdEffective = storedClientId || config?.client?.clientId || "";
|
|
1066
|
-
logger.debug("[initiateAuthFlow] Resolved inputs", {
|
|
1067
|
-
authServerFromProps: authServerFromProps ? "(provided)" : "(empty)",
|
|
1068
|
-
authServerEffective,
|
|
1069
|
-
redirectUriEffective,
|
|
1070
|
-
tenantIdEffective,
|
|
1071
|
-
clientIdEffective,
|
|
1072
|
-
scopes: config?.client?.scopes,
|
|
1073
|
-
hasRuntime: !!getAuthRuntime()
|
|
1074
|
-
});
|
|
1075
|
-
const token = getAccessToken();
|
|
1076
|
-
if (token && !isAccessTokenExpired()) {
|
|
1077
|
-
logger.debug("Valid token already exists, skipping auth flow");
|
|
1078
|
-
clearFlowFlags();
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
if (isCallbackUrl()) {
|
|
1082
|
-
logger.debug("Callback URL detected - clearing flow flag to allow callback processing");
|
|
1083
|
-
clearFlowFlags();
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
if (isFlowInProgress()) {
|
|
1087
|
-
if (isFlowFlagStale(3e3)) {
|
|
1088
|
-
logger.info("Stale auth flow flag detected, clearing and proceeding");
|
|
1089
|
-
clearFlowFlags();
|
|
1090
|
-
} else {
|
|
1091
|
-
logger.debug("Auth flow already in progress (recent), skipping duplicate redirect");
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
setFlowFlags();
|
|
1096
|
-
if (!(Config.getAuthServerUrl() || config?.authServer) || !config.client?.clientId || !config.client?.redirectUri) {
|
|
1097
|
-
logger.error("[initiateAuthFlow] Missing required tenant config", {
|
|
1098
|
-
authServerEffective,
|
|
1099
|
-
clientId: config?.client?.clientId,
|
|
1100
|
-
redirectUriFromTenantApi: config?.client?.redirectUri,
|
|
1101
|
-
redirectUriEffective
|
|
1102
|
-
});
|
|
1103
|
-
throw new Error("Invalid tenant configuration for auth flow");
|
|
1104
|
-
}
|
|
1105
|
-
storeTenantConfig(config);
|
|
1106
|
-
logger.debug("[initiateAuthFlow] Stored tenant config, generating PKCE");
|
|
1107
|
-
const codeVerifier = generateCodeVerifier();
|
|
1108
|
-
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
1109
|
-
const state = generateState();
|
|
1110
|
-
removeStorage(STORAGE_KEYS.codeVerifier);
|
|
1111
|
-
removeStorage(STORAGE_KEYS.state);
|
|
1112
|
-
setStorage(STORAGE_KEYS.codeVerifier, codeVerifier);
|
|
1113
|
-
setStorage(STORAGE_KEYS.state, state);
|
|
1114
|
-
const authUrl = buildAuthorizationUrl(config, codeChallenge, state);
|
|
1115
|
-
logger.debug("[initiateAuthFlow] Built authorization URL", {
|
|
1116
|
-
authServerEffective,
|
|
1117
|
-
urlPrefix: authUrl?.slice(0, 80)
|
|
1118
|
-
});
|
|
1119
|
-
if (!authUrl || !(authUrl.startsWith("http://") || authUrl.startsWith("https://"))) {
|
|
1120
|
-
throw new Error("Invalid authorization URL generated");
|
|
1121
|
-
}
|
|
1122
|
-
if (!checkAndIncrementRedirect()) {
|
|
1123
|
-
logger.warn("Redirect loop protection: max redirect attempts exceeded");
|
|
1124
|
-
clearFlowFlags();
|
|
1125
|
-
return;
|
|
1126
|
-
}
|
|
1127
|
-
logger.info("Redirecting to authorization server");
|
|
1128
|
-
emitAuthEvent(AuthEventNames.LOGIN_STARTED, { tenantKey: config.tenantKey });
|
|
1129
|
-
const runtime = getAuthRuntime();
|
|
1130
|
-
if (runtime) {
|
|
1131
|
-
logger.debug("[initiateAuthFlow] Redirect via runtime.urlProvider.redirect");
|
|
1132
|
-
runtime.urlProvider.redirect(authUrl);
|
|
1133
|
-
} else if (typeof window !== "undefined") {
|
|
1134
|
-
logger.debug("[initiateAuthFlow] Redirect via window.location.replace");
|
|
1135
|
-
window.location.replace(authUrl);
|
|
1136
|
-
} else {
|
|
1137
|
-
logger.error("[initiateAuthFlow] No runtime and no window; cannot redirect", { authUrl });
|
|
1138
|
-
}
|
|
1139
|
-
} catch (error) {
|
|
1140
|
-
clearFlowFlags();
|
|
1141
|
-
logger.error("Failed to initiate auth flow", {
|
|
1142
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1143
|
-
});
|
|
1144
|
-
throw error;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
function buildAuthorizationUrl(config, codeChallenge, state) {
|
|
1148
|
-
const authServerBase = (Config.getAuthServerUrl() || config.authServer || "").replace(/\/+$/, "");
|
|
1149
|
-
if (isLocalhost2()) {
|
|
1150
|
-
logger.debug("Building authorization URL", {
|
|
1151
|
-
authServer: authServerBase,
|
|
1152
|
-
clientId: config.client.clientId,
|
|
1153
|
-
redirectUri: config.client.redirectUri,
|
|
1154
|
-
scopes: config.client.scopes
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
|
-
const storedTenantId = getStorage(STORAGE_KEYS.tenantId);
|
|
1158
|
-
const storedClientId = getStorage(STORAGE_KEYS.clientId);
|
|
1159
|
-
const tenantIdToUse = storedTenantId || config.tenantId;
|
|
1160
|
-
const clientIdToUse = storedClientId || config.client.clientId;
|
|
1161
|
-
const redirectUriToUse = Config.getRedirectUri();
|
|
1162
|
-
const params = new URLSearchParams({
|
|
1163
|
-
client_id: `public-${clientIdToUse}`,
|
|
1164
|
-
tenant_id: tenantIdToUse,
|
|
1165
|
-
redirect_uri: redirectUriToUse,
|
|
1166
|
-
response_type: DEFAULT_RESPONSE_TYPE,
|
|
1167
|
-
scope: config.client.scopes,
|
|
1168
|
-
state,
|
|
1169
|
-
code_challenge: codeChallenge,
|
|
1170
|
-
code_challenge_method: DEFAULT_CODE_CHALLENGE_METHOD
|
|
1171
|
-
});
|
|
1172
|
-
return `${authServerBase}/connect/authorize?${params.toString()}`;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
982
|
// src/strategies/oauth2-pkce/token-exchange.ts
|
|
1176
983
|
function storeTokens(tokenData) {
|
|
1177
984
|
const expiresIn = tokenData.expires_in || 3600;
|
|
@@ -1331,6 +1138,31 @@ async function refreshAccessToken() {
|
|
|
1331
1138
|
})();
|
|
1332
1139
|
return refreshPromise;
|
|
1333
1140
|
}
|
|
1141
|
+
|
|
1142
|
+
// src/application/security/redirectLoopGuard.ts
|
|
1143
|
+
var DEFAULT_MAX_REDIRECTS = 5;
|
|
1144
|
+
var WINDOW_MS = 60 * 1e3;
|
|
1145
|
+
var redirectCount = 0;
|
|
1146
|
+
var firstRedirectAt = 0;
|
|
1147
|
+
function resetRedirectLoopGuard() {
|
|
1148
|
+
redirectCount = 0;
|
|
1149
|
+
firstRedirectAt = 0;
|
|
1150
|
+
}
|
|
1151
|
+
function checkAndIncrementRedirect(maxAttempts = DEFAULT_MAX_REDIRECTS) {
|
|
1152
|
+
const now = Date.now();
|
|
1153
|
+
if (now - firstRedirectAt > WINDOW_MS) {
|
|
1154
|
+
redirectCount = 0;
|
|
1155
|
+
firstRedirectAt = now;
|
|
1156
|
+
}
|
|
1157
|
+
if (redirectCount >= maxAttempts) {
|
|
1158
|
+
emitAuthEvent(AuthEventNames.REDIRECT_LOOP_RISK, {
|
|
1159
|
+
message: `Redirect attempt limit (${maxAttempts}) exceeded`
|
|
1160
|
+
});
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
redirectCount++;
|
|
1164
|
+
return true;
|
|
1165
|
+
}
|
|
1334
1166
|
function isProcessed() {
|
|
1335
1167
|
return getStorage(getStorageKey("callback_processed")) === "true";
|
|
1336
1168
|
}
|
|
@@ -1349,15 +1181,6 @@ function clearProcessing() {
|
|
|
1349
1181
|
removeStorage(getStorageKey("callback_processed"));
|
|
1350
1182
|
}
|
|
1351
1183
|
var urlCleaned = false;
|
|
1352
|
-
function cleanUrl() {
|
|
1353
|
-
if (urlCleaned) return;
|
|
1354
|
-
if (isProcessing()) return;
|
|
1355
|
-
const runtime = getAuthRuntime();
|
|
1356
|
-
const clean = runtime ? `${runtime.urlProvider.getCurrentUrl().origin}${runtime.urlProvider.getCurrentUrl().pathname}` : typeof window !== "undefined" ? `${window.location.origin}${window.location.pathname}` : "/";
|
|
1357
|
-
if (runtime) runtime.urlProvider.redirect(clean);
|
|
1358
|
-
else if (typeof window !== "undefined") window.location.replace(clean);
|
|
1359
|
-
urlCleaned = true;
|
|
1360
|
-
}
|
|
1361
1184
|
function cleanUrlSoft() {
|
|
1362
1185
|
if (urlCleaned) return;
|
|
1363
1186
|
if (isProcessing()) return;
|
|
@@ -1368,13 +1191,20 @@ function cleanUrlSoft() {
|
|
|
1368
1191
|
urlCleaned = true;
|
|
1369
1192
|
}
|
|
1370
1193
|
function clearAllAuthFlags() {
|
|
1371
|
-
removeStorage(getStorageKey("force_login"));
|
|
1372
|
-
removeStorage(getStorageKey("logout_in_progress"));
|
|
1373
1194
|
clearFlowFlags();
|
|
1374
1195
|
}
|
|
1375
1196
|
async function handleCallback() {
|
|
1197
|
+
const runtimeSearch = getAuthRuntime()?.urlProvider.getCurrentUrl().search;
|
|
1198
|
+
const windowSearch = typeof window !== "undefined" ? window.location.search : "";
|
|
1199
|
+
logger.debug("[Callback] handleCallback invoked", {
|
|
1200
|
+
isProcessed: isProcessed(),
|
|
1201
|
+
isProcessing: isProcessing(),
|
|
1202
|
+
isCallbackUrl: isCallbackUrl(),
|
|
1203
|
+
runtimeSearch: runtimeSearch ?? "(none)",
|
|
1204
|
+
windowSearch: windowSearch || "(none)"
|
|
1205
|
+
});
|
|
1376
1206
|
if (isProcessed()) {
|
|
1377
|
-
logger.
|
|
1207
|
+
logger.warn("[Callback] Early exit: callback already processed");
|
|
1378
1208
|
if (isCallbackUrl()) {
|
|
1379
1209
|
if (!isProcessing()) cleanUrlSoft();
|
|
1380
1210
|
}
|
|
@@ -1382,31 +1212,16 @@ async function handleCallback() {
|
|
|
1382
1212
|
return true;
|
|
1383
1213
|
}
|
|
1384
1214
|
if (isProcessing()) {
|
|
1385
|
-
logger.
|
|
1215
|
+
logger.warn("[Callback] Early exit: callback processing already in progress");
|
|
1386
1216
|
return true;
|
|
1387
1217
|
}
|
|
1388
|
-
if (getStorage(getStorageKey("force_login")) === "true") {
|
|
1389
|
-
logger.info("Force-login flag present \u2192 callback skipped (logout occurred)");
|
|
1390
|
-
const runtime = getAuthRuntime();
|
|
1391
|
-
if (runtime && runtime.urlProvider.getCurrentUrl().search) cleanUrlSoft();
|
|
1392
|
-
else if (typeof window !== "undefined" && window.location.search) cleanUrlSoft();
|
|
1393
|
-
clearAllAuthFlags();
|
|
1394
|
-
clearProcessing();
|
|
1395
|
-
return false;
|
|
1396
|
-
}
|
|
1397
1218
|
const storedTenantKey = getTenantKey();
|
|
1398
|
-
|
|
1399
|
-
try {
|
|
1400
|
-
const propsConfigStr = getStorage(getStorageKey("props_config"));
|
|
1401
|
-
if (propsConfigStr) {
|
|
1402
|
-
const propsConfig = JSON.parse(propsConfigStr);
|
|
1403
|
-
currentTenantKey = propsConfig.tenantKey || null;
|
|
1404
|
-
}
|
|
1405
|
-
} catch {
|
|
1406
|
-
}
|
|
1407
|
-
if (!currentTenantKey) currentTenantKey = storedTenantKey;
|
|
1219
|
+
const currentTenantKey = resolveTenantKey() ?? storedTenantKey;
|
|
1408
1220
|
if (currentTenantKey && storedTenantKey && currentTenantKey !== storedTenantKey) {
|
|
1409
|
-
logger.
|
|
1221
|
+
logger.warn("[Callback] Early exit: tenant key changed before exchange", {
|
|
1222
|
+
currentTenantKey,
|
|
1223
|
+
storedTenantKey
|
|
1224
|
+
});
|
|
1410
1225
|
clearAllStorage();
|
|
1411
1226
|
clearAllAuthFlags();
|
|
1412
1227
|
clearProcessing();
|
|
@@ -1416,7 +1231,10 @@ async function handleCallback() {
|
|
|
1416
1231
|
if (isCallbackUrl()) cleanUrlSoft();
|
|
1417
1232
|
return false;
|
|
1418
1233
|
}
|
|
1419
|
-
if (!isCallbackUrl())
|
|
1234
|
+
if (!isCallbackUrl()) {
|
|
1235
|
+
logger.warn("[Callback] Early exit: URL is not callback URL");
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1420
1238
|
markProcessing();
|
|
1421
1239
|
clearFlowFlags();
|
|
1422
1240
|
urlCleaned = false;
|
|
@@ -1427,44 +1245,33 @@ async function handleCallback() {
|
|
|
1427
1245
|
const state = params.get("state");
|
|
1428
1246
|
const error = params.get("error");
|
|
1429
1247
|
if (error || !code || !state) {
|
|
1430
|
-
logger.error("
|
|
1248
|
+
logger.error("[Callback] Early exit: invalid OAuth callback params", { error, code, state });
|
|
1431
1249
|
clearAllAuthFlags();
|
|
1432
1250
|
clearProcessing();
|
|
1433
|
-
markProcessed();
|
|
1434
1251
|
cleanUrlSoft();
|
|
1435
1252
|
return false;
|
|
1436
1253
|
}
|
|
1437
1254
|
const storedState = getState();
|
|
1438
1255
|
if (!storedState || storedState !== state) {
|
|
1439
1256
|
const currentTenantKeyFromUrl = extractTenantKey();
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
const propsConfigStr = getStorage(getStorageKey("props_config"));
|
|
1443
|
-
if (propsConfigStr) {
|
|
1444
|
-
const propsConfig = JSON.parse(propsConfigStr);
|
|
1445
|
-
storedTenantKeyFromProps = propsConfig.tenantKey || null;
|
|
1446
|
-
}
|
|
1447
|
-
} catch {
|
|
1448
|
-
}
|
|
1449
|
-
const tenantKeyChanged = currentTenantKeyFromUrl && storedTenantKeyFromProps && currentTenantKeyFromUrl !== storedTenantKeyFromProps;
|
|
1257
|
+
const resolvedTenantKey = resolveTenantKey();
|
|
1258
|
+
const tenantKeyChanged = currentTenantKeyFromUrl && resolvedTenantKey && currentTenantKeyFromUrl !== resolvedTenantKey;
|
|
1450
1259
|
if (tenantKeyChanged) {
|
|
1451
|
-
logger.
|
|
1260
|
+
logger.warn("[Callback] Early exit: state mismatch due to tenant key change");
|
|
1452
1261
|
} else {
|
|
1453
|
-
logger.error("OAuth state mismatch - redirecting to login");
|
|
1262
|
+
logger.error("[Callback] Early exit: OAuth state mismatch - redirecting to login");
|
|
1454
1263
|
}
|
|
1455
1264
|
clearAllAuthFlags();
|
|
1456
1265
|
clearProcessing();
|
|
1457
|
-
markProcessed();
|
|
1458
1266
|
cleanUrlSoft();
|
|
1459
1267
|
setStorage(getStorageKey("state_mismatch"), "true");
|
|
1460
1268
|
return false;
|
|
1461
1269
|
}
|
|
1462
1270
|
const codeVerifier = getCodeVerifier();
|
|
1463
1271
|
if (!codeVerifier) {
|
|
1464
|
-
logger.error("
|
|
1272
|
+
logger.error("[Callback] Early exit: missing PKCE verifier");
|
|
1465
1273
|
clearAllAuthFlags();
|
|
1466
1274
|
clearProcessing();
|
|
1467
|
-
markProcessed();
|
|
1468
1275
|
cleanUrlSoft();
|
|
1469
1276
|
return false;
|
|
1470
1277
|
}
|
|
@@ -1472,25 +1279,29 @@ async function handleCallback() {
|
|
|
1472
1279
|
const redirectUri = getRedirectUri();
|
|
1473
1280
|
const tenantId = getTenantId();
|
|
1474
1281
|
if (!clientId || !redirectUri || !tenantId) {
|
|
1475
|
-
logger.error("
|
|
1282
|
+
logger.error("[Callback] Early exit: missing OAuth config during callback", {
|
|
1283
|
+
hasClientId: !!clientId,
|
|
1284
|
+
hasRedirectUri: !!redirectUri,
|
|
1285
|
+
hasTenantId: !!tenantId
|
|
1286
|
+
});
|
|
1476
1287
|
clearAllAuthFlags();
|
|
1477
1288
|
clearProcessing();
|
|
1478
|
-
markProcessed();
|
|
1479
1289
|
cleanUrlSoft();
|
|
1480
1290
|
return false;
|
|
1481
1291
|
}
|
|
1482
1292
|
const existingToken = getAccessToken();
|
|
1483
1293
|
if (existingToken && !isAccessTokenExpired()) {
|
|
1484
|
-
logger.
|
|
1294
|
+
logger.warn("[Callback] Early exit: valid token already exists - skipping code exchange");
|
|
1485
1295
|
removeStorage(STORAGE_KEYS.codeVerifier);
|
|
1486
1296
|
removeStorage(STORAGE_KEYS.state);
|
|
1487
1297
|
clearAllAuthFlags();
|
|
1488
1298
|
markProcessed();
|
|
1489
|
-
|
|
1299
|
+
cleanUrlSoft();
|
|
1490
1300
|
resetRedirectLoopGuard();
|
|
1491
1301
|
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
1492
1302
|
return true;
|
|
1493
1303
|
}
|
|
1304
|
+
logger.info("[Callback] Entering token exchange");
|
|
1494
1305
|
const tokenData = await exchangeCodeForTokens({
|
|
1495
1306
|
code,
|
|
1496
1307
|
codeVerifier,
|
|
@@ -1510,7 +1321,7 @@ async function handleCallback() {
|
|
|
1510
1321
|
removeStorage(STORAGE_KEYS.state);
|
|
1511
1322
|
clearAllAuthFlags();
|
|
1512
1323
|
markProcessed();
|
|
1513
|
-
|
|
1324
|
+
cleanUrlSoft();
|
|
1514
1325
|
logger.info("OAuth callback processed successfully");
|
|
1515
1326
|
resetRedirectLoopGuard();
|
|
1516
1327
|
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
@@ -1554,173 +1365,22 @@ async function handleCallback() {
|
|
|
1554
1365
|
}
|
|
1555
1366
|
clearAllAuthFlags();
|
|
1556
1367
|
const existingToken = getAccessToken();
|
|
1557
|
-
const tokenValid = existingToken && !isAccessTokenExpired();
|
|
1558
|
-
if (tokenValid) {
|
|
1559
|
-
markProcessed();
|
|
1560
|
-
cleanUrlSoft();
|
|
1561
|
-
resetRedirectLoopGuard();
|
|
1562
|
-
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
1563
|
-
return true;
|
|
1564
|
-
}
|
|
1565
|
-
emitAuthEvent(AuthEventNames.CALLBACK_FAILED, {
|
|
1566
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1567
|
-
});
|
|
1568
|
-
clearProcessing();
|
|
1569
|
-
if (isCallbackUrl()) cleanUrlSoft();
|
|
1570
|
-
return false;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
// src/infrastructure/tenant/TenantConfigCache.ts
|
|
1575
|
-
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
1576
|
-
var TenantConfigCache = class {
|
|
1577
|
-
constructor(ttlMs = DEFAULT_TTL_MS) {
|
|
1578
|
-
this.cache = /* @__PURE__ */ new Map();
|
|
1579
|
-
this.inFlight = /* @__PURE__ */ new Map();
|
|
1580
|
-
this.ttlMs = ttlMs;
|
|
1581
|
-
}
|
|
1582
|
-
get(tenantKey) {
|
|
1583
|
-
const entry = this.cache.get(tenantKey);
|
|
1584
|
-
if (!entry) return null;
|
|
1585
|
-
if (Date.now() >= entry.expiresAt) {
|
|
1586
|
-
this.cache.delete(tenantKey);
|
|
1587
|
-
return null;
|
|
1588
|
-
}
|
|
1589
|
-
return entry.config;
|
|
1590
|
-
}
|
|
1591
|
-
set(tenantKey, config) {
|
|
1592
|
-
this.cache.set(tenantKey, {
|
|
1593
|
-
config,
|
|
1594
|
-
expiresAt: Date.now() + this.ttlMs
|
|
1595
|
-
});
|
|
1596
|
-
}
|
|
1597
|
-
invalidate(tenantKey) {
|
|
1598
|
-
if (tenantKey) {
|
|
1599
|
-
this.cache.delete(tenantKey);
|
|
1600
|
-
this.inFlight.delete(tenantKey);
|
|
1601
|
-
} else {
|
|
1602
|
-
this.cache.clear();
|
|
1603
|
-
this.inFlight.clear();
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
async getOrResolve(tenantKey, resolver) {
|
|
1607
|
-
if (!tenantKey || !tenantKey.trim()) return null;
|
|
1608
|
-
const key = tenantKey.trim();
|
|
1609
|
-
const cached = this.get(key);
|
|
1610
|
-
if (cached) {
|
|
1611
|
-
logger.debug("Tenant config from cache", { tenantKey: key });
|
|
1612
|
-
return cached;
|
|
1613
|
-
}
|
|
1614
|
-
const existing = this.inFlight.get(key);
|
|
1615
|
-
if (existing) {
|
|
1616
|
-
logger.debug("Tenant resolution reused (in-flight)", { tenantKey: key });
|
|
1617
|
-
return existing;
|
|
1618
|
-
}
|
|
1619
|
-
const promise = (async () => {
|
|
1620
|
-
try {
|
|
1621
|
-
const config = await resolver(key);
|
|
1622
|
-
if (config) this.set(key, config);
|
|
1623
|
-
return config;
|
|
1624
|
-
} finally {
|
|
1625
|
-
this.inFlight.delete(key);
|
|
1626
|
-
}
|
|
1627
|
-
})();
|
|
1628
|
-
this.inFlight.set(key, promise);
|
|
1629
|
-
return promise;
|
|
1630
|
-
}
|
|
1631
|
-
};
|
|
1632
|
-
|
|
1633
|
-
// src/infrastructure/tenant/tenantResolution.ts
|
|
1634
|
-
var defaultTenantConfigCache = new TenantConfigCache();
|
|
1635
|
-
function getTenantKeyFromRuntime(url, config) {
|
|
1636
|
-
try {
|
|
1637
|
-
const propsConfigStr = getStorage(getStorageKey("props_config"));
|
|
1638
|
-
if (propsConfigStr) {
|
|
1639
|
-
const propsConfig = JSON.parse(propsConfigStr);
|
|
1640
|
-
if (propsConfig.tenantKey?.trim()) return propsConfig.tenantKey.trim();
|
|
1641
|
-
return null;
|
|
1642
|
-
}
|
|
1643
|
-
} catch {
|
|
1644
|
-
}
|
|
1645
|
-
return extractTenantKeyFromUrl(url, config, getStorage, getStorageKey);
|
|
1646
|
-
}
|
|
1647
|
-
function getPropsConfig() {
|
|
1648
|
-
try {
|
|
1649
|
-
const propsConfigStr = getStorage(getStorageKey("props_config"));
|
|
1650
|
-
if (!propsConfigStr) return null;
|
|
1651
|
-
const parsed = JSON.parse(propsConfigStr);
|
|
1652
|
-
if (parsed && typeof parsed === "object" && "authServerUrl" in parsed) {
|
|
1653
|
-
delete parsed.authServerUrl;
|
|
1654
|
-
}
|
|
1655
|
-
return parsed;
|
|
1656
|
-
} catch {
|
|
1657
|
-
removeStorage(getStorageKey("props_config"));
|
|
1658
|
-
return null;
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
function mergeConfigWithProps(apiEnvConfig, propsConfig, tenantKey) {
|
|
1662
|
-
return {
|
|
1663
|
-
tenantId: propsConfig.tenantId || apiEnvConfig.tenantId,
|
|
1664
|
-
authServer: apiEnvConfig.authServer,
|
|
1665
|
-
tenantKey: tenantKey || apiEnvConfig.tenantKey,
|
|
1666
|
-
client: {
|
|
1667
|
-
clientId: propsConfig.clientId || apiEnvConfig.client.clientId,
|
|
1668
|
-
redirectUri: apiEnvConfig.client.redirectUri,
|
|
1669
|
-
scopes: apiEnvConfig.client.scopes
|
|
1670
|
-
}
|
|
1671
|
-
};
|
|
1672
|
-
}
|
|
1673
|
-
async function resolveTenantConfig(tenantKey, tenantResolver) {
|
|
1674
|
-
if (!tenantKey) return null;
|
|
1675
|
-
const propsConfig = getPropsConfig();
|
|
1676
|
-
const resolved = await tenantResolver.resolve(tenantKey);
|
|
1677
|
-
if (!resolved) return null;
|
|
1678
|
-
removeStorage(getStorageKey("props_config"));
|
|
1679
|
-
return {
|
|
1680
|
-
...propsConfig ? mergeConfigWithProps(resolved, propsConfig, tenantKey) : resolved,
|
|
1681
|
-
tenantKey: tenantKey || resolved.tenantKey
|
|
1682
|
-
};
|
|
1683
|
-
}
|
|
1684
|
-
async function resolveTenantConfigCached(tenantKey, tenantResolver) {
|
|
1685
|
-
if (!tenantKey) return null;
|
|
1686
|
-
return defaultTenantConfigCache.getOrResolve(
|
|
1687
|
-
tenantKey,
|
|
1688
|
-
(key) => resolveTenantConfig(key, tenantResolver)
|
|
1689
|
-
);
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
// src/application/flows/ForceLoginFlow.ts
|
|
1693
|
-
var ForceLoginFlow = class {
|
|
1694
|
-
async execute(context) {
|
|
1695
|
-
const { runtime } = context;
|
|
1696
|
-
const { urlProvider, logger: log } = runtime;
|
|
1697
|
-
const getUrl = () => urlProvider.getCurrentUrl();
|
|
1698
|
-
if (isCallbackUrlFromUrl(getUrl())) {
|
|
1699
|
-
await handleCallback();
|
|
1700
|
-
if (isCallbackUrlFromUrl(getUrl())) {
|
|
1701
|
-
log.warn("Callback URL still present after cleanup, forcing URL clean");
|
|
1702
|
-
const u = getUrl();
|
|
1703
|
-
urlProvider.redirect(`${u.origin}${u.pathname}`);
|
|
1704
|
-
return { completed: true };
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
clearFlowFlags();
|
|
1708
|
-
const tenantKey = getTenantKeyFromRuntime(getUrl(), runtime.configProvider);
|
|
1709
|
-
if (!tenantKey) return { completed: true };
|
|
1710
|
-
const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
|
|
1711
|
-
if (!tenantConfig) {
|
|
1712
|
-
log.error("Tenant config resolution failed for force login", { tenantKey });
|
|
1713
|
-
throw new SanitizedError(
|
|
1714
|
-
"TENANT_CONFIG_RESOLUTION_FAILED",
|
|
1715
|
-
"Unable to resolve tenant configuration",
|
|
1716
|
-
new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
|
|
1717
|
-
);
|
|
1368
|
+
const tokenValid = existingToken && !isAccessTokenExpired();
|
|
1369
|
+
if (tokenValid) {
|
|
1370
|
+
markProcessed();
|
|
1371
|
+
cleanUrlSoft();
|
|
1372
|
+
resetRedirectLoopGuard();
|
|
1373
|
+
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
1374
|
+
return true;
|
|
1718
1375
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1376
|
+
emitAuthEvent(AuthEventNames.CALLBACK_FAILED, {
|
|
1377
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1378
|
+
});
|
|
1379
|
+
clearProcessing();
|
|
1380
|
+
if (isCallbackUrl()) cleanUrlSoft();
|
|
1381
|
+
return false;
|
|
1722
1382
|
}
|
|
1723
|
-
}
|
|
1383
|
+
}
|
|
1724
1384
|
|
|
1725
1385
|
// src/application/flows/CallbackFlow.ts
|
|
1726
1386
|
var CallbackFlow = class {
|
|
@@ -1791,6 +1451,206 @@ function getAccessToken2() {
|
|
|
1791
1451
|
return getAccessToken();
|
|
1792
1452
|
}
|
|
1793
1453
|
|
|
1454
|
+
// src/strategies/oauth2-pkce/pkce.ts
|
|
1455
|
+
function generateCodeVerifier(length = 128) {
|
|
1456
|
+
const array = new Uint8Array(length);
|
|
1457
|
+
crypto.getRandomValues(array);
|
|
1458
|
+
const base64 = btoa(String.fromCharCode(...array));
|
|
1459
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1460
|
+
}
|
|
1461
|
+
async function generateCodeChallenge(verifier) {
|
|
1462
|
+
try {
|
|
1463
|
+
const encoder = new TextEncoder();
|
|
1464
|
+
const data = encoder.encode(verifier);
|
|
1465
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1466
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1467
|
+
const base64 = btoa(String.fromCharCode(...hashArray));
|
|
1468
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1469
|
+
} catch {
|
|
1470
|
+
throw new Error("Failed to generate code challenge");
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function generateState() {
|
|
1474
|
+
const array = new Uint8Array(32);
|
|
1475
|
+
crypto.getRandomValues(array);
|
|
1476
|
+
const base64 = btoa(String.fromCharCode(...array));
|
|
1477
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// src/utils/validation.ts
|
|
1481
|
+
function isValidGuid(guid) {
|
|
1482
|
+
if (!guid || typeof guid !== "string") {
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
1485
|
+
const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1486
|
+
return guidRegex.test(guid.trim());
|
|
1487
|
+
}
|
|
1488
|
+
function validateTenantId(tenantId) {
|
|
1489
|
+
if (!tenantId || tenantId.trim() === "") {
|
|
1490
|
+
return {
|
|
1491
|
+
isValid: false,
|
|
1492
|
+
error: "Tenant ID is required"
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
if (!isValidGuid(tenantId)) {
|
|
1496
|
+
return {
|
|
1497
|
+
isValid: false,
|
|
1498
|
+
error: "Tenant ID must be a valid GUID format (00000000-0000-0000-0000-000000000001)"
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
return { isValid: true };
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// src/strategies/oauth2-pkce/oauth.ts
|
|
1505
|
+
function storeTenantConfig(config) {
|
|
1506
|
+
const validation = validateTenantId(config.tenantId);
|
|
1507
|
+
if (!validation.isValid) {
|
|
1508
|
+
logger.warn("Tenant ID validation failed", { tenantId: config.tenantId });
|
|
1509
|
+
}
|
|
1510
|
+
const authServerToStore = Config.getAuthServerUrl() || config.authServer;
|
|
1511
|
+
const redirectUriToStore = Config.getRedirectUri();
|
|
1512
|
+
const hostConfigKey = getStorageKey("host_config");
|
|
1513
|
+
let existing = {};
|
|
1514
|
+
try {
|
|
1515
|
+
const raw = getStorage(hostConfigKey);
|
|
1516
|
+
if (raw) existing = JSON.parse(raw);
|
|
1517
|
+
} catch {
|
|
1518
|
+
existing = {};
|
|
1519
|
+
}
|
|
1520
|
+
setStorage(
|
|
1521
|
+
hostConfigKey,
|
|
1522
|
+
JSON.stringify({
|
|
1523
|
+
...existing,
|
|
1524
|
+
authServerUrl: authServerToStore,
|
|
1525
|
+
clientId: config.client.clientId,
|
|
1526
|
+
redirectUri: redirectUriToStore,
|
|
1527
|
+
tenantId: config.tenantId,
|
|
1528
|
+
tenantKey: config.tenantKey ?? existing.tenantKey ?? "",
|
|
1529
|
+
tenantCode: config.tenantKey ?? existing.tenantCode ?? ""
|
|
1530
|
+
})
|
|
1531
|
+
);
|
|
1532
|
+
if (config.tenantKey) {
|
|
1533
|
+
setStorage(STORAGE_KEYS.tenantKey, config.tenantKey);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
async function initiateAuthFlow(config) {
|
|
1537
|
+
try {
|
|
1538
|
+
logger.info("Initiating OAuth authorization flow");
|
|
1539
|
+
const authServerFromProps = Config.getAuthServerUrl();
|
|
1540
|
+
const authServerEffective = (authServerFromProps || config?.authServer || "").replace(/\/+$/, "");
|
|
1541
|
+
const redirectUriEffective = Config.getRedirectUri();
|
|
1542
|
+
const tenantIdEffective = config?.tenantId || "";
|
|
1543
|
+
const clientIdEffective = config?.client?.clientId || "";
|
|
1544
|
+
logger.debug("[initiateAuthFlow] Resolved inputs", {
|
|
1545
|
+
authServerFromProps: authServerFromProps ? "(provided)" : "(empty)",
|
|
1546
|
+
authServerEffective,
|
|
1547
|
+
redirectUriEffective,
|
|
1548
|
+
tenantIdEffective,
|
|
1549
|
+
clientIdEffective,
|
|
1550
|
+
scopes: config?.client?.scopes,
|
|
1551
|
+
hasRuntime: !!getAuthRuntime()
|
|
1552
|
+
});
|
|
1553
|
+
const token = getAccessToken();
|
|
1554
|
+
if (token && !isAccessTokenExpired()) {
|
|
1555
|
+
logger.debug("Valid token already exists, skipping auth flow");
|
|
1556
|
+
clearFlowFlags();
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (isCallbackUrl()) {
|
|
1560
|
+
logger.debug("Callback URL detected - clearing flow flag to allow callback processing");
|
|
1561
|
+
clearFlowFlags();
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
if (isFlowInProgress()) {
|
|
1565
|
+
if (isFlowFlagStale(3e3)) {
|
|
1566
|
+
logger.info("Stale auth flow flag detected, clearing and proceeding");
|
|
1567
|
+
clearFlowFlags();
|
|
1568
|
+
} else {
|
|
1569
|
+
logger.debug("Auth flow already in progress (recent), skipping duplicate redirect");
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
setFlowFlags();
|
|
1574
|
+
if (!(Config.getAuthServerUrl() || config?.authServer) || !config.client?.clientId || !config.client?.redirectUri) {
|
|
1575
|
+
logger.error("[initiateAuthFlow] Missing required tenant config", {
|
|
1576
|
+
authServerEffective,
|
|
1577
|
+
clientId: config?.client?.clientId,
|
|
1578
|
+
redirectUriFromTenantApi: config?.client?.redirectUri,
|
|
1579
|
+
redirectUriEffective
|
|
1580
|
+
});
|
|
1581
|
+
throw new Error("Invalid tenant configuration for auth flow");
|
|
1582
|
+
}
|
|
1583
|
+
storeTenantConfig(config);
|
|
1584
|
+
logger.debug("[initiateAuthFlow] Stored tenant config, generating PKCE");
|
|
1585
|
+
removeStorage(getStorageKey("callback_processed"));
|
|
1586
|
+
removeStorage(getStorageKey("state_mismatch"));
|
|
1587
|
+
clearCallbackProcessing();
|
|
1588
|
+
const codeVerifier = generateCodeVerifier();
|
|
1589
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
1590
|
+
const state = generateState();
|
|
1591
|
+
removeStorage(STORAGE_KEYS.codeVerifier);
|
|
1592
|
+
removeStorage(STORAGE_KEYS.state);
|
|
1593
|
+
setStorage(STORAGE_KEYS.codeVerifier, codeVerifier);
|
|
1594
|
+
setStorage(STORAGE_KEYS.state, state);
|
|
1595
|
+
const authUrl = buildAuthorizationUrl(config, codeChallenge, state);
|
|
1596
|
+
logger.debug("[initiateAuthFlow] Built authorization URL", {
|
|
1597
|
+
authServerEffective,
|
|
1598
|
+
urlPrefix: authUrl?.slice(0, 80)
|
|
1599
|
+
});
|
|
1600
|
+
if (!authUrl || !(authUrl.startsWith("http://") || authUrl.startsWith("https://"))) {
|
|
1601
|
+
throw new Error("Invalid authorization URL generated");
|
|
1602
|
+
}
|
|
1603
|
+
if (!checkAndIncrementRedirect()) {
|
|
1604
|
+
logger.warn("Redirect loop protection: max redirect attempts exceeded");
|
|
1605
|
+
clearFlowFlags();
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
logger.info("Redirecting to authorization server");
|
|
1609
|
+
emitAuthEvent(AuthEventNames.LOGIN_STARTED, { tenantKey: config.tenantKey });
|
|
1610
|
+
const runtime = getAuthRuntime();
|
|
1611
|
+
if (runtime) {
|
|
1612
|
+
logger.debug("[initiateAuthFlow] Redirect via runtime.urlProvider.redirect");
|
|
1613
|
+
runtime.urlProvider.redirect(authUrl);
|
|
1614
|
+
} else if (typeof window !== "undefined") {
|
|
1615
|
+
logger.debug("[initiateAuthFlow] Redirect via window.location.replace");
|
|
1616
|
+
window.location.replace(authUrl);
|
|
1617
|
+
} else {
|
|
1618
|
+
logger.error("[initiateAuthFlow] No runtime and no window; cannot redirect", { authUrl });
|
|
1619
|
+
}
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
clearFlowFlags();
|
|
1622
|
+
logger.error("Failed to initiate auth flow", {
|
|
1623
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1624
|
+
});
|
|
1625
|
+
throw error;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
function buildAuthorizationUrl(config, codeChallenge, state) {
|
|
1629
|
+
const authServerBase = (Config.getAuthServerUrl() || config.authServer || "").replace(/\/+$/, "");
|
|
1630
|
+
if (isLocalhost2()) {
|
|
1631
|
+
logger.debug("Building authorization URL", {
|
|
1632
|
+
authServer: authServerBase,
|
|
1633
|
+
clientId: config.client.clientId,
|
|
1634
|
+
redirectUri: config.client.redirectUri,
|
|
1635
|
+
scopes: config.client.scopes
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
const tenantIdToUse = config.tenantId;
|
|
1639
|
+
const clientIdToUse = config.client.clientId;
|
|
1640
|
+
const redirectUriToUse = Config.getRedirectUri();
|
|
1641
|
+
const params = new URLSearchParams({
|
|
1642
|
+
client_id: `public-${clientIdToUse}`,
|
|
1643
|
+
tenant_id: tenantIdToUse,
|
|
1644
|
+
redirect_uri: redirectUriToUse,
|
|
1645
|
+
response_type: DEFAULT_RESPONSE_TYPE,
|
|
1646
|
+
scope: config.client.scopes,
|
|
1647
|
+
state,
|
|
1648
|
+
code_challenge: codeChallenge,
|
|
1649
|
+
code_challenge_method: DEFAULT_CODE_CHALLENGE_METHOD
|
|
1650
|
+
});
|
|
1651
|
+
return `${authServerBase}/connect/authorize?${params.toString()}`;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1794
1654
|
// src/application/flows/RefreshFlow.ts
|
|
1795
1655
|
var RefreshFlow = class {
|
|
1796
1656
|
async execute(context) {
|
|
@@ -1809,21 +1669,16 @@ var RefreshFlow = class {
|
|
|
1809
1669
|
error: error instanceof Error ? error.message : String(error)
|
|
1810
1670
|
});
|
|
1811
1671
|
}
|
|
1812
|
-
const
|
|
1813
|
-
const tenantKeyFromStorage = getStorage(getStorageKey("tenant_key"));
|
|
1814
|
-
const tenantKeyFromConfig = runtime.configProvider.getClientCode?.() ?? "";
|
|
1815
|
-
const tenantKey = (tenantKeyFromRuntime || tenantKeyFromStorage || tenantKeyFromConfig)?.trim();
|
|
1672
|
+
const tenantKey = resolveTenantKey();
|
|
1816
1673
|
log.debug("[RefreshFlow] Tenant key resolved", {
|
|
1817
1674
|
tenantKey: tenantKey ?? "(null)",
|
|
1818
|
-
|
|
1819
|
-
fromStorage: tenantKeyFromStorage ?? "(null)",
|
|
1820
|
-
fromConfig: tenantKeyFromConfig || "(null)"
|
|
1675
|
+
source: tenantKey ? "resolved" : "none"
|
|
1821
1676
|
});
|
|
1822
1677
|
if (!tenantKey) {
|
|
1823
1678
|
throw new SanitizedError(
|
|
1824
1679
|
"TENANT_KEY_RESOLUTION_FAILED",
|
|
1825
1680
|
"Unable to resolve tenant",
|
|
1826
|
-
new Error(
|
|
1681
|
+
new Error(`Tenant key missing for refresh flow at url=${getUrl().href}`)
|
|
1827
1682
|
);
|
|
1828
1683
|
}
|
|
1829
1684
|
const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
|
|
@@ -1852,22 +1707,17 @@ var TenantSwitchFlow = class {
|
|
|
1852
1707
|
emitAuthEvent(AuthEventNames.TENANT_SWITCHED, { tenantKey: newTenantKey ?? void 0 });
|
|
1853
1708
|
removeStorage(getStorageKey("tenant_switched"));
|
|
1854
1709
|
removeStorage(getStorageKey("new_tenant_key"));
|
|
1855
|
-
const
|
|
1856
|
-
const tenantKeyFromStorage = getStorage(getStorageKey("tenant_key"));
|
|
1857
|
-
const tenantKeyFromConfig = runtime.configProvider.getClientCode?.() ?? "";
|
|
1858
|
-
const tenantKey = (newTenantKey || tenantKeyFromRuntime || tenantKeyFromStorage || tenantKeyFromConfig)?.trim();
|
|
1710
|
+
const tenantKey = (newTenantKey || resolveTenantKey())?.trim() || null;
|
|
1859
1711
|
log.debug("[TenantSwitchFlow] Tenant key resolved", {
|
|
1860
1712
|
tenantKey: tenantKey ?? "(null)",
|
|
1861
1713
|
fromNewTenantKey: newTenantKey ?? "(null)",
|
|
1862
|
-
|
|
1863
|
-
fromStorage: tenantKeyFromStorage ?? "(null)",
|
|
1864
|
-
fromConfig: tenantKeyFromConfig || "(null)"
|
|
1714
|
+
source: newTenantKey ? "new_tenant_key" : tenantKey ? "resolved" : "none"
|
|
1865
1715
|
});
|
|
1866
1716
|
if (!tenantKey) {
|
|
1867
1717
|
throw new SanitizedError(
|
|
1868
1718
|
"TENANT_KEY_RESOLUTION_FAILED",
|
|
1869
1719
|
"Unable to resolve tenant after switch",
|
|
1870
|
-
new Error(
|
|
1720
|
+
new Error(`Tenant key missing after switch at url=${getUrl().href}`)
|
|
1871
1721
|
);
|
|
1872
1722
|
}
|
|
1873
1723
|
const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
|
|
@@ -1878,6 +1728,16 @@ var TenantSwitchFlow = class {
|
|
|
1878
1728
|
new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
|
|
1879
1729
|
);
|
|
1880
1730
|
}
|
|
1731
|
+
try {
|
|
1732
|
+
const cfg = getAuthConfig();
|
|
1733
|
+
setStorage(getStorageKey("host_config"), JSON.stringify({
|
|
1734
|
+
...cfg,
|
|
1735
|
+
tenantKey,
|
|
1736
|
+
tenantCode: tenantKey,
|
|
1737
|
+
clientCode: tenantConfig.client?.clientCode ?? cfg.clientCode ?? ""
|
|
1738
|
+
}));
|
|
1739
|
+
} catch {
|
|
1740
|
+
}
|
|
1881
1741
|
setTenantKey(tenantKey);
|
|
1882
1742
|
storeTenantConfig(tenantConfig);
|
|
1883
1743
|
await initiateAuthFlow(tenantConfig);
|
|
@@ -1902,21 +1762,16 @@ var LoginFlow = class {
|
|
|
1902
1762
|
}
|
|
1903
1763
|
const wasStateMismatch = getStorage(getStorageKey("state_mismatch")) === "true";
|
|
1904
1764
|
if (wasStateMismatch) removeStorage(getStorageKey("state_mismatch"));
|
|
1905
|
-
const
|
|
1906
|
-
const tenantKeyFromStorage = getStorage(getStorageKey("tenant_key"));
|
|
1907
|
-
const tenantKeyFromConfig = runtime.configProvider.getClientCode?.() ?? "";
|
|
1908
|
-
const tenantKey = (tenantKeyFromRuntime || tenantKeyFromStorage || tenantKeyFromConfig)?.trim();
|
|
1765
|
+
const tenantKey = resolveTenantKey();
|
|
1909
1766
|
log.debug("[LoginFlow] Tenant key resolved", {
|
|
1910
1767
|
tenantKey: tenantKey ?? "(null)",
|
|
1911
|
-
|
|
1912
|
-
fromStorage: tenantKeyFromStorage ?? "(null)",
|
|
1913
|
-
fromConfig: tenantKeyFromConfig || "(null)"
|
|
1768
|
+
source: tenantKey ? "resolved" : "none"
|
|
1914
1769
|
});
|
|
1915
1770
|
if (!tenantKey) {
|
|
1916
1771
|
throw new SanitizedError(
|
|
1917
1772
|
"TENANT_KEY_RESOLUTION_FAILED",
|
|
1918
1773
|
"Unable to resolve tenant",
|
|
1919
|
-
new Error(
|
|
1774
|
+
new Error(`Tenant key missing for login flow at url=${url.href}`)
|
|
1920
1775
|
);
|
|
1921
1776
|
}
|
|
1922
1777
|
log.debug("[LoginFlow] Resolving tenant config", { tenantKey });
|
|
@@ -1945,7 +1800,6 @@ var LoginFlow = class {
|
|
|
1945
1800
|
};
|
|
1946
1801
|
|
|
1947
1802
|
// src/application/flows/getFlowForState.ts
|
|
1948
|
-
var forceLoginFlow = new ForceLoginFlow();
|
|
1949
1803
|
var callbackFlow = new CallbackFlow();
|
|
1950
1804
|
var authenticatedFlow = new AuthenticatedFlow();
|
|
1951
1805
|
var refreshFlow = new RefreshFlow();
|
|
@@ -1953,8 +1807,6 @@ var tenantSwitchFlow = new TenantSwitchFlow();
|
|
|
1953
1807
|
var loginFlow = new LoginFlow();
|
|
1954
1808
|
function getFlowForState(state) {
|
|
1955
1809
|
switch (state) {
|
|
1956
|
-
case "FORCE_LOGIN" /* FORCE_LOGIN */:
|
|
1957
|
-
return forceLoginFlow;
|
|
1958
1810
|
case "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */:
|
|
1959
1811
|
return callbackFlow;
|
|
1960
1812
|
case "AUTHENTICATED" /* AUTHENTICATED */:
|
|
@@ -2175,34 +2027,8 @@ function checkCookiesDeleted() {
|
|
|
2175
2027
|
function handleCookieDeletion() {
|
|
2176
2028
|
try {
|
|
2177
2029
|
logger.info("Cookies deleted detected - clearing session and redirecting to login");
|
|
2178
|
-
const forceLoginSet = getStorage(getStorageKey("force_login")) === "true";
|
|
2179
|
-
const logoutInProgress = getStorage(getStorageKey("logout_in_progress")) === "true";
|
|
2180
|
-
if (forceLoginSet || logoutInProgress) {
|
|
2181
|
-
logger.debug(
|
|
2182
|
-
"Force login or logout in progress - skipping bootstrap from cookie watcher (avoids duplicate by-code API call)"
|
|
2183
|
-
);
|
|
2184
|
-
return;
|
|
2185
|
-
}
|
|
2186
2030
|
clearAuthStorage();
|
|
2187
|
-
|
|
2188
|
-
if (typeof window !== "undefined") {
|
|
2189
|
-
const forceLogin = window.localStorage.getItem(getStorageKey("force_login"));
|
|
2190
|
-
const logoutInProgressPreserve = window.localStorage.getItem(
|
|
2191
|
-
getStorageKey("logout_in_progress")
|
|
2192
|
-
);
|
|
2193
|
-
clearAuthSessionStorage();
|
|
2194
|
-
if (forceLogin === "true")
|
|
2195
|
-
window.localStorage.setItem(getStorageKey("force_login"), "true");
|
|
2196
|
-
if (logoutInProgressPreserve === "true")
|
|
2197
|
-
window.localStorage.setItem(getStorageKey("logout_in_progress"), "true");
|
|
2198
|
-
} else {
|
|
2199
|
-
clearAuthSessionStorage();
|
|
2200
|
-
}
|
|
2201
|
-
} catch (error) {
|
|
2202
|
-
logger.warn("Failed to clear sessionStorage", {
|
|
2203
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2204
|
-
});
|
|
2205
|
-
}
|
|
2031
|
+
clearAuthSessionStorage();
|
|
2206
2032
|
const cb = getSessionClearedCallback();
|
|
2207
2033
|
if (cb) {
|
|
2208
2034
|
Promise.resolve(cb()).catch((error) => {
|
|
@@ -2236,13 +2062,6 @@ function startCookieWatcher() {
|
|
|
2236
2062
|
allCookies: initialCookies
|
|
2237
2063
|
});
|
|
2238
2064
|
watchInterval = setInterval(async () => {
|
|
2239
|
-
const logoutInProgress = typeof window !== "undefined" && window.localStorage.getItem(getStorageKey("logout_in_progress")) === "true";
|
|
2240
|
-
if (logoutInProgress) {
|
|
2241
|
-
logger.debug("Logout detected. Cookie watcher will not trigger bootstrap.");
|
|
2242
|
-
lastCookieSnapshot = getCookieSnapshot();
|
|
2243
|
-
lastCookieMap = getCookieMap();
|
|
2244
|
-
return;
|
|
2245
|
-
}
|
|
2246
2065
|
const cookiesDeleted = checkCookiesDeleted();
|
|
2247
2066
|
if (cookiesDeleted) {
|
|
2248
2067
|
logger.info("Cookies deleted detected by cookie watcher");
|
|
@@ -2328,11 +2147,6 @@ function startStorageWatcher() {
|
|
|
2328
2147
|
storageEventListener = handleStorageEvent;
|
|
2329
2148
|
window.addEventListener("storage", storageEventListener);
|
|
2330
2149
|
const pollInterval = setInterval(() => {
|
|
2331
|
-
const logoutInProgress = typeof window !== "undefined" && window.localStorage.getItem(getStorageKey("logout_in_progress")) === "true";
|
|
2332
|
-
if (logoutInProgress) {
|
|
2333
|
-
logger.debug("Logout detected. Storage watcher will not trigger bootstrap.");
|
|
2334
|
-
return;
|
|
2335
|
-
}
|
|
2336
2150
|
const wasCleared = checkStorageCleared();
|
|
2337
2151
|
if (wasCleared) {
|
|
2338
2152
|
logger.info("Detected manual sessionStorage clear (polling)! Redirecting to login...");
|
|
@@ -2405,6 +2219,27 @@ function buildUrl(baseUrl, path) {
|
|
|
2405
2219
|
url.pathname = url.pathname.endsWith("/") ? `${url.pathname}${normalizedPath.slice(1)}` : `${url.pathname}${normalizedPath}`;
|
|
2406
2220
|
return url.toString();
|
|
2407
2221
|
}
|
|
2222
|
+
function snapshotHostConfig() {
|
|
2223
|
+
try {
|
|
2224
|
+
const raw = getStorage(getStorageKey("host_config"));
|
|
2225
|
+
if (!raw) return null;
|
|
2226
|
+
const parsed = JSON.parse(raw);
|
|
2227
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
2228
|
+
} catch {
|
|
2229
|
+
return null;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
function restoreHostConfig(config) {
|
|
2233
|
+
if (!config) return;
|
|
2234
|
+
try {
|
|
2235
|
+
setStorage(getStorageKey("host_config"), JSON.stringify(config));
|
|
2236
|
+
logger.debug("Restored host_config after logout cleanup");
|
|
2237
|
+
} catch (error) {
|
|
2238
|
+
logger.warn("Failed to restore host_config after logout cleanup", {
|
|
2239
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2408
2243
|
function getCsrfToken() {
|
|
2409
2244
|
try {
|
|
2410
2245
|
if (typeof document === "undefined") return null;
|
|
@@ -2512,9 +2347,14 @@ function releaseLogoutLock() {
|
|
|
2512
2347
|
}
|
|
2513
2348
|
}
|
|
2514
2349
|
async function logout(tenantKey) {
|
|
2515
|
-
|
|
2350
|
+
const preservedHostConfig = snapshotHostConfig();
|
|
2516
2351
|
if (tenantKey?.trim()) {
|
|
2517
|
-
|
|
2352
|
+
const updated = {
|
|
2353
|
+
...preservedHostConfig ?? {},
|
|
2354
|
+
tenantKey: tenantKey.trim(),
|
|
2355
|
+
tenantCode: tenantKey.trim()
|
|
2356
|
+
};
|
|
2357
|
+
setStorage(getStorageKey("host_config"), JSON.stringify(updated));
|
|
2518
2358
|
}
|
|
2519
2359
|
if (!acquireLogoutLock()) {
|
|
2520
2360
|
return { success: true, message: "Logout already in progress." };
|
|
@@ -2522,7 +2362,8 @@ async function logout(tenantKey) {
|
|
|
2522
2362
|
try {
|
|
2523
2363
|
stopStorageWatcher();
|
|
2524
2364
|
stopCookieWatcher();
|
|
2525
|
-
const
|
|
2365
|
+
const authConfig = getAuthConfig();
|
|
2366
|
+
const apiAuthBaseUrl = authConfig.authServerUrl || Config.getAuthServerUrl();
|
|
2526
2367
|
let serverLogoutSuccess = false;
|
|
2527
2368
|
let serverResponse;
|
|
2528
2369
|
if (apiAuthBaseUrl) {
|
|
@@ -2541,25 +2382,7 @@ async function logout(tenantKey) {
|
|
|
2541
2382
|
logger.warn("API auth base URL not configured, performing local logout only");
|
|
2542
2383
|
}
|
|
2543
2384
|
performLocalLogoutCleanup();
|
|
2544
|
-
|
|
2545
|
-
if (preservedTenantKey) {
|
|
2546
|
-
const propsKey = getStorageKey("props_config");
|
|
2547
|
-
let existing = {};
|
|
2548
|
-
try {
|
|
2549
|
-
const raw = getStorage(propsKey);
|
|
2550
|
-
if (raw) {
|
|
2551
|
-
existing = JSON.parse(raw);
|
|
2552
|
-
}
|
|
2553
|
-
} catch {
|
|
2554
|
-
existing = {};
|
|
2555
|
-
}
|
|
2556
|
-
const merged = {
|
|
2557
|
-
...existing,
|
|
2558
|
-
tenantKey: preservedTenantKey
|
|
2559
|
-
};
|
|
2560
|
-
setStorage(propsKey, JSON.stringify(merged));
|
|
2561
|
-
removeStorage(getStorageKey("preserved_tenant_key"));
|
|
2562
|
-
}
|
|
2385
|
+
restoreHostConfig(preservedHostConfig);
|
|
2563
2386
|
resetCookieWatcher();
|
|
2564
2387
|
startStorageWatcher();
|
|
2565
2388
|
releaseLogoutLock();
|
|
@@ -2569,7 +2392,7 @@ async function logout(tenantKey) {
|
|
|
2569
2392
|
});
|
|
2570
2393
|
emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
|
|
2571
2394
|
serverLogoutSuccess,
|
|
2572
|
-
tenantKey:
|
|
2395
|
+
tenantKey: tenantKey?.trim() || preservedHostConfig?.tenantKey || void 0,
|
|
2573
2396
|
message: serverLogoutSuccess ? "Server logout success" : "Local logout only"
|
|
2574
2397
|
});
|
|
2575
2398
|
return {
|
|
@@ -2582,25 +2405,7 @@ async function logout(tenantKey) {
|
|
|
2582
2405
|
error: error instanceof Error ? error.message : String(error)
|
|
2583
2406
|
});
|
|
2584
2407
|
performLocalLogoutCleanup();
|
|
2585
|
-
|
|
2586
|
-
if (preservedTenantKey) {
|
|
2587
|
-
const propsKey = getStorageKey("props_config");
|
|
2588
|
-
let existing = {};
|
|
2589
|
-
try {
|
|
2590
|
-
const raw = getStorage(propsKey);
|
|
2591
|
-
if (raw) {
|
|
2592
|
-
existing = JSON.parse(raw);
|
|
2593
|
-
}
|
|
2594
|
-
} catch {
|
|
2595
|
-
existing = {};
|
|
2596
|
-
}
|
|
2597
|
-
const merged = {
|
|
2598
|
-
...existing,
|
|
2599
|
-
tenantKey: preservedTenantKey
|
|
2600
|
-
};
|
|
2601
|
-
setStorage(propsKey, JSON.stringify(merged));
|
|
2602
|
-
removeStorage(getStorageKey("preserved_tenant_key"));
|
|
2603
|
-
}
|
|
2408
|
+
restoreHostConfig(preservedHostConfig);
|
|
2604
2409
|
releaseLogoutLock();
|
|
2605
2410
|
return {
|
|
2606
2411
|
success: true,
|
|
@@ -2953,13 +2758,19 @@ function AuthProvider({
|
|
|
2953
2758
|
}, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
|
|
2954
2759
|
react.useEffect(() => {
|
|
2955
2760
|
try {
|
|
2761
|
+
const hostConfigKey = getStorageKey("host_config");
|
|
2762
|
+
const raw = getStorage(hostConfigKey);
|
|
2763
|
+
const existing = raw ? JSON.parse(raw) : {};
|
|
2956
2764
|
const payload = {
|
|
2765
|
+
...existing,
|
|
2957
2766
|
tenantCode: mergedTenantCode ?? "",
|
|
2767
|
+
tenantKey: mergedTenantCode ?? "",
|
|
2768
|
+
appId: appId ?? "",
|
|
2958
2769
|
apiBaseUrl: mergedApiBaseUrl ?? "",
|
|
2959
2770
|
authServerUrl: mergedAuthServerUrl ?? "",
|
|
2960
2771
|
callbackUrl: mergedCallbackUrl ?? ""
|
|
2961
2772
|
};
|
|
2962
|
-
setStorage(
|
|
2773
|
+
setStorage(hostConfigKey, JSON.stringify(payload));
|
|
2963
2774
|
} catch {
|
|
2964
2775
|
}
|
|
2965
2776
|
}, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
|
|
@@ -3003,24 +2814,18 @@ function AuthProvider({
|
|
|
3003
2814
|
const currentUrl = window.location.href;
|
|
3004
2815
|
const isCallback = currentUrl.includes("code=") || currentUrl.includes("state=");
|
|
3005
2816
|
if (isCallback) {
|
|
3006
|
-
const
|
|
3007
|
-
window.history.replaceState({}, document.title,
|
|
2817
|
+
const cleanUrl = window.location.origin + window.location.pathname;
|
|
2818
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
3008
2819
|
}
|
|
3009
2820
|
}
|
|
3010
2821
|
removeStorage(getStorageKey("access_token"));
|
|
3011
2822
|
removeStorage(getStorageKey("refresh_token"));
|
|
3012
2823
|
removeStorage(getStorageKey("expires_at"));
|
|
3013
|
-
removeStorage(getStorageKey("id_token"));
|
|
3014
2824
|
removeStorage(getStorageKey("state"));
|
|
3015
2825
|
removeStorage(getStorageKey("code_verifier"));
|
|
3016
2826
|
removeStorage(getStorageKey("callback_processed"));
|
|
3017
2827
|
clearCallbackProcessing();
|
|
3018
2828
|
clearFlowFlags();
|
|
3019
|
-
removeStorage(getStorageKey("tenant_id"));
|
|
3020
|
-
if (typeof window !== "undefined") {
|
|
3021
|
-
window.localStorage.removeItem(getStorageKey("force_login"));
|
|
3022
|
-
window.localStorage.removeItem(getStorageKey("logout_in_progress"));
|
|
3023
|
-
}
|
|
3024
2829
|
setStorage(getStorageKey("props_changed"), "true");
|
|
3025
2830
|
setStorage(getStorageKey("tenant_switched"), "true");
|
|
3026
2831
|
setStorage(getStorageKey("new_tenant_key"), resolvedTenantKey2);
|
|
@@ -3033,7 +2838,17 @@ function AuthProvider({
|
|
|
3033
2838
|
} else if (previousTenantKey) {
|
|
3034
2839
|
logger.debug("[AuthProvider] TenantKey unchanged", { tenantKey: resolvedTenantKey2 });
|
|
3035
2840
|
}
|
|
3036
|
-
|
|
2841
|
+
try {
|
|
2842
|
+
const hostConfigKey = getStorageKey("host_config");
|
|
2843
|
+
const raw = getStorage(hostConfigKey);
|
|
2844
|
+
const existing = raw ? JSON.parse(raw) : {};
|
|
2845
|
+
setStorage(hostConfigKey, JSON.stringify({
|
|
2846
|
+
...existing,
|
|
2847
|
+
tenantKey: resolvedTenantKey2,
|
|
2848
|
+
tenantCode: resolvedTenantKey2
|
|
2849
|
+
}));
|
|
2850
|
+
} catch {
|
|
2851
|
+
}
|
|
3037
2852
|
setStorage(getStorageKey("tenant_key"), resolvedTenantKey2);
|
|
3038
2853
|
} else {
|
|
3039
2854
|
logger.debug("[AuthProvider] No tenant code determined");
|
|
@@ -3232,17 +3047,29 @@ function AuthCallback({
|
|
|
3232
3047
|
setStatus("loading");
|
|
3233
3048
|
setMessage("Completing sign-in...");
|
|
3234
3049
|
const handled = await handleCallback();
|
|
3235
|
-
if (handled) {
|
|
3236
|
-
const token = getAccessToken();
|
|
3237
|
-
if (token) setTokens(token);
|
|
3238
|
-
setStatus("success");
|
|
3239
|
-
setMessage("Authentication successful. Redirecting...");
|
|
3240
|
-
window.location.replace(successPath);
|
|
3241
|
-
} else {
|
|
3050
|
+
if (!handled) {
|
|
3242
3051
|
setStatus("error");
|
|
3243
3052
|
setMessage("Authentication failed. Please try again.");
|
|
3244
3053
|
window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
const startedAt = Date.now();
|
|
3057
|
+
const MAX_WAIT_MS = 5e3;
|
|
3058
|
+
while (true) {
|
|
3059
|
+
const token = getAccessToken();
|
|
3060
|
+
if (token) {
|
|
3061
|
+
setTokens(token);
|
|
3062
|
+
setStatus("success");
|
|
3063
|
+
setMessage("Authentication successful. Redirecting...");
|
|
3064
|
+
window.location.replace(successPath);
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
if (Date.now() - startedAt > MAX_WAIT_MS) break;
|
|
3068
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
3245
3069
|
}
|
|
3070
|
+
setStatus("error");
|
|
3071
|
+
setMessage("Authentication did not complete. Please try again.");
|
|
3072
|
+
window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
|
|
3246
3073
|
};
|
|
3247
3074
|
runCallback();
|
|
3248
3075
|
}, [homePath, loginPath, successPath, setTokens]);
|
|
@@ -3444,6 +3271,7 @@ exports.enforceHttps = enforceHttps;
|
|
|
3444
3271
|
exports.ensureValidAccessToken = ensureValidAccessToken;
|
|
3445
3272
|
exports.extractTenantKey = extractTenantKey;
|
|
3446
3273
|
exports.getAccessToken = getAccessToken2;
|
|
3274
|
+
exports.getAuthConfig = getAuthConfig;
|
|
3447
3275
|
exports.getAuthRuntime = getAuthRuntime;
|
|
3448
3276
|
exports.getConfig = getConfig;
|
|
3449
3277
|
exports.getStorageAdapter = getStorageAdapter;
|