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 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: "https://localhost:5020/api/v1",
922
- API_AUTH_BASE_URL: "https://localhost:5001",
923
- AUTH_SERVER_URL: "https://localhost:5001",
924
- DEFAULT_DOMAIN: "https://localhost:5001"
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
- logger.debug("No refresh token - cannot refresh; login required");
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
- async function callServerLogout(logoutUrl) {
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: "POST",
2540
- headers: {
2541
- "Content-Type": "application/x-www-form-urlencoded",
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("Server logout response (POST)", { response: data });
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 (POST)", { response: data });
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.debug("POST logout failed, trying GET", {
2667
+ logger.warn("Server logout failed (both POST and GET)", {
2556
2668
  error: error instanceof Error ? error.message : String(error)
2557
2669
  });
2558
- try {
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
- clearAllStorage();
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()) setStorage(getStorageKey("preserved_tenant_key"), tenantKey.trim());
2600
- if (getStorage(getStorageKey("logout_processing")) === "true") {
2601
- logger.debug("Logout already in progress \u2013 skipping duplicate call");
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
- setStorage(getStorageKey("props_config"), JSON.stringify({ tenantKey: preservedTenantKey }));
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
- removeStorage(getStorageKey("logout_processing"));
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
- setStorage(getStorageKey("props_config"), JSON.stringify({ tenantKey: preservedTenantKey }));
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
- removeStorage(getStorageKey("logout_processing"));
2805
+ releaseLogoutLock();
2649
2806
  return {
2650
2807
  success: true,
2651
2808
  message: "Logged out locally due to an error. Fresh login required."