tenneo-auth-plugin 0.1.7 → 0.1.8
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 +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +198 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +198 -47
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -8,12 +8,22 @@ declare function bootstrap(): Promise<void>;
|
|
|
8
8
|
/**
|
|
9
9
|
* Logout functionality.
|
|
10
10
|
* Sets force_login, clears auth storage; does not redirect (bootstrap handles that).
|
|
11
|
+
*
|
|
12
|
+
* This module is hardened for:
|
|
13
|
+
* - Multi-tab race safety (timestamp-based lock)
|
|
14
|
+
* - Proper server logout validation
|
|
15
|
+
* - Scoped auth storage cleanup
|
|
16
|
+
* - Watcher lifecycle correctness
|
|
17
|
+
* - CSRF protection (when host app provides a token)
|
|
11
18
|
*/
|
|
12
19
|
interface LogoutResponse {
|
|
13
20
|
success?: boolean;
|
|
14
21
|
message?: string;
|
|
15
22
|
logout?: boolean;
|
|
16
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Public logout API – keeps signature/backward-compatibility.
|
|
26
|
+
*/
|
|
17
27
|
declare function logout(tenantKey?: string): Promise<{
|
|
18
28
|
success: boolean;
|
|
19
29
|
message: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -8,12 +8,22 @@ declare function bootstrap(): Promise<void>;
|
|
|
8
8
|
/**
|
|
9
9
|
* Logout functionality.
|
|
10
10
|
* Sets force_login, clears auth storage; does not redirect (bootstrap handles that).
|
|
11
|
+
*
|
|
12
|
+
* This module is hardened for:
|
|
13
|
+
* - Multi-tab race safety (timestamp-based lock)
|
|
14
|
+
* - Proper server logout validation
|
|
15
|
+
* - Scoped auth storage cleanup
|
|
16
|
+
* - Watcher lifecycle correctness
|
|
17
|
+
* - CSRF protection (when host app provides a token)
|
|
11
18
|
*/
|
|
12
19
|
interface LogoutResponse {
|
|
13
20
|
success?: boolean;
|
|
14
21
|
message?: string;
|
|
15
22
|
logout?: boolean;
|
|
16
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Public logout API – keeps signature/backward-compatibility.
|
|
26
|
+
*/
|
|
17
27
|
declare function logout(tenantKey?: string): Promise<{
|
|
18
28
|
success: boolean;
|
|
19
29
|
message: string;
|
package/dist/index.js
CHANGED
|
@@ -918,10 +918,10 @@ var DEFAULT_REFRESH_GRANT_TYPE = OAUTH_CONSTANTS.REFRESH_GRANT_TYPE;
|
|
|
918
918
|
|
|
919
919
|
// src/config/index.ts
|
|
920
920
|
var INTERNAL_DEFAULTS = {
|
|
921
|
-
API_TENANT_RESOLUTION_BASE_URL: "
|
|
922
|
-
API_AUTH_BASE_URL: "
|
|
923
|
-
AUTH_SERVER_URL: "
|
|
924
|
-
DEFAULT_DOMAIN: "
|
|
921
|
+
API_TENANT_RESOLUTION_BASE_URL: "http://localhost:5021/api/v1",
|
|
922
|
+
API_AUTH_BASE_URL: "http://localhost:5000",
|
|
923
|
+
AUTH_SERVER_URL: "http://localhost:5000",
|
|
924
|
+
DEFAULT_DOMAIN: "http://localhost:5000"
|
|
925
925
|
};
|
|
926
926
|
function envOr(key) {
|
|
927
927
|
const v = getEnvValue([`VITE_${key}`, key]);
|
|
@@ -2489,7 +2489,74 @@ function resetCookieWatcher() {
|
|
|
2489
2489
|
authCookies
|
|
2490
2490
|
});
|
|
2491
2491
|
}
|
|
2492
|
+
|
|
2493
|
+
// src/infrastructure/watchers/storage-watcher.ts
|
|
2494
|
+
var isWatching2 = false;
|
|
2495
|
+
var lastStorageCheck = null;
|
|
2496
|
+
var storageEventListener = null;
|
|
2497
|
+
function checkStorageCleared() {
|
|
2498
|
+
try {
|
|
2499
|
+
const hasAnyAuthData = Object.values(STORAGE_KEYS).some(
|
|
2500
|
+
(key) => getStorage(key) !== null
|
|
2501
|
+
);
|
|
2502
|
+
if (lastStorageCheck === "had_data" && !hasAnyAuthData) return true;
|
|
2503
|
+
lastStorageCheck = hasAnyAuthData ? "had_data" : "no_data";
|
|
2504
|
+
return false;
|
|
2505
|
+
} catch {
|
|
2506
|
+
return false;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
function handleStorageEvent(event) {
|
|
2510
|
+
if (typeof window !== "undefined" && event.storageArea === window.sessionStorage) {
|
|
2511
|
+
const wasCleared = checkStorageCleared();
|
|
2512
|
+
if (wasCleared) {
|
|
2513
|
+
logger.info("Detected manual sessionStorage clear! Redirecting to login...");
|
|
2514
|
+
const cb = getSessionClearedCallback();
|
|
2515
|
+
if (cb) {
|
|
2516
|
+
Promise.resolve(cb()).catch((error) => {
|
|
2517
|
+
logger.error("Failed to redirect to login after manual clear", {
|
|
2518
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2519
|
+
});
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
function startStorageWatcher() {
|
|
2526
|
+
if (isWatching2) return;
|
|
2527
|
+
isWatching2 = true;
|
|
2528
|
+
checkStorageCleared();
|
|
2529
|
+
storageEventListener = handleStorageEvent;
|
|
2530
|
+
window.addEventListener("storage", storageEventListener);
|
|
2531
|
+
const pollInterval = setInterval(() => {
|
|
2532
|
+
const logoutInProgress = typeof window !== "undefined" && window.localStorage.getItem(getStorageKey("logout_in_progress")) === "true";
|
|
2533
|
+
if (logoutInProgress) {
|
|
2534
|
+
logger.debug("Logout detected. Storage watcher will not trigger bootstrap.");
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
const wasCleared = checkStorageCleared();
|
|
2538
|
+
if (wasCleared) {
|
|
2539
|
+
logger.info("Detected manual sessionStorage clear (polling)! Redirecting to login...");
|
|
2540
|
+
clearInterval(pollInterval);
|
|
2541
|
+
const cb = getSessionClearedCallback();
|
|
2542
|
+
if (cb) {
|
|
2543
|
+
Promise.resolve(cb()).catch((error) => {
|
|
2544
|
+
logger.error("Failed to redirect to login after manual clear", {
|
|
2545
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2546
|
+
});
|
|
2547
|
+
if (!isWatching2) startStorageWatcher();
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}, 1e3);
|
|
2552
|
+
window.__tenneo_auth_storage_watcher_interval = pollInterval;
|
|
2553
|
+
}
|
|
2492
2554
|
function stopStorageWatcher() {
|
|
2555
|
+
isWatching2 = false;
|
|
2556
|
+
if (storageEventListener) {
|
|
2557
|
+
window.removeEventListener("storage", storageEventListener);
|
|
2558
|
+
storageEventListener = null;
|
|
2559
|
+
}
|
|
2493
2560
|
const win = window;
|
|
2494
2561
|
const intervalId = win.__tenneo_auth_storage_watcher_interval;
|
|
2495
2562
|
if (intervalId) {
|
|
@@ -2539,51 +2606,68 @@ function buildUrl(baseUrl, path) {
|
|
|
2539
2606
|
url.pathname = url.pathname.endsWith("/") ? `${url.pathname}${normalizedPath.slice(1)}` : `${url.pathname}${normalizedPath}`;
|
|
2540
2607
|
return url.toString();
|
|
2541
2608
|
}
|
|
2542
|
-
|
|
2609
|
+
function getCsrfToken() {
|
|
2543
2610
|
try {
|
|
2611
|
+
if (typeof document === "undefined") return null;
|
|
2612
|
+
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
2613
|
+
if (meta?.content) return meta.content;
|
|
2614
|
+
const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/i);
|
|
2615
|
+
if (match) return decodeURIComponent(match[1]);
|
|
2616
|
+
return null;
|
|
2617
|
+
} catch {
|
|
2618
|
+
return null;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
async function callServerLogout(logoutUrl) {
|
|
2622
|
+
const csrfToken = getCsrfToken();
|
|
2623
|
+
async function doRequest(method) {
|
|
2624
|
+
const isPost = method === "POST";
|
|
2625
|
+
const headers = {
|
|
2626
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
2627
|
+
Accept: "application/json, */*"
|
|
2628
|
+
};
|
|
2629
|
+
if (isPost) {
|
|
2630
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
2631
|
+
}
|
|
2632
|
+
if (csrfToken) {
|
|
2633
|
+
headers["X-CSRF-Token"] = csrfToken;
|
|
2634
|
+
}
|
|
2544
2635
|
const response = await fetch(logoutUrl, {
|
|
2545
|
-
method
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
2549
|
-
Accept: "application/json"
|
|
2550
|
-
},
|
|
2636
|
+
method,
|
|
2637
|
+
mode: "cors",
|
|
2638
|
+
headers,
|
|
2551
2639
|
credentials: "include"
|
|
2552
2640
|
});
|
|
2641
|
+
if (!response.ok) {
|
|
2642
|
+
logger.warn("Server logout HTTP error", {
|
|
2643
|
+
method,
|
|
2644
|
+
status: response.status,
|
|
2645
|
+
statusText: response.statusText
|
|
2646
|
+
});
|
|
2647
|
+
return { success: false };
|
|
2648
|
+
}
|
|
2553
2649
|
const data = await response.json().catch(() => ({}));
|
|
2554
|
-
logger.info(
|
|
2650
|
+
logger.info(`Server logout response (${method})`, { response: data });
|
|
2555
2651
|
if (data && (data.success === true || data.logout === true)) {
|
|
2556
2652
|
return { success: true, response: data };
|
|
2557
2653
|
}
|
|
2558
|
-
logger.warn("Server logout response invalid
|
|
2654
|
+
logger.warn("Server logout response invalid payload", {
|
|
2655
|
+
method,
|
|
2656
|
+
response: data
|
|
2657
|
+
});
|
|
2559
2658
|
return { success: false, response: data };
|
|
2659
|
+
}
|
|
2660
|
+
try {
|
|
2661
|
+
const postResult = await doRequest("POST");
|
|
2662
|
+
if (postResult.success) return postResult;
|
|
2663
|
+
logger.debug("POST logout did not succeed, trying GET");
|
|
2664
|
+
const getResult = await doRequest("GET");
|
|
2665
|
+
return getResult;
|
|
2560
2666
|
} catch (error) {
|
|
2561
|
-
logger.
|
|
2667
|
+
logger.warn("Server logout failed (both POST and GET)", {
|
|
2562
2668
|
error: error instanceof Error ? error.message : String(error)
|
|
2563
2669
|
});
|
|
2564
|
-
|
|
2565
|
-
const response = await fetch(logoutUrl, {
|
|
2566
|
-
method: "GET",
|
|
2567
|
-
headers: {
|
|
2568
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
2569
|
-
Accept: "application/json"
|
|
2570
|
-
},
|
|
2571
|
-
credentials: "include"
|
|
2572
|
-
});
|
|
2573
|
-
const data = await response.json().catch(() => ({}));
|
|
2574
|
-
logger.info("Server logout response (GET)", { response: data });
|
|
2575
|
-
if (data && (data.success === true || data.logout === true)) {
|
|
2576
|
-
return { success: true, response: data };
|
|
2577
|
-
}
|
|
2578
|
-
logger.warn("Server logout response invalid (GET)", { response: data });
|
|
2579
|
-
return { success: false, response: data };
|
|
2580
|
-
} catch (getError) {
|
|
2581
|
-
logger.warn("Server logout failed (both POST and GET)", {
|
|
2582
|
-
postError: error instanceof Error ? error.message : String(error),
|
|
2583
|
-
getError: getError instanceof Error ? getError.message : String(getError)
|
|
2584
|
-
});
|
|
2585
|
-
return { success: false };
|
|
2586
|
-
}
|
|
2670
|
+
return { success: false };
|
|
2587
2671
|
}
|
|
2588
2672
|
}
|
|
2589
2673
|
function performLocalLogoutCleanup() {
|
|
@@ -2596,18 +2680,46 @@ function performLocalLogoutCleanup() {
|
|
|
2596
2680
|
});
|
|
2597
2681
|
}
|
|
2598
2682
|
clearClientCookies();
|
|
2599
|
-
|
|
2683
|
+
try {
|
|
2684
|
+
clearAuthStorage();
|
|
2685
|
+
} catch (error) {
|
|
2686
|
+
logger.warn("clearAuthStorage failed (ignored)", {
|
|
2687
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2600
2690
|
logger.info("Local logout cleanup completed");
|
|
2601
2691
|
}
|
|
2692
|
+
function acquireLogoutLock() {
|
|
2693
|
+
const LOCK_KEY = getStorageKey("logout_lock");
|
|
2694
|
+
const now = Date.now();
|
|
2695
|
+
const existingLock = getStorage(LOCK_KEY);
|
|
2696
|
+
if (existingLock) {
|
|
2697
|
+
const lockTime = Number(existingLock);
|
|
2698
|
+
if (!Number.isNaN(lockTime) && now - lockTime < 5e3) {
|
|
2699
|
+
logger.debug("Logout lock active \u2013 skipping duplicate logout", {
|
|
2700
|
+
now,
|
|
2701
|
+
lockTime
|
|
2702
|
+
});
|
|
2703
|
+
return false;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
setStorage(LOCK_KEY, String(now));
|
|
2707
|
+
return true;
|
|
2708
|
+
}
|
|
2709
|
+
function releaseLogoutLock() {
|
|
2710
|
+
try {
|
|
2711
|
+
removeStorage(getStorageKey("logout_lock"));
|
|
2712
|
+
} catch {
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2602
2715
|
async function logout(tenantKey) {
|
|
2603
|
-
setStorage(getStorageKey("logout_in_progress"), "true");
|
|
2604
2716
|
setStorage(getStorageKey("force_login"), "true");
|
|
2605
|
-
if (tenantKey?.trim())
|
|
2606
|
-
|
|
2607
|
-
|
|
2717
|
+
if (tenantKey?.trim()) {
|
|
2718
|
+
setStorage(getStorageKey("preserved_tenant_key"), tenantKey.trim());
|
|
2719
|
+
}
|
|
2720
|
+
if (!acquireLogoutLock()) {
|
|
2608
2721
|
return { success: true, message: "Logout already in progress." };
|
|
2609
2722
|
}
|
|
2610
|
-
setStorage(getStorageKey("logout_processing"), "true");
|
|
2611
2723
|
try {
|
|
2612
2724
|
stopStorageWatcher();
|
|
2613
2725
|
stopCookieWatcher();
|
|
@@ -2620,20 +2732,45 @@ async function logout(tenantKey) {
|
|
|
2620
2732
|
const logoutResult = await callServerLogout(logoutUrl);
|
|
2621
2733
|
serverLogoutSuccess = logoutResult.success;
|
|
2622
2734
|
serverResponse = logoutResult.response;
|
|
2735
|
+
if (!serverLogoutSuccess) {
|
|
2736
|
+
logger.warn("Server logout failed \u2013 session may still exist", {
|
|
2737
|
+
logoutUrl,
|
|
2738
|
+
serverResponse
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
} else {
|
|
2742
|
+
logger.warn("API auth base URL not configured, performing local logout only");
|
|
2623
2743
|
}
|
|
2624
2744
|
performLocalLogoutCleanup();
|
|
2625
2745
|
const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
|
|
2626
2746
|
if (preservedTenantKey) {
|
|
2627
|
-
|
|
2747
|
+
const propsKey = getStorageKey("props_config");
|
|
2748
|
+
let existing = {};
|
|
2749
|
+
try {
|
|
2750
|
+
const raw = getStorage(propsKey);
|
|
2751
|
+
if (raw) {
|
|
2752
|
+
existing = JSON.parse(raw);
|
|
2753
|
+
}
|
|
2754
|
+
} catch {
|
|
2755
|
+
existing = {};
|
|
2756
|
+
}
|
|
2757
|
+
const merged = {
|
|
2758
|
+
...existing,
|
|
2759
|
+
tenantKey: preservedTenantKey
|
|
2760
|
+
};
|
|
2761
|
+
setStorage(propsKey, JSON.stringify(merged));
|
|
2628
2762
|
removeStorage(getStorageKey("preserved_tenant_key"));
|
|
2629
2763
|
}
|
|
2630
2764
|
resetCookieWatcher();
|
|
2631
|
-
|
|
2765
|
+
startStorageWatcher();
|
|
2766
|
+
releaseLogoutLock();
|
|
2632
2767
|
logger.info("Logout completed \u2013 flags set, storage cleared, fresh login required", {
|
|
2633
2768
|
serverLogoutSuccess,
|
|
2634
2769
|
serverResponse
|
|
2635
2770
|
});
|
|
2636
2771
|
emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
|
|
2772
|
+
serverLogoutSuccess,
|
|
2773
|
+
tenantKey: getStorage(getStorageKey("preserved_tenant_key")) || void 0,
|
|
2637
2774
|
message: serverLogoutSuccess ? "Server logout success" : "Local logout only"
|
|
2638
2775
|
});
|
|
2639
2776
|
return {
|
|
@@ -2648,10 +2785,24 @@ async function logout(tenantKey) {
|
|
|
2648
2785
|
performLocalLogoutCleanup();
|
|
2649
2786
|
const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
|
|
2650
2787
|
if (preservedTenantKey) {
|
|
2651
|
-
|
|
2788
|
+
const propsKey = getStorageKey("props_config");
|
|
2789
|
+
let existing = {};
|
|
2790
|
+
try {
|
|
2791
|
+
const raw = getStorage(propsKey);
|
|
2792
|
+
if (raw) {
|
|
2793
|
+
existing = JSON.parse(raw);
|
|
2794
|
+
}
|
|
2795
|
+
} catch {
|
|
2796
|
+
existing = {};
|
|
2797
|
+
}
|
|
2798
|
+
const merged = {
|
|
2799
|
+
...existing,
|
|
2800
|
+
tenantKey: preservedTenantKey
|
|
2801
|
+
};
|
|
2802
|
+
setStorage(propsKey, JSON.stringify(merged));
|
|
2652
2803
|
removeStorage(getStorageKey("preserved_tenant_key"));
|
|
2653
2804
|
}
|
|
2654
|
-
|
|
2805
|
+
releaseLogoutLock();
|
|
2655
2806
|
return {
|
|
2656
2807
|
success: true,
|
|
2657
2808
|
message: "Logged out locally due to an error. Fresh login required."
|