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 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]);
@@ -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
- async function callServerLogout(logoutUrl) {
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: "POST",
2546
- headers: {
2547
- "Content-Type": "application/x-www-form-urlencoded",
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("Server logout response (POST)", { response: data });
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 (POST)", { response: data });
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.debug("POST logout failed, trying GET", {
2667
+ logger.warn("Server logout failed (both POST and GET)", {
2562
2668
  error: error instanceof Error ? error.message : String(error)
2563
2669
  });
2564
- try {
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
- 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
+ }
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()) setStorage(getStorageKey("preserved_tenant_key"), tenantKey.trim());
2606
- if (getStorage(getStorageKey("logout_processing")) === "true") {
2607
- 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()) {
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
- 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));
2628
2762
  removeStorage(getStorageKey("preserved_tenant_key"));
2629
2763
  }
2630
2764
  resetCookieWatcher();
2631
- removeStorage(getStorageKey("logout_processing"));
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
- 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));
2652
2803
  removeStorage(getStorageKey("preserved_tenant_key"));
2653
2804
  }
2654
- removeStorage(getStorageKey("logout_processing"));
2805
+ releaseLogoutLock();
2655
2806
  return {
2656
2807
  success: true,
2657
2808
  message: "Logged out locally due to an error. Fresh login required."