1import { events } from '@dropins/tools/event-bus.js'; 2 3import { getCookie, isAccountRedirectEnabled } from '../configs.js'; 4import { ACCOUNT_REGISTRATION_COMPLETE_KEY, REVEAL_PROJECTS_CACHE_KEY } from '../constants.js'; 5import buildSsoLoginUrl from './sso-config.js'; 6 7const SSO_COOKIE_NAMES = ['sso_id_token', 'sso_refresh_token']; 8const DROPIN_AUTH_COOKIE_NAMES = ['auth_dropin_user_token', 'auth_dropin_firstname']; 9const ALL_AUTH_COOKIE_NAMES = [...DROPIN_AUTH_COOKIE_NAMES, ...SSO_COOKIE_NAMES]; 10 11// Cart state the Cart drop-in persists per session (the nav cart icon reads CART__DATA 12// to decide badged-vs-plain). Cleared on logout too, so a logged-out tab doesn't keep 13// showing the previous customer's cart count until the drop-in next reloads. 14const CART_SESSION_KEYS = ['DROPIN__CART__CART__DATA', 'DROPIN__CART__SHIPPING__DATA', 'DROPINS_CART_ID', 'nav-cart-count']; 15 16// The Cart drop-in keys guest-vs-customer solely on this cookie. Clear it on auth 17// invalidation too, so a logged-out (or expired) tab doesn't keep mutating the previous 18// session's cart id; the next login re-seeds the customer cart (see initializers/cart.js). 19const CART_COOKIE_NAMES = ['DROPIN__CART__CART-ID']; 20 21// Cookies are now set host-only (see scripts/auth/sso-callback.js). We still delete the 22// Domain-scoped variant alongside the host-only one to clean up any pre-existing 23// domain cookies from older sessions before this fix landed â a host-only delete only 24// removes the host-only variant, leaving a domain-scoped duplicate behind. 25function deleteCookies(names) { 26 const domainAttr = window.location.hostname !== 'localhost' 27 ? ` domain=${window.location.hostname};` 28 : ''; 29 names.forEach((name) => { 30 document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; 31 if (domainAttr) { 32 document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;${domainAttr}`; 33 } 34 }); 35} 36 37// `atob` returns a binary string where each character is a single byte (Latin-1 38// interpretation). JWT payloads are UTF-8, so multi-byte sequences (e.g. "José", 39// "Müller") would render garbled if we returned the binary string directly. Route 40// through Uint8Array â TextDecoder('utf-8') so non-ASCII claims decode correctly. 41function base64UrlDecode(input) { 42 const normalized = input.replace(/-/g, '+').replace(/_/g, '/'); 43 const padding = (4 - (normalized.length % 4)) % 4; 44 const binary = atob(normalized + '='.repeat(padding)); 45 const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0)); 46 return new TextDecoder('utf-8').decode(bytes); 47} 48 49// Tri-state cache: 50// undefined = not yet decoded this page load 51// null = decoded but no valid token / claim 52// object = parsed payload / customer data 53let payloadCache; 54let customerCache; 55
56function invalidateCache() { 57 payloadCache = undefined; 58 customerCache = undefined; 59 // Cleared on every auth-invalidation path so an expired/cleared session can't 60 // leave stale per-user data for the next user in this tab â the "registration 61 // complete" flag and the cached Reveal projects list. 62 try { sessionStorage.removeItem(ACCOUNT_REGISTRATION_COMPLETE_KEY); } catch (e) { /* ignore */ } 63 try { sessionStorage.removeItem(REVEAL_PROJECTS_CACHE_KEY); } catch (e) { /* ignore */ } 64 CART_SESSION_KEYS.forEach((key) => { 65 try { sessionStorage.removeItem(key); } catch (e) { /* ignore */ } 66 }); 67 deleteCookies(CART_COOKIE_NAMES); 68} 69 70/** 71 * Clear all four auth cookies (dropin tokens + SSO tokens) and invalidate the decode 72 * cache. Shared by the session-invalidation listener and the post-logout return handler 73 * (scripts/auth/sso-logout.js) so they use one cookie list + delete pattern. 74 */ 75export function clearAuthCookies() { 76 deleteCookies(ALL_AUTH_COOKIE_NAMES); 77 invalidateCache(); 78} 79 80/** True when any auth cookie (drop-in token or SSO token) is still present. */ 81function hasAnyAuthCookie() { 82 return ALL_AUTH_COOKIE_NAMES.some((name) => !!getCookie(name)); 83} 84 85function decodeSsoIdToken() { 86 if (payloadCache !== undefined) return payloadCache; 87 const token = getCookie('sso_id_token'); 88 if (!token) { payloadCache = null; return null; } 89 const parts = token.split('.'); 90 if (parts.length !== 3) { payloadCache = null; return null; } 91 try { 92 payloadCache = JSON.parse(base64UrlDecode(parts[1])); 93 } catch { 94 payloadCache = null; 95 } 96 return payloadCache; 97} 98 99/** 100 * Pre-gate validity check: if `sso_id_token` is a JWT whose `exp` claim has passed, 101 * clear all auth cookies so the auth gate downstream treats the user as unauthed 102 * (and redirects to SSO on /account/*). Silently no-ops if the cookie is missing, 103 * not a JWT, or has no `exp` â the post-render verifyToken pipeline still catches 104 * revocations. 105 */ 106export function handleExpiredSsoToken() { 107 const payload = decodeSsoIdToken(); 108 if (!payload) return; 109 if (typeof payload.exp !== 'number') return; 110 if (payload.exp * 1000 > Date.now()) return; 111 deleteCookies(ALL_AUTH_COOKIE_NAMES); 112 invalidateCache(); 113} 114 115/** 116 * Return `{ firstName, lastName }` extracted from the `sso_id_token` JWT for display 117 * purposes. Returns `null` if there's no token, it isn't a JWT, or neither name claim 118 * is present. Memoized for the page load (cookies don't change without a navigation; 119 * the cache is invalidated when this module clears cookies itself). 120 * 121 * SECURITY: the JWT signature is NOT verified client-side. Use ONLY for cosmetic 122 * display (greetings, labels). Never use these claims for authorization decisions â 123 * those must go through server-validated GraphQL via the Bearer token. 124 * 125 * @returns {{firstName: string|null, lastName: string|null} | null} 126 */ 127export function getSsoCustomer() { 128 if (customerCache !== undefined) return customerCache; 129 const payload = decodeSsoIdToken(); 130 if (!payload) { customerCache = null; return null; } 131 const firstName = payload.given_name || null; 132 const lastName = payload.family_name || null; 133 if (!firstName && !lastName) { customerCache = null; return null; } 134 customerCache = { firstName, lastName }; 135 return customerCache; 136} 137 138/** 139 * Return `{ userId, email, accountType, primaryPhone, companyName }` extracted from 140 * the `sso_id_token` JWT. `userId` is the stable subject id (`oid`, falling back to 141 * `sub`). Returns `null` if there's no token, it isn't a JWT, or all claims are missing. 142 * 143 * SECURITY: same caveat as `getSsoCustomer()` â signature is NOT verified 144 * client-side. Cosmetic display / analytics only, never authorization. 145 * 146 * @returns {{userId: string|null, email: string|null, accountType: string|null, primaryPhone: string|null, companyName: string|null} | null} 147 */ 148export function getSsoClaims() { 149 const payload = decodeSsoIdToken(); 150 if (!payload) return null; 151 152 const userId = payload.oid || payload.sub || null; 153 const email = payload.email || null; 154 const accountType = payload.extension_accountType || null; 155 const primaryPhone = payload.mobile || null; 156 const companyName = payload.extension_companyName || null; 157 if (!userId && !email && !accountType && !primaryPhone && !companyName) return null; 158 return { 159 userId, email, accountType, primaryPhone, companyName, 160 }; 161} 162 163/** @deprecated use `getSsoClaims()` â kept for existing importers */ 164export const getSsoEmailAndAccountType = getSsoClaims; 165 166let attached = false;
167 168/** 169 * Subscribe once to the dropin's `authenticated` event so a revoked or expired session is 170 * fully torn down. When verifyToken detects an invalid token it clears 171 * `auth_dropin_user_token` (host-only) and emits `false`; we clear all four auth cookies 172 * (dropin tokens + SSO tokens) via the host-only + Domain-scoped dual delete so legacy 173 * domain-scoped cookies the dropin's delete couldn't reach also go. Without this, 174 * `checkIsAuthenticated()` could keep returning `true` after invalidation and the user 175 * would get stuck. If the user is on an /account/* page, bounce them back through SSO. 176 * 177 * Guarded on a session having existed: verifyToken emits `authenticated: false` on every 178 * page load when no `auth_dropin_user_token` is present â not only on revocation â so 179 * anonymous visitors reach this listener too. Unguarded, the teardown wiped 180 * `DROPIN__CART__CART-ID` on each guest page load and the Cart drop-in then mounted with 181 * no cart, so the accessories cart rendered empty right after an add (AB#259775). Guests 182 * on /account/* are already bounced by the auth gate in loadEager, so no redirect is lost. 183 * 184 * Prerender-safe: if the event fires during a Chrome speculation-rule prerender 185 * (e.g. when verifyToken emits `false` from the ghost tab because no cookie is 186 * present), the cleanup + redirect is deferred to `prerenderingchange` so we 187 * don't race the user's real navigation from the ghost context. Mirrors the 188 * `!document.prerendering` guard in the auth gate (scripts/scripts.js). 189 */ 190export function setupSessionInvalidationListener() { 191 if (attached) return; 192 attached = true; 193 // Sampled before the dropin's first verifyToken (this runs earlier in initializeDropins), 194 // then latched by any later sign-in â the dropin emits `authenticated: true` once the 195 // token cookie is written â so a subsequent invalidation still tears the session down. 196 let hadAuthSession = hasAnyAuthCookie(); 197 events.on('authenticated', (state) => { 198 if (state) { 199 hadAuthSession = true; 200 return; 201 } 202 // Cookie re-check is a fail-safe, not a live path: every cookie writer today runs before 203 // this listener attaches. If one ever lands after, err towards clearing a real session 204 // rather than stranding the user logged-in against a token the server has rejected. 205 if (!hadAuthSession && !hasAnyAuthCookie()) return; 206 hadAuthSession = false; 207 const handle = () => { 208 clearAuthCookies(); 209 // Honours the same `sso.account-redirect-enabled` kill-switch as the loadEager gate, 210 // otherwise an invalidated session would still bounce to SSO with the flag off. 211 if (window.location.pathname.startsWith('/account') && isAccountRedirectEnabled()) { 212 window.location.replace(buildSsoLoginUrl()); 213 } 214 }; 215 if (document.prerendering) { 216 document.addEventListener('prerenderingchange', handle, { once: true }); 217 } else { 218 handle(); 219 } 220 }); 221}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.