tenneo-auth-plugin 0.1.11 → 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 +575 -720
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +575 -721
- 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);
|
|
@@ -1557,170 +1368,19 @@ async function handleCallback() {
|
|
|
1557
1368
|
const tokenValid = existingToken && !isAccessTokenExpired();
|
|
1558
1369
|
if (tokenValid) {
|
|
1559
1370
|
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
|
-
);
|
|
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,12 +1669,16 @@ var RefreshFlow = class {
|
|
|
1809
1669
|
error: error instanceof Error ? error.message : String(error)
|
|
1810
1670
|
});
|
|
1811
1671
|
}
|
|
1812
|
-
const tenantKey =
|
|
1672
|
+
const tenantKey = resolveTenantKey();
|
|
1673
|
+
log.debug("[RefreshFlow] Tenant key resolved", {
|
|
1674
|
+
tenantKey: tenantKey ?? "(null)",
|
|
1675
|
+
source: tenantKey ? "resolved" : "none"
|
|
1676
|
+
});
|
|
1813
1677
|
if (!tenantKey) {
|
|
1814
1678
|
throw new SanitizedError(
|
|
1815
1679
|
"TENANT_KEY_RESOLUTION_FAILED",
|
|
1816
1680
|
"Unable to resolve tenant",
|
|
1817
|
-
new Error(
|
|
1681
|
+
new Error(`Tenant key missing for refresh flow at url=${getUrl().href}`)
|
|
1818
1682
|
);
|
|
1819
1683
|
}
|
|
1820
1684
|
const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
|
|
@@ -1843,12 +1707,17 @@ var TenantSwitchFlow = class {
|
|
|
1843
1707
|
emitAuthEvent(AuthEventNames.TENANT_SWITCHED, { tenantKey: newTenantKey ?? void 0 });
|
|
1844
1708
|
removeStorage(getStorageKey("tenant_switched"));
|
|
1845
1709
|
removeStorage(getStorageKey("new_tenant_key"));
|
|
1846
|
-
const tenantKey = newTenantKey ||
|
|
1710
|
+
const tenantKey = (newTenantKey || resolveTenantKey())?.trim() || null;
|
|
1711
|
+
log.debug("[TenantSwitchFlow] Tenant key resolved", {
|
|
1712
|
+
tenantKey: tenantKey ?? "(null)",
|
|
1713
|
+
fromNewTenantKey: newTenantKey ?? "(null)",
|
|
1714
|
+
source: newTenantKey ? "new_tenant_key" : tenantKey ? "resolved" : "none"
|
|
1715
|
+
});
|
|
1847
1716
|
if (!tenantKey) {
|
|
1848
1717
|
throw new SanitizedError(
|
|
1849
1718
|
"TENANT_KEY_RESOLUTION_FAILED",
|
|
1850
1719
|
"Unable to resolve tenant after switch",
|
|
1851
|
-
new Error(
|
|
1720
|
+
new Error(`Tenant key missing after switch at url=${getUrl().href}`)
|
|
1852
1721
|
);
|
|
1853
1722
|
}
|
|
1854
1723
|
const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
|
|
@@ -1859,6 +1728,16 @@ var TenantSwitchFlow = class {
|
|
|
1859
1728
|
new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
|
|
1860
1729
|
);
|
|
1861
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
|
+
}
|
|
1862
1741
|
setTenantKey(tenantKey);
|
|
1863
1742
|
storeTenantConfig(tenantConfig);
|
|
1864
1743
|
await initiateAuthFlow(tenantConfig);
|
|
@@ -1883,13 +1762,16 @@ var LoginFlow = class {
|
|
|
1883
1762
|
}
|
|
1884
1763
|
const wasStateMismatch = getStorage(getStorageKey("state_mismatch")) === "true";
|
|
1885
1764
|
if (wasStateMismatch) removeStorage(getStorageKey("state_mismatch"));
|
|
1886
|
-
const tenantKey =
|
|
1887
|
-
log.debug("[LoginFlow] Tenant key resolved", {
|
|
1765
|
+
const tenantKey = resolveTenantKey();
|
|
1766
|
+
log.debug("[LoginFlow] Tenant key resolved", {
|
|
1767
|
+
tenantKey: tenantKey ?? "(null)",
|
|
1768
|
+
source: tenantKey ? "resolved" : "none"
|
|
1769
|
+
});
|
|
1888
1770
|
if (!tenantKey) {
|
|
1889
1771
|
throw new SanitizedError(
|
|
1890
1772
|
"TENANT_KEY_RESOLUTION_FAILED",
|
|
1891
1773
|
"Unable to resolve tenant",
|
|
1892
|
-
new Error(
|
|
1774
|
+
new Error(`Tenant key missing for login flow at url=${url.href}`)
|
|
1893
1775
|
);
|
|
1894
1776
|
}
|
|
1895
1777
|
log.debug("[LoginFlow] Resolving tenant config", { tenantKey });
|
|
@@ -1918,7 +1800,6 @@ var LoginFlow = class {
|
|
|
1918
1800
|
};
|
|
1919
1801
|
|
|
1920
1802
|
// src/application/flows/getFlowForState.ts
|
|
1921
|
-
var forceLoginFlow = new ForceLoginFlow();
|
|
1922
1803
|
var callbackFlow = new CallbackFlow();
|
|
1923
1804
|
var authenticatedFlow = new AuthenticatedFlow();
|
|
1924
1805
|
var refreshFlow = new RefreshFlow();
|
|
@@ -1926,8 +1807,6 @@ var tenantSwitchFlow = new TenantSwitchFlow();
|
|
|
1926
1807
|
var loginFlow = new LoginFlow();
|
|
1927
1808
|
function getFlowForState(state) {
|
|
1928
1809
|
switch (state) {
|
|
1929
|
-
case "FORCE_LOGIN" /* FORCE_LOGIN */:
|
|
1930
|
-
return forceLoginFlow;
|
|
1931
1810
|
case "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */:
|
|
1932
1811
|
return callbackFlow;
|
|
1933
1812
|
case "AUTHENTICATED" /* AUTHENTICATED */:
|
|
@@ -2148,34 +2027,8 @@ function checkCookiesDeleted() {
|
|
|
2148
2027
|
function handleCookieDeletion() {
|
|
2149
2028
|
try {
|
|
2150
2029
|
logger.info("Cookies deleted detected - clearing session and redirecting to login");
|
|
2151
|
-
const forceLoginSet = getStorage(getStorageKey("force_login")) === "true";
|
|
2152
|
-
const logoutInProgress = getStorage(getStorageKey("logout_in_progress")) === "true";
|
|
2153
|
-
if (forceLoginSet || logoutInProgress) {
|
|
2154
|
-
logger.debug(
|
|
2155
|
-
"Force login or logout in progress - skipping bootstrap from cookie watcher (avoids duplicate by-code API call)"
|
|
2156
|
-
);
|
|
2157
|
-
return;
|
|
2158
|
-
}
|
|
2159
2030
|
clearAuthStorage();
|
|
2160
|
-
|
|
2161
|
-
if (typeof window !== "undefined") {
|
|
2162
|
-
const forceLogin = window.localStorage.getItem(getStorageKey("force_login"));
|
|
2163
|
-
const logoutInProgressPreserve = window.localStorage.getItem(
|
|
2164
|
-
getStorageKey("logout_in_progress")
|
|
2165
|
-
);
|
|
2166
|
-
clearAuthSessionStorage();
|
|
2167
|
-
if (forceLogin === "true")
|
|
2168
|
-
window.localStorage.setItem(getStorageKey("force_login"), "true");
|
|
2169
|
-
if (logoutInProgressPreserve === "true")
|
|
2170
|
-
window.localStorage.setItem(getStorageKey("logout_in_progress"), "true");
|
|
2171
|
-
} else {
|
|
2172
|
-
clearAuthSessionStorage();
|
|
2173
|
-
}
|
|
2174
|
-
} catch (error) {
|
|
2175
|
-
logger.warn("Failed to clear sessionStorage", {
|
|
2176
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2177
|
-
});
|
|
2178
|
-
}
|
|
2031
|
+
clearAuthSessionStorage();
|
|
2179
2032
|
const cb = getSessionClearedCallback();
|
|
2180
2033
|
if (cb) {
|
|
2181
2034
|
Promise.resolve(cb()).catch((error) => {
|
|
@@ -2209,13 +2062,6 @@ function startCookieWatcher() {
|
|
|
2209
2062
|
allCookies: initialCookies
|
|
2210
2063
|
});
|
|
2211
2064
|
watchInterval = setInterval(async () => {
|
|
2212
|
-
const logoutInProgress = typeof window !== "undefined" && window.localStorage.getItem(getStorageKey("logout_in_progress")) === "true";
|
|
2213
|
-
if (logoutInProgress) {
|
|
2214
|
-
logger.debug("Logout detected. Cookie watcher will not trigger bootstrap.");
|
|
2215
|
-
lastCookieSnapshot = getCookieSnapshot();
|
|
2216
|
-
lastCookieMap = getCookieMap();
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
2065
|
const cookiesDeleted = checkCookiesDeleted();
|
|
2220
2066
|
if (cookiesDeleted) {
|
|
2221
2067
|
logger.info("Cookies deleted detected by cookie watcher");
|
|
@@ -2301,11 +2147,6 @@ function startStorageWatcher() {
|
|
|
2301
2147
|
storageEventListener = handleStorageEvent;
|
|
2302
2148
|
window.addEventListener("storage", storageEventListener);
|
|
2303
2149
|
const pollInterval = setInterval(() => {
|
|
2304
|
-
const logoutInProgress = typeof window !== "undefined" && window.localStorage.getItem(getStorageKey("logout_in_progress")) === "true";
|
|
2305
|
-
if (logoutInProgress) {
|
|
2306
|
-
logger.debug("Logout detected. Storage watcher will not trigger bootstrap.");
|
|
2307
|
-
return;
|
|
2308
|
-
}
|
|
2309
2150
|
const wasCleared = checkStorageCleared();
|
|
2310
2151
|
if (wasCleared) {
|
|
2311
2152
|
logger.info("Detected manual sessionStorage clear (polling)! Redirecting to login...");
|
|
@@ -2378,6 +2219,27 @@ function buildUrl(baseUrl, path) {
|
|
|
2378
2219
|
url.pathname = url.pathname.endsWith("/") ? `${url.pathname}${normalizedPath.slice(1)}` : `${url.pathname}${normalizedPath}`;
|
|
2379
2220
|
return url.toString();
|
|
2380
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
|
+
}
|
|
2381
2243
|
function getCsrfToken() {
|
|
2382
2244
|
try {
|
|
2383
2245
|
if (typeof document === "undefined") return null;
|
|
@@ -2485,9 +2347,14 @@ function releaseLogoutLock() {
|
|
|
2485
2347
|
}
|
|
2486
2348
|
}
|
|
2487
2349
|
async function logout(tenantKey) {
|
|
2488
|
-
|
|
2350
|
+
const preservedHostConfig = snapshotHostConfig();
|
|
2489
2351
|
if (tenantKey?.trim()) {
|
|
2490
|
-
|
|
2352
|
+
const updated = {
|
|
2353
|
+
...preservedHostConfig ?? {},
|
|
2354
|
+
tenantKey: tenantKey.trim(),
|
|
2355
|
+
tenantCode: tenantKey.trim()
|
|
2356
|
+
};
|
|
2357
|
+
setStorage(getStorageKey("host_config"), JSON.stringify(updated));
|
|
2491
2358
|
}
|
|
2492
2359
|
if (!acquireLogoutLock()) {
|
|
2493
2360
|
return { success: true, message: "Logout already in progress." };
|
|
@@ -2495,7 +2362,8 @@ async function logout(tenantKey) {
|
|
|
2495
2362
|
try {
|
|
2496
2363
|
stopStorageWatcher();
|
|
2497
2364
|
stopCookieWatcher();
|
|
2498
|
-
const
|
|
2365
|
+
const authConfig = getAuthConfig();
|
|
2366
|
+
const apiAuthBaseUrl = authConfig.authServerUrl || Config.getAuthServerUrl();
|
|
2499
2367
|
let serverLogoutSuccess = false;
|
|
2500
2368
|
let serverResponse;
|
|
2501
2369
|
if (apiAuthBaseUrl) {
|
|
@@ -2514,25 +2382,7 @@ async function logout(tenantKey) {
|
|
|
2514
2382
|
logger.warn("API auth base URL not configured, performing local logout only");
|
|
2515
2383
|
}
|
|
2516
2384
|
performLocalLogoutCleanup();
|
|
2517
|
-
|
|
2518
|
-
if (preservedTenantKey) {
|
|
2519
|
-
const propsKey = getStorageKey("props_config");
|
|
2520
|
-
let existing = {};
|
|
2521
|
-
try {
|
|
2522
|
-
const raw = getStorage(propsKey);
|
|
2523
|
-
if (raw) {
|
|
2524
|
-
existing = JSON.parse(raw);
|
|
2525
|
-
}
|
|
2526
|
-
} catch {
|
|
2527
|
-
existing = {};
|
|
2528
|
-
}
|
|
2529
|
-
const merged = {
|
|
2530
|
-
...existing,
|
|
2531
|
-
tenantKey: preservedTenantKey
|
|
2532
|
-
};
|
|
2533
|
-
setStorage(propsKey, JSON.stringify(merged));
|
|
2534
|
-
removeStorage(getStorageKey("preserved_tenant_key"));
|
|
2535
|
-
}
|
|
2385
|
+
restoreHostConfig(preservedHostConfig);
|
|
2536
2386
|
resetCookieWatcher();
|
|
2537
2387
|
startStorageWatcher();
|
|
2538
2388
|
releaseLogoutLock();
|
|
@@ -2542,7 +2392,7 @@ async function logout(tenantKey) {
|
|
|
2542
2392
|
});
|
|
2543
2393
|
emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
|
|
2544
2394
|
serverLogoutSuccess,
|
|
2545
|
-
tenantKey:
|
|
2395
|
+
tenantKey: tenantKey?.trim() || preservedHostConfig?.tenantKey || void 0,
|
|
2546
2396
|
message: serverLogoutSuccess ? "Server logout success" : "Local logout only"
|
|
2547
2397
|
});
|
|
2548
2398
|
return {
|
|
@@ -2555,25 +2405,7 @@ async function logout(tenantKey) {
|
|
|
2555
2405
|
error: error instanceof Error ? error.message : String(error)
|
|
2556
2406
|
});
|
|
2557
2407
|
performLocalLogoutCleanup();
|
|
2558
|
-
|
|
2559
|
-
if (preservedTenantKey) {
|
|
2560
|
-
const propsKey = getStorageKey("props_config");
|
|
2561
|
-
let existing = {};
|
|
2562
|
-
try {
|
|
2563
|
-
const raw = getStorage(propsKey);
|
|
2564
|
-
if (raw) {
|
|
2565
|
-
existing = JSON.parse(raw);
|
|
2566
|
-
}
|
|
2567
|
-
} catch {
|
|
2568
|
-
existing = {};
|
|
2569
|
-
}
|
|
2570
|
-
const merged = {
|
|
2571
|
-
...existing,
|
|
2572
|
-
tenantKey: preservedTenantKey
|
|
2573
|
-
};
|
|
2574
|
-
setStorage(propsKey, JSON.stringify(merged));
|
|
2575
|
-
removeStorage(getStorageKey("preserved_tenant_key"));
|
|
2576
|
-
}
|
|
2408
|
+
restoreHostConfig(preservedHostConfig);
|
|
2577
2409
|
releaseLogoutLock();
|
|
2578
2410
|
return {
|
|
2579
2411
|
success: true,
|
|
@@ -2926,13 +2758,19 @@ function AuthProvider({
|
|
|
2926
2758
|
}, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
|
|
2927
2759
|
react.useEffect(() => {
|
|
2928
2760
|
try {
|
|
2761
|
+
const hostConfigKey = getStorageKey("host_config");
|
|
2762
|
+
const raw = getStorage(hostConfigKey);
|
|
2763
|
+
const existing = raw ? JSON.parse(raw) : {};
|
|
2929
2764
|
const payload = {
|
|
2765
|
+
...existing,
|
|
2930
2766
|
tenantCode: mergedTenantCode ?? "",
|
|
2767
|
+
tenantKey: mergedTenantCode ?? "",
|
|
2768
|
+
appId: appId ?? "",
|
|
2931
2769
|
apiBaseUrl: mergedApiBaseUrl ?? "",
|
|
2932
2770
|
authServerUrl: mergedAuthServerUrl ?? "",
|
|
2933
2771
|
callbackUrl: mergedCallbackUrl ?? ""
|
|
2934
2772
|
};
|
|
2935
|
-
setStorage(
|
|
2773
|
+
setStorage(hostConfigKey, JSON.stringify(payload));
|
|
2936
2774
|
} catch {
|
|
2937
2775
|
}
|
|
2938
2776
|
}, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
|
|
@@ -2976,24 +2814,18 @@ function AuthProvider({
|
|
|
2976
2814
|
const currentUrl = window.location.href;
|
|
2977
2815
|
const isCallback = currentUrl.includes("code=") || currentUrl.includes("state=");
|
|
2978
2816
|
if (isCallback) {
|
|
2979
|
-
const
|
|
2980
|
-
window.history.replaceState({}, document.title,
|
|
2817
|
+
const cleanUrl = window.location.origin + window.location.pathname;
|
|
2818
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
2981
2819
|
}
|
|
2982
2820
|
}
|
|
2983
2821
|
removeStorage(getStorageKey("access_token"));
|
|
2984
2822
|
removeStorage(getStorageKey("refresh_token"));
|
|
2985
2823
|
removeStorage(getStorageKey("expires_at"));
|
|
2986
|
-
removeStorage(getStorageKey("id_token"));
|
|
2987
2824
|
removeStorage(getStorageKey("state"));
|
|
2988
2825
|
removeStorage(getStorageKey("code_verifier"));
|
|
2989
2826
|
removeStorage(getStorageKey("callback_processed"));
|
|
2990
2827
|
clearCallbackProcessing();
|
|
2991
2828
|
clearFlowFlags();
|
|
2992
|
-
removeStorage(getStorageKey("tenant_id"));
|
|
2993
|
-
if (typeof window !== "undefined") {
|
|
2994
|
-
window.localStorage.removeItem(getStorageKey("force_login"));
|
|
2995
|
-
window.localStorage.removeItem(getStorageKey("logout_in_progress"));
|
|
2996
|
-
}
|
|
2997
2829
|
setStorage(getStorageKey("props_changed"), "true");
|
|
2998
2830
|
setStorage(getStorageKey("tenant_switched"), "true");
|
|
2999
2831
|
setStorage(getStorageKey("new_tenant_key"), resolvedTenantKey2);
|
|
@@ -3006,7 +2838,17 @@ function AuthProvider({
|
|
|
3006
2838
|
} else if (previousTenantKey) {
|
|
3007
2839
|
logger.debug("[AuthProvider] TenantKey unchanged", { tenantKey: resolvedTenantKey2 });
|
|
3008
2840
|
}
|
|
3009
|
-
|
|
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
|
+
}
|
|
3010
2852
|
setStorage(getStorageKey("tenant_key"), resolvedTenantKey2);
|
|
3011
2853
|
} else {
|
|
3012
2854
|
logger.debug("[AuthProvider] No tenant code determined");
|
|
@@ -3205,17 +3047,29 @@ function AuthCallback({
|
|
|
3205
3047
|
setStatus("loading");
|
|
3206
3048
|
setMessage("Completing sign-in...");
|
|
3207
3049
|
const handled = await handleCallback();
|
|
3208
|
-
if (handled) {
|
|
3209
|
-
const token = getAccessToken();
|
|
3210
|
-
if (token) setTokens(token);
|
|
3211
|
-
setStatus("success");
|
|
3212
|
-
setMessage("Authentication successful. Redirecting...");
|
|
3213
|
-
window.location.replace(successPath);
|
|
3214
|
-
} else {
|
|
3050
|
+
if (!handled) {
|
|
3215
3051
|
setStatus("error");
|
|
3216
3052
|
setMessage("Authentication failed. Please try again.");
|
|
3217
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));
|
|
3218
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);
|
|
3219
3073
|
};
|
|
3220
3074
|
runCallback();
|
|
3221
3075
|
}, [homePath, loginPath, successPath, setTokens]);
|
|
@@ -3417,6 +3271,7 @@ exports.enforceHttps = enforceHttps;
|
|
|
3417
3271
|
exports.ensureValidAccessToken = ensureValidAccessToken;
|
|
3418
3272
|
exports.extractTenantKey = extractTenantKey;
|
|
3419
3273
|
exports.getAccessToken = getAccessToken2;
|
|
3274
|
+
exports.getAuthConfig = getAuthConfig;
|
|
3420
3275
|
exports.getAuthRuntime = getAuthRuntime;
|
|
3421
3276
|
exports.getConfig = getConfig;
|
|
3422
3277
|
exports.getStorageAdapter = getStorageAdapter;
|