tenneo-auth-plugin 0.1.6 → 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 +205 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +205 -48
- 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]);
|
|
@@ -2005,6 +2005,8 @@ function setDefaultAuthEngine(engine) {
|
|
|
2005
2005
|
}
|
|
2006
2006
|
|
|
2007
2007
|
// src/application/token.ts
|
|
2008
|
+
var lastNoRefreshLog = 0;
|
|
2009
|
+
var NO_REFRESH_LOG_INTERVAL_MS = 5e3;
|
|
2008
2010
|
async function ensureValidAccessToken() {
|
|
2009
2011
|
const engine = getDefaultAuthEngine();
|
|
2010
2012
|
if (engine) {
|
|
@@ -2017,7 +2019,11 @@ async function ensureValidAccessToken() {
|
|
|
2017
2019
|
if (token && !isAccessTokenExpired()) return token;
|
|
2018
2020
|
const refreshToken = getRefreshToken();
|
|
2019
2021
|
if (!refreshToken) {
|
|
2020
|
-
|
|
2022
|
+
const now = Date.now();
|
|
2023
|
+
if (now - lastNoRefreshLog >= NO_REFRESH_LOG_INTERVAL_MS) {
|
|
2024
|
+
lastNoRefreshLog = now;
|
|
2025
|
+
logger.debug("No refresh token - cannot refresh; login required");
|
|
2026
|
+
}
|
|
2021
2027
|
throw new Error("No refresh token; login required");
|
|
2022
2028
|
}
|
|
2023
2029
|
return await refreshAccessToken();
|
|
@@ -2483,7 +2489,74 @@ function resetCookieWatcher() {
|
|
|
2483
2489
|
authCookies
|
|
2484
2490
|
});
|
|
2485
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
|
+
}
|
|
2486
2554
|
function stopStorageWatcher() {
|
|
2555
|
+
isWatching2 = false;
|
|
2556
|
+
if (storageEventListener) {
|
|
2557
|
+
window.removeEventListener("storage", storageEventListener);
|
|
2558
|
+
storageEventListener = null;
|
|
2559
|
+
}
|
|
2487
2560
|
const win = window;
|
|
2488
2561
|
const intervalId = win.__tenneo_auth_storage_watcher_interval;
|
|
2489
2562
|
if (intervalId) {
|
|
@@ -2533,51 +2606,68 @@ function buildUrl(baseUrl, path) {
|
|
|
2533
2606
|
url.pathname = url.pathname.endsWith("/") ? `${url.pathname}${normalizedPath.slice(1)}` : `${url.pathname}${normalizedPath}`;
|
|
2534
2607
|
return url.toString();
|
|
2535
2608
|
}
|
|
2536
|
-
|
|
2609
|
+
function getCsrfToken() {
|
|
2537
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
|
+
}
|
|
2538
2635
|
const response = await fetch(logoutUrl, {
|
|
2539
|
-
method
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
2543
|
-
Accept: "application/json"
|
|
2544
|
-
},
|
|
2636
|
+
method,
|
|
2637
|
+
mode: "cors",
|
|
2638
|
+
headers,
|
|
2545
2639
|
credentials: "include"
|
|
2546
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
|
+
}
|
|
2547
2649
|
const data = await response.json().catch(() => ({}));
|
|
2548
|
-
logger.info(
|
|
2650
|
+
logger.info(`Server logout response (${method})`, { response: data });
|
|
2549
2651
|
if (data && (data.success === true || data.logout === true)) {
|
|
2550
2652
|
return { success: true, response: data };
|
|
2551
2653
|
}
|
|
2552
|
-
logger.warn("Server logout response invalid
|
|
2654
|
+
logger.warn("Server logout response invalid payload", {
|
|
2655
|
+
method,
|
|
2656
|
+
response: data
|
|
2657
|
+
});
|
|
2553
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;
|
|
2554
2666
|
} catch (error) {
|
|
2555
|
-
logger.
|
|
2667
|
+
logger.warn("Server logout failed (both POST and GET)", {
|
|
2556
2668
|
error: error instanceof Error ? error.message : String(error)
|
|
2557
2669
|
});
|
|
2558
|
-
|
|
2559
|
-
const response = await fetch(logoutUrl, {
|
|
2560
|
-
method: "GET",
|
|
2561
|
-
headers: {
|
|
2562
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
2563
|
-
Accept: "application/json"
|
|
2564
|
-
},
|
|
2565
|
-
credentials: "include"
|
|
2566
|
-
});
|
|
2567
|
-
const data = await response.json().catch(() => ({}));
|
|
2568
|
-
logger.info("Server logout response (GET)", { response: data });
|
|
2569
|
-
if (data && (data.success === true || data.logout === true)) {
|
|
2570
|
-
return { success: true, response: data };
|
|
2571
|
-
}
|
|
2572
|
-
logger.warn("Server logout response invalid (GET)", { response: data });
|
|
2573
|
-
return { success: false, response: data };
|
|
2574
|
-
} catch (getError) {
|
|
2575
|
-
logger.warn("Server logout failed (both POST and GET)", {
|
|
2576
|
-
postError: error instanceof Error ? error.message : String(error),
|
|
2577
|
-
getError: getError instanceof Error ? getError.message : String(getError)
|
|
2578
|
-
});
|
|
2579
|
-
return { success: false };
|
|
2580
|
-
}
|
|
2670
|
+
return { success: false };
|
|
2581
2671
|
}
|
|
2582
2672
|
}
|
|
2583
2673
|
function performLocalLogoutCleanup() {
|
|
@@ -2590,18 +2680,46 @@ function performLocalLogoutCleanup() {
|
|
|
2590
2680
|
});
|
|
2591
2681
|
}
|
|
2592
2682
|
clearClientCookies();
|
|
2593
|
-
|
|
2683
|
+
try {
|
|
2684
|
+
clearAuthStorage();
|
|
2685
|
+
} catch (error) {
|
|
2686
|
+
logger.warn("clearAuthStorage failed (ignored)", {
|
|
2687
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2594
2690
|
logger.info("Local logout cleanup completed");
|
|
2595
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
|
+
}
|
|
2596
2715
|
async function logout(tenantKey) {
|
|
2597
|
-
setStorage(getStorageKey("logout_in_progress"), "true");
|
|
2598
2716
|
setStorage(getStorageKey("force_login"), "true");
|
|
2599
|
-
if (tenantKey?.trim())
|
|
2600
|
-
|
|
2601
|
-
|
|
2717
|
+
if (tenantKey?.trim()) {
|
|
2718
|
+
setStorage(getStorageKey("preserved_tenant_key"), tenantKey.trim());
|
|
2719
|
+
}
|
|
2720
|
+
if (!acquireLogoutLock()) {
|
|
2602
2721
|
return { success: true, message: "Logout already in progress." };
|
|
2603
2722
|
}
|
|
2604
|
-
setStorage(getStorageKey("logout_processing"), "true");
|
|
2605
2723
|
try {
|
|
2606
2724
|
stopStorageWatcher();
|
|
2607
2725
|
stopCookieWatcher();
|
|
@@ -2614,20 +2732,45 @@ async function logout(tenantKey) {
|
|
|
2614
2732
|
const logoutResult = await callServerLogout(logoutUrl);
|
|
2615
2733
|
serverLogoutSuccess = logoutResult.success;
|
|
2616
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");
|
|
2617
2743
|
}
|
|
2618
2744
|
performLocalLogoutCleanup();
|
|
2619
2745
|
const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
|
|
2620
2746
|
if (preservedTenantKey) {
|
|
2621
|
-
|
|
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));
|
|
2622
2762
|
removeStorage(getStorageKey("preserved_tenant_key"));
|
|
2623
2763
|
}
|
|
2624
2764
|
resetCookieWatcher();
|
|
2625
|
-
|
|
2765
|
+
startStorageWatcher();
|
|
2766
|
+
releaseLogoutLock();
|
|
2626
2767
|
logger.info("Logout completed \u2013 flags set, storage cleared, fresh login required", {
|
|
2627
2768
|
serverLogoutSuccess,
|
|
2628
2769
|
serverResponse
|
|
2629
2770
|
});
|
|
2630
2771
|
emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
|
|
2772
|
+
serverLogoutSuccess,
|
|
2773
|
+
tenantKey: getStorage(getStorageKey("preserved_tenant_key")) || void 0,
|
|
2631
2774
|
message: serverLogoutSuccess ? "Server logout success" : "Local logout only"
|
|
2632
2775
|
});
|
|
2633
2776
|
return {
|
|
@@ -2642,10 +2785,24 @@ async function logout(tenantKey) {
|
|
|
2642
2785
|
performLocalLogoutCleanup();
|
|
2643
2786
|
const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
|
|
2644
2787
|
if (preservedTenantKey) {
|
|
2645
|
-
|
|
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));
|
|
2646
2803
|
removeStorage(getStorageKey("preserved_tenant_key"));
|
|
2647
2804
|
}
|
|
2648
|
-
|
|
2805
|
+
releaseLogoutLock();
|
|
2649
2806
|
return {
|
|
2650
2807
|
success: true,
|
|
2651
2808
|
message: "Logged out locally due to an error. Fresh login required."
|