@spidy092/auth-client 3.0.0 → 3.0.1
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/api.cjs +45 -37
- package/dist/api.cjs.map +1 -1
- package/dist/api.js +45 -37
- package/dist/api.js.map +1 -1
- package/dist/index.cjs +73 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +73 -55
- package/dist/index.js.map +1 -1
- package/dist/react/AuthProvider.cjs +15 -7
- package/dist/react/AuthProvider.cjs.map +1 -1
- package/dist/react/AuthProvider.js +15 -7
- package/dist/react/AuthProvider.js.map +1 -1
- package/dist/react/useAuth.cjs +9 -3
- package/dist/react/useAuth.cjs.map +1 -1
- package/dist/react/useAuth.js +9 -3
- package/dist/react/useAuth.js.map +1 -1
- package/dist/react/useSessionMonitor.cjs +73 -55
- package/dist/react/useSessionMonitor.cjs.map +1 -1
- package/dist/react/useSessionMonitor.js +73 -55
- package/dist/react/useSessionMonitor.js.map +1 -1
- package/dist/utils/jwt.cjs.map +1 -1
- package/dist/utils/jwt.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../react/useAuth.js","../../react/AuthProvider.jsx","../../token.js","../../config.js"],"sourcesContent":["import { useContext } from 'react';\nimport { AuthContext } from './AuthProvider';\n\nexport function useAuth() {\n const context = useContext(AuthContext);\n if (!context) {\n throw new Error('useAuth must be used within an AuthProvider');\n }\n return context;\n}","// auth-client/react/AuthProvider.jsx\nimport React, { createContext, useState, useEffect, useRef } from 'react';\nimport { getToken, setToken, clearToken } from '../token';\nimport { getConfig } from '../config';\nimport { \n login as coreLogin, \n logout as coreLogout,\n startSessionSecurity,\n stopSessionSecurity,\n onSessionInvalid\n} from '../core';\n\nexport const AuthContext = createContext();\n\nexport function AuthProvider({ children, onSessionExpired }) {\n const [token, setTokenState] = useState(getToken());\n const [user, setUser] = useState(null);\n const [loading, setLoading] = useState(!!token); // Loading if we have a token to validate\n const [sessionValid, setSessionValid] = useState(true);\n const sessionSecurityRef = useRef(null);\n\n // Handle session invalidation (from Keycloak admin deletion or expiry)\n const handleSessionInvalid = (reason) => {\n console.log('🚨 AuthProvider: Session invalidated -', reason);\n setSessionValid(false);\n setUser(null);\n setTokenState(null);\n \n // Call custom callback if provided\n if (onSessionExpired && typeof onSessionExpired === 'function') {\n onSessionExpired(reason);\n }\n };\n\n // Start session security on mount (when we have a token)\n useEffect(() => {\n if (token && !sessionSecurityRef.current) {\n console.log('🔐 AuthProvider: Starting session security');\n \n // Register session invalid handler\n const unsubscribe = onSessionInvalid(handleSessionInvalid);\n \n // Start proactive refresh + session monitoring\n sessionSecurityRef.current = startSessionSecurity(handleSessionInvalid);\n \n return () => {\n unsubscribe();\n if (sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n };\n }\n \n // Cleanup when token is removed\n if (!token && sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n }, [token]);\n\n useEffect(() => {\n console.log('🔍 AuthProvider useEffect triggered:', { \n hasToken: !!token, \n tokenLength: token?.length \n });\n \n if (!token) {\n console.log('⚠️ AuthProvider: No token, setting loading=false');\n setLoading(false);\n return;\n }\n \n const { authBaseUrl } = getConfig();\n if (!authBaseUrl) {\n console.warn('AuthProvider: No authBaseUrl configured');\n setLoading(false);\n return;\n }\n\n console.log('🌐 AuthProvider: Fetching profile with token...', {\n authBaseUrl,\n tokenPreview: token.slice(0, 50) + '...'\n });\n\n fetch(`${authBaseUrl}/account/profile`, {\n headers: { Authorization: `Bearer ${token}` },\n credentials: 'include',\n })\n .then(res => {\n console.log('📥 Profile response status:', res.status);\n if (!res.ok) throw new Error('Failed to fetch user');\n return res.json();\n })\n .then(userData => {\n console.log('✅ Profile fetched successfully:', userData.email);\n setUser(userData);\n setSessionValid(true);\n setLoading(false);\n })\n .catch(err => {\n console.error('❌ Fetch user error:', err);\n clearToken();\n setTokenState(null);\n setUser(null);\n setLoading(false);\n });\n }, [token]);\n\n const login = (clientKey, redirectUri, state) => {\n coreLogin(clientKey, redirectUri, state);\n };\n\n const logout = () => {\n // Stop session security before logout\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n \n coreLogout();\n setUser(null);\n setTokenState(null);\n setSessionValid(true);\n };\n\n const value = {\n token,\n user,\n loading,\n login,\n logout,\n isAuthenticated: !!token && !!user && sessionValid,\n sessionValid,\n setUser,\n setToken: (newToken) => {\n setToken(newToken);\n setTokenState(newToken);\n setSessionValid(true);\n },\n clearToken: () => {\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n clearToken();\n setTokenState(null);\n setUser(null);\n },\n };\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n","// auth-client/token.js - MINIMAL WORKING VERSION\n\nimport { jwtDecode } from 'jwt-decode';\n\nlet accessToken = null;\nconst listeners = new Set();\n\nconst REFRESH_COOKIE = 'account_refresh_token';\nconst COOKIE_MAX_AGE = 7 * 24 * 60 * 60;\n\nfunction secureAttribute() {\n try {\n return typeof window !== 'undefined' && window.location?.protocol === 'https:'\n ? '; Secure'\n : '';\n } catch (err) {\n return '';\n }\n}\n\n// ========== ACCESS TOKEN ==========\nfunction writeAccessToken(token) {\n if (!token) {\n try {\n localStorage.removeItem('authToken');\n } catch (err) {\n console.warn('Could not clear token from localStorage:', err);\n }\n return;\n }\n\n try {\n localStorage.setItem('authToken', token);\n } catch (err) {\n console.warn('Could not persist token to localStorage:', err);\n }\n}\n\nfunction readAccessToken() {\n try {\n return localStorage.getItem('authToken');\n } catch (err) {\n console.warn('Could not read token from localStorage:', err);\n return null;\n }\n}\n\n// ========== REFRESH TOKEN (KEEP SIMPLE) ==========\n// export function setRefreshToken(token) {\n// if (!token) {\n// clearRefreshToken();\n// return;\n// }\n\n// const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);\n\n// try {\n// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax${secureAttribute()}; Expires=${expires.toUTCString()}`;\n// } catch (err) {\n// console.warn('Could not set refresh token:', err);\n// }\n// }\n\n// export function getRefreshToken() {\n// try {\n// const match = document.cookie\n// ?.split('; ')\n// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));\n\n// if (match) {\n// return decodeURIComponent(match.split('=')[1]);\n// }\n// } catch (err) {\n// console.warn('Could not read refresh token:', err);\n// }\n\n// return null;\n// }\n\n// export function clearRefreshToken() {\n// try {\n// document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Lax${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n// } catch (err) {\n// console.warn('Could not clear refresh token:', err);\n// }\n// }\n\n// ========== ACCESS TOKEN FUNCTIONS ==========\nfunction decode(token) {\n try {\n return jwtDecode(token);\n } catch (err) {\n return null;\n }\n}\n\nfunction isExpired(token, bufferSeconds = 60) {\n if (!token) return true;\n const decoded = decode(token);\n if (!decoded?.exp) return true;\n const now = Date.now() / 1000;\n return decoded.exp < now + bufferSeconds;\n}\n\n// ========== TOKEN EXPIRY UTILITIES ==========\n// Get the exact expiry time of a token as a Date object\nexport function getTokenExpiryTime(token) {\n if (!token) return null;\n const decoded = decode(token);\n if (!decoded?.exp) return null;\n return new Date(decoded.exp * 1000);\n}\n\n// Get seconds until token expires (negative if already expired)\nexport function getTimeUntilExpiry(token) {\n if (!token) return -1;\n const decoded = decode(token);\n if (!decoded?.exp) return -1;\n const now = Date.now() / 1000;\n return Math.floor(decoded.exp - now);\n}\n\n// Check if token will expire within the next N seconds\nexport function willExpireSoon(token, withinSeconds = 60) {\n const timeLeft = getTimeUntilExpiry(token);\n return timeLeft >= 0 && timeLeft <= withinSeconds;\n}\n\nexport function setToken(token) {\n const previousToken = accessToken;\n accessToken = token || null;\n writeAccessToken(accessToken);\n\n if (previousToken !== accessToken) {\n listeners.forEach((listener) => {\n try {\n listener(accessToken, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n }\n}\n\nexport function getToken() {\n if (accessToken) return accessToken;\n accessToken = readAccessToken();\n return accessToken;\n}\n\nexport function clearToken() {\n if (!accessToken) {\n writeAccessToken(null);\n clearRefreshToken();\n return;\n }\n\n const previousToken = accessToken;\n accessToken = null;\n writeAccessToken(null);\n clearRefreshToken();\n\n listeners.forEach((listener) => {\n try {\n listener(null, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n}\n\n// ========== REFRESH TOKEN STORAGE FOR HTTP DEVELOPMENT ==========\n// In production (HTTPS), refresh tokens should ONLY be in httpOnly cookies set by server\n// For HTTP development (cross-origin cookies don't work), we store in localStorage\nconst REFRESH_TOKEN_KEY = 'auth_refresh_token';\n\nfunction isHttpDevelopment() {\n try {\n return typeof window !== 'undefined' &&\n window.location?.protocol === 'http:';\n } catch (err) {\n return false;\n }\n}\n\nexport function setRefreshToken(token) {\n if (!token) {\n clearRefreshToken();\n return;\n }\n\n // For HTTP development, store in localStorage (since httpOnly cookies don't work cross-origin)\n if (isHttpDevelopment()) {\n try {\n localStorage.setItem(REFRESH_TOKEN_KEY, token);\n console.log('📦 Refresh token stored in localStorage (HTTP dev mode)');\n } catch (err) {\n console.warn('Could not store refresh token:', err);\n }\n } else {\n // In production (HTTPS), refresh token should be in httpOnly cookie only\n console.log('🔒 Refresh token managed by server httpOnly cookie (production mode)');\n }\n}\n\nexport function getRefreshToken() {\n // For HTTP development, read from localStorage\n if (isHttpDevelopment()) {\n try {\n const token = localStorage.getItem(REFRESH_TOKEN_KEY);\n return token;\n } catch (err) {\n console.warn('Could not read refresh token:', err);\n return null;\n }\n }\n\n // In production, refresh token is in httpOnly cookie (not accessible via JS)\n // The refresh endpoint uses credentials: 'include' to send the cookie\n return null;\n}\n\nexport function clearRefreshToken() {\n // Clear localStorage (for HTTP dev)\n try {\n localStorage.removeItem(REFRESH_TOKEN_KEY);\n } catch (err) {\n // Ignore\n }\n\n // Clear cookie (for production)\n try {\n document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Strict${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n } catch (err) {\n console.warn('Could not clear refresh token cookie:', err);\n }\n\n // Clear sessionStorage\n try {\n sessionStorage.removeItem(REFRESH_COOKIE);\n } catch (err) {\n // Ignore\n }\n}\n\nexport function addTokenListener(listener) {\n if (typeof listener !== 'function') {\n throw new Error('Token listener must be a function');\n }\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nexport function removeTokenListener(listener) {\n listeners.delete(listener);\n}\n\nexport function getListenerCount() {\n return listeners.size;\n}\n\nexport function isAuthenticated() {\n const token = getToken();\n return !!token && !isExpired(token, 15);\n}\n\n\n\n\n// // auth-client/token.js\n// import { jwtDecode } from 'jwt-decode';\n\n// let accessToken = null;\n// const listeners = new Set();\n\n// const REFRESH_COOKIE = 'account_refresh_token';\n// const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days in seconds\n\n// function secureAttribute() {\n// try {\n// return typeof window !== 'undefined' && window.location?.protocol === 'https:'\n// ? '; Secure'\n// : '';\n// } catch (err) {\n// return '';\n// }\n// }\n\n// function writeAccessToken(token) {\n// if (!token) {\n// try {\n// localStorage.removeItem('authToken');\n// } catch (err) {\n// console.warn('Could not clear token from localStorage:', err);\n// }\n// return;\n// }\n\n// try {\n// localStorage.setItem('authToken', token);\n// } catch (err) {\n// console.warn('Could not persist token to localStorage:', err);\n// }\n// }\n\n// function readAccessToken() {\n// try {\n// return localStorage.getItem('authToken');\n// } catch (err) {\n// console.warn('Could not read token from localStorage:', err);\n// return null;\n// }\n// }\n\n// function decode(token) {\n// try {\n// return jwtDecode(token);\n// } catch (err) {\n// return null;\n// }\n// }\n\n// function isExpired(token, bufferSeconds = 60) {\n// if (!token) return true;\n// const decoded = decode(token);\n// if (!decoded?.exp) return true;\n// const now = Date.now() / 1000;\n// return decoded.exp < now + bufferSeconds;\n// }\n\n// export function setToken(token) {\n// const previousToken = accessToken;\n// accessToken = token || null;\n// writeAccessToken(accessToken);\n\n// if (previousToken !== accessToken) {\n// listeners.forEach((listener) => {\n// try {\n// listener(accessToken, previousToken);\n// } catch (err) {\n// console.warn('Token listener error:', err);\n// }\n// });\n// }\n// }\n\n// export function getToken() {\n// if (accessToken) return accessToken;\n// accessToken = readAccessToken();\n// return accessToken;\n// }\n\n// export function clearToken() {\n// if (!accessToken) {\n// writeAccessToken(null);\n// clearRefreshToken();\n// return;\n// }\n\n// const previousToken = accessToken;\n// accessToken = null;\n// writeAccessToken(null);\n// clearRefreshToken();\n\n// listeners.forEach((listener) => {\n// try {\n// listener(null, previousToken);\n// } catch (err) {\n// console.warn('Token listener error:', err);\n// }\n// });\n// }\n\n// export function setRefreshToken(token) {\n// if (!token) {\n// clearRefreshToken();\n// return;\n// }\n\n// const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);\n// try {\n// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict${secureAttribute()}; Expires=${expires.toUTCString()}`;\n// } catch (err) {\n// console.warn('Could not persist refresh token cookie:', err);\n// }\n\n// try {\n// sessionStorage.setItem(REFRESH_COOKIE, token);\n// } catch (err) {\n// console.warn('Could not persist refresh token to sessionStorage:', err);\n// }\n// }\n\n// export function getRefreshToken() {\n// // Prefer cookie to align with server expectations\n// let cookieMatch = null;\n\n// try {\n// cookieMatch = document.cookie\n// ?.split('; ')\n// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));\n// } catch (err) {\n// cookieMatch = null;\n// }\n\n// if (cookieMatch) {\n// return decodeURIComponent(cookieMatch.split('=')[1]);\n// }\n\n// try {\n// return sessionStorage.getItem(REFRESH_COOKIE);\n// } catch (err) {\n// console.warn('Could not read refresh token from sessionStorage:', err);\n// return null;\n// }\n// }\n\n// export function clearRefreshToken() {\n// try {\n// document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Strict${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n// } catch (err) {\n// console.warn('Could not clear refresh token cookie:', err);\n// }\n// try {\n// sessionStorage.removeItem(REFRESH_COOKIE);\n// } catch (err) {\n// console.warn('Could not clear refresh token from sessionStorage:', err);\n// }\n// }\n\n// export function addTokenListener(listener) {\n// if (typeof listener !== 'function') {\n// throw new Error('Token listener must be a function');\n// }\n// listeners.add(listener);\n// return () => {\n// listeners.delete(listener);\n// };\n// }\n\n// export function removeTokenListener(listener) {\n// listeners.delete(listener);\n// }\n\n// export function getListenerCount() {\n// return listeners.size;\n// }\n\n// export function isAuthenticated() {\n// const token = getToken();\n// return !!token && !isExpired(token, 15);\n// }\n\n","// auth-client/config.js\n\n// ========== SESSION SECURITY CONFIGURATION ==========\n// These settings control how the auth-client handles token refresh and session validation\n// to ensure deleted sessions in Keycloak are detected quickly.\n\nlet config = {\n clientKey: null,\n authBaseUrl: null,\n redirectUri: null,\n accountUiUrl: null,\n isRouter: false, // ✅ Add router flag\n\n // ========== SESSION SECURITY SETTINGS ==========\n // Buffer time (in seconds) before token expiry to trigger proactive refresh\n // With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX\n tokenRefreshBuffer: 60,\n\n // Interval (in milliseconds) for periodic session validation\n // Validates that the session still exists in Keycloak (not deleted by admin)\n // Default: 2 minutes (120000ms) - balances responsiveness vs server load\n sessionValidationInterval: 2 * 60 * 1000,\n\n // Enable/disable periodic session validation\n // When enabled, the client will ping the server to verify session is still active\n enableSessionValidation: true,\n\n // Enable/disable proactive token refresh\n // When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)\n enableProactiveRefresh: true,\n\n // Validate session when browser tab becomes visible again\n // Catches session deletions that happened while the tab was inactive\n validateOnVisibility: true,\n};\n\nexport function setConfig(customConfig = {}) {\n if (!customConfig.clientKey || !customConfig.authBaseUrl) {\n throw new Error('Missing required config: clientKey and authBaseUrl are required');\n }\n\n config = {\n ...config,\n ...customConfig,\n redirectUri: customConfig.redirectUri || window.location.origin + '/callback',\n // ✅ Auto-detect router mode\n isRouter: customConfig.isRouter || customConfig.clientKey === 'account-ui'\n };\n\n console.log(`🔧 Auth Client Mode: ${config.isRouter ? 'ROUTER' : 'CLIENT'}`, {\n clientKey: config.clientKey,\n isRouter: config.isRouter\n });\n}\n\nexport function getConfig() {\n return { ...config };\n}\n\n// ✅ Helper function\nexport function isRouterMode() {\n return config.isRouter;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,gBAA2B;;;ACC3B,mBAAkE;;;ACClE,wBAA0B;AAM1B,IAAM,iBAAiB,IAAI,KAAK,KAAK;;;ACFrC,IAAI,SAAS;AAAA,EACX,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,cAAc;AAAA,EACd,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,EAKV,oBAAoB;AAAA;AAAA;AAAA;AAAA,EAKpB,2BAA2B,IAAI,KAAK;AAAA;AAAA;AAAA,EAIpC,yBAAyB;AAAA;AAAA;AAAA,EAIzB,wBAAwB;AAAA;AAAA;AAAA,EAIxB,sBAAsB;AACxB;;;AFtBO,IAAM,kBAAc,4BAAc;;;ADTlC,SAAS,UAAU;AACxB,QAAM,cAAU,0BAAW,WAAW;AACtC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AACA,SAAO;AACT;","names":["import_react"]}
|
|
1
|
+
{"version":3,"sources":["../../react/useAuth.js","../../react/AuthProvider.jsx","../../token.js","../../config.js"],"sourcesContent":["import { useContext } from 'react';\nimport { AuthContext } from './AuthProvider';\n\nexport function useAuth() {\n const context = useContext(AuthContext);\n if (!context) {\n throw new Error('useAuth must be used within an AuthProvider');\n }\n return context;\n}","// auth-client/react/AuthProvider.jsx\nimport React, { createContext, useState, useEffect, useRef } from 'react';\nimport { getToken, setToken, clearToken } from '../token';\nimport { getConfig } from '../config';\nimport { \n login as coreLogin, \n logout as coreLogout,\n startSessionSecurity,\n stopSessionSecurity,\n onSessionInvalid\n} from '../core';\n\nexport const AuthContext = createContext();\n\nexport function AuthProvider({ children, onSessionExpired }) {\n const [token, setTokenState] = useState(getToken());\n const [user, setUser] = useState(null);\n const [loading, setLoading] = useState(!!token); // Loading if we have a token to validate\n const [sessionValid, setSessionValid] = useState(true);\n const sessionSecurityRef = useRef(null);\n\n // Handle session invalidation (from Keycloak admin deletion or expiry)\n const handleSessionInvalid = (reason) => {\n console.log('🚨 AuthProvider: Session invalidated -', reason);\n setSessionValid(false);\n setUser(null);\n setTokenState(null);\n \n // Call custom callback if provided\n if (onSessionExpired && typeof onSessionExpired === 'function') {\n onSessionExpired(reason);\n }\n };\n\n // Start session security on mount (when we have a token)\n useEffect(() => {\n if (token && !sessionSecurityRef.current) {\n console.log('🔐 AuthProvider: Starting session security');\n \n // Register session invalid handler\n const unsubscribe = onSessionInvalid(handleSessionInvalid);\n \n // Start proactive refresh + session monitoring\n sessionSecurityRef.current = startSessionSecurity(handleSessionInvalid);\n \n return () => {\n unsubscribe();\n if (sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n };\n }\n \n // Cleanup when token is removed\n if (!token && sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n }, [token]);\n\n useEffect(() => {\n console.log('🔍 AuthProvider useEffect triggered:', { \n hasToken: !!token, \n tokenLength: token?.length \n });\n \n if (!token) {\n console.log('⚠️ AuthProvider: No token, setting loading=false');\n setLoading(false);\n return;\n }\n \n const { authBaseUrl } = getConfig();\n if (!authBaseUrl) {\n console.warn('AuthProvider: No authBaseUrl configured');\n setLoading(false);\n return;\n }\n\n console.log('🌐 AuthProvider: Fetching profile with token...', {\n authBaseUrl,\n tokenPreview: token.slice(0, 50) + '...'\n });\n\n fetch(`${authBaseUrl}/account/profile`, {\n headers: { Authorization: `Bearer ${token}` },\n credentials: 'include',\n })\n .then(res => {\n console.log('📥 Profile response status:', res.status);\n if (!res.ok) throw new Error('Failed to fetch user');\n return res.json();\n })\n .then(userData => {\n console.log('✅ Profile fetched successfully:', userData.email);\n setUser(userData);\n setSessionValid(true);\n setLoading(false);\n })\n .catch(err => {\n console.error('❌ Fetch user error:', err);\n clearToken();\n setTokenState(null);\n setUser(null);\n setLoading(false);\n });\n }, [token]);\n\n const login = (clientKey, redirectUri, state) => {\n coreLogin(clientKey, redirectUri, state);\n };\n\n const logout = () => {\n // Stop session security before logout\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n \n coreLogout();\n setUser(null);\n setTokenState(null);\n setSessionValid(true);\n };\n\n const value = {\n token,\n user,\n loading,\n login,\n logout,\n isAuthenticated: !!token && !!user && sessionValid,\n sessionValid,\n setUser,\n setToken: (newToken) => {\n setToken(newToken);\n setTokenState(newToken);\n setSessionValid(true);\n },\n clearToken: () => {\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n clearToken();\n setTokenState(null);\n setUser(null);\n },\n };\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n","// auth-client/token.js - MINIMAL WORKING VERSION\n\nimport { jwtDecode } from 'jwt-decode';\n\nlet accessToken = null;\nconst listeners = new Set();\n\nconst REFRESH_COOKIE = 'account_refresh_token';\nconst COOKIE_MAX_AGE = 7 * 24 * 60 * 60;\n\nfunction secureAttribute() {\n try {\n return typeof window !== 'undefined' && window.location?.protocol === 'https:'\n ? '; Secure'\n : '';\n } catch (err) {\n return '';\n }\n}\n\n// ========== ACCESS TOKEN ==========\nfunction writeAccessToken(token) {\n if (!token) {\n try {\n localStorage.removeItem('authToken');\n } catch (err) {\n console.warn('Could not clear token from localStorage:', err);\n }\n return;\n }\n\n try {\n localStorage.setItem('authToken', token);\n } catch (err) {\n console.warn('Could not persist token to localStorage:', err);\n }\n}\n\nfunction readAccessToken() {\n try {\n return localStorage.getItem('authToken');\n } catch (err) {\n console.warn('Could not read token from localStorage:', err);\n return null;\n }\n}\n\n// ========== REFRESH TOKEN (KEEP SIMPLE) ==========\n// export function setRefreshToken(token) {\n// if (!token) {\n// clearRefreshToken();\n// return;\n// }\n\n// const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);\n\n// try {\n// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax${secureAttribute()}; Expires=${expires.toUTCString()}`;\n// } catch (err) {\n// console.warn('Could not set refresh token:', err);\n// }\n// }\n\n// export function getRefreshToken() {\n// try {\n// const match = document.cookie\n// ?.split('; ')\n// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));\n\n// if (match) {\n// return decodeURIComponent(match.split('=')[1]);\n// }\n// } catch (err) {\n// console.warn('Could not read refresh token:', err);\n// }\n\n// return null;\n// }\n\n// export function clearRefreshToken() {\n// try {\n// document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Lax${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n// } catch (err) {\n// console.warn('Could not clear refresh token:', err);\n// }\n// }\n\n// ========== ACCESS TOKEN FUNCTIONS ==========\nfunction decode(token) {\n try {\n return jwtDecode(token);\n } catch (err) {\n return null;\n }\n}\n\nfunction isExpired(token, bufferSeconds = 60) {\n if (!token) return true;\n const decoded = decode(token);\n if (!decoded?.exp) return true;\n const now = Date.now() / 1000;\n return decoded.exp < now + bufferSeconds;\n}\n\n// ========== TOKEN EXPIRY UTILITIES ==========\n// Get the exact expiry time of a token as a Date object\nexport function getTokenExpiryTime(token) {\n if (!token) return null;\n const decoded = decode(token);\n if (!decoded?.exp) return null;\n return new Date(decoded.exp * 1000);\n}\n\n// Get seconds until token expires (negative if already expired)\nexport function getTimeUntilExpiry(token) {\n if (!token) return -1;\n const decoded = decode(token);\n if (!decoded?.exp) return -1;\n const now = Date.now() / 1000;\n return Math.floor(decoded.exp - now);\n}\n\n// Check if token will expire within the next N seconds\nexport function willExpireSoon(token, withinSeconds = 60) {\n const timeLeft = getTimeUntilExpiry(token);\n return timeLeft >= 0 && timeLeft <= withinSeconds;\n}\n\nexport function setToken(token) {\n const previousToken = accessToken;\n accessToken = token || null;\n writeAccessToken(accessToken);\n\n if (previousToken !== accessToken) {\n listeners.forEach((listener) => {\n try {\n listener(accessToken, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n }\n}\n\nexport function getToken() {\n if (accessToken) return accessToken;\n accessToken = readAccessToken();\n return accessToken;\n}\n\nexport function clearToken() {\n if (!accessToken) {\n writeAccessToken(null);\n clearRefreshToken();\n return;\n }\n\n const previousToken = accessToken;\n accessToken = null;\n writeAccessToken(null);\n clearRefreshToken();\n\n listeners.forEach((listener) => {\n try {\n listener(null, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n}\n\n// ========== REFRESH TOKEN STORAGE ==========\n// By default:\n// HTTP → localStorage (cookies don't work cross-origin in dev)\n// HTTPS → httpOnly cookies (secure, managed by server)\n// When persistRefreshToken is enabled:\n// Always use localStorage (for local HTTPS with mkcert/self-signed certs)\nconst REFRESH_TOKEN_KEY = 'auth_refresh_token';\n\n// ✅ Persistence flag - controlled by config.persistRefreshToken\nlet _persistRefreshToken = false;\n\nexport function enableRefreshTokenPersistence(enabled) {\n _persistRefreshToken = !!enabled;\n console.log(`🔧 Refresh token persistence: ${_persistRefreshToken ? 'ENABLED' : 'DISABLED'}`);\n}\n\nfunction shouldUseLocalStorage() {\n // If persistence is forced, always use localStorage\n if (_persistRefreshToken) return true;\n // Otherwise, only use localStorage on HTTP (dev mode)\n try {\n return typeof window !== 'undefined' &&\n window.location?.protocol === 'http:';\n } catch (err) {\n return false;\n }\n}\n\nexport function setRefreshToken(token) {\n if (!token) {\n clearRefreshToken();\n return;\n }\n\n if (shouldUseLocalStorage()) {\n try {\n localStorage.setItem(REFRESH_TOKEN_KEY, token);\n console.log(`📦 Refresh token stored in localStorage (${_persistRefreshToken ? 'persistence enabled' : 'HTTP dev mode'})`);\n } catch (err) {\n console.warn('Could not store refresh token:', err);\n }\n } else {\n // HTTPS without persistence: refresh token is in httpOnly cookie only\n console.log('🔒 Refresh token managed by server httpOnly cookie (production mode)');\n }\n}\n\nexport function getRefreshToken() {\n if (shouldUseLocalStorage()) {\n try {\n const token = localStorage.getItem(REFRESH_TOKEN_KEY);\n return token;\n } catch (err) {\n console.warn('Could not read refresh token:', err);\n return null;\n }\n }\n\n // HTTPS without persistence: refresh token is in httpOnly cookie (not accessible via JS)\n // The refresh endpoint uses credentials: 'include' to send the cookie\n return null;\n}\n\nexport function clearRefreshToken() {\n // Clear localStorage (for HTTP dev)\n try {\n localStorage.removeItem(REFRESH_TOKEN_KEY);\n } catch (err) {\n // Ignore\n }\n\n // Clear cookie (for production)\n try {\n document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Strict${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n } catch (err) {\n console.warn('Could not clear refresh token cookie:', err);\n }\n\n // Clear sessionStorage\n try {\n sessionStorage.removeItem(REFRESH_COOKIE);\n } catch (err) {\n // Ignore\n }\n}\n\nexport function addTokenListener(listener) {\n if (typeof listener !== 'function') {\n throw new Error('Token listener must be a function');\n }\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nexport function removeTokenListener(listener) {\n listeners.delete(listener);\n}\n\nexport function getListenerCount() {\n return listeners.size;\n}\n\nexport function isAuthenticated() {\n const token = getToken();\n return !!token && !isExpired(token, 15);\n}\n\n\n\n","// auth-client/config.js\nimport { enableRefreshTokenPersistence } from './token';\n\n// ========== SESSION SECURITY CONFIGURATION ==========\n// These settings control how the auth-client handles token refresh and session validation\n// to ensure deleted sessions in Keycloak are detected quickly.\n\nlet config = {\n clientKey: null,\n authBaseUrl: null,\n redirectUri: null,\n accountUiUrl: null,\n isRouter: false, // ✅ Add router flag\n\n // ========== SESSION SECURITY SETTINGS ==========\n // Buffer time (in seconds) before token expiry to trigger proactive refresh\n // With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX\n tokenRefreshBuffer: 60,\n\n // Interval (in milliseconds) for periodic session validation\n // Validates that the session still exists in Keycloak (not deleted by admin)\n // Default: 15 minutes (900000ms) - Increased from 2m to avoid frequent checks\n sessionValidationInterval: 15 * 60 * 1000,\n\n // Enable/disable periodic session validation\n // When enabled, the client will ping the server to verify session is still active\n enableSessionValidation: true,\n\n // Enable/disable proactive token refresh\n // When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)\n enableProactiveRefresh: true,\n\n // Validate session when browser tab becomes visible again\n // Catches session deletions that happened while the tab was inactive\n validateOnVisibility: true,\n\n // ========== REFRESH TOKEN PERSISTENCE ==========\n // When true, stores refresh token in localStorage even on HTTPS\n // Required for local dev with mkcert/self-signed certs where httpOnly cookies\n // may not work reliably across origins\n // ⚠️ In true production, set to false and rely on httpOnly cookies\n persistRefreshToken: false,\n};\n\nexport function setConfig(customConfig = {}) {\n if (!customConfig.clientKey || !customConfig.authBaseUrl) {\n throw new Error('Missing required config: clientKey and authBaseUrl are required');\n }\n\n config = {\n ...config,\n ...customConfig,\n redirectUri: customConfig.redirectUri || window.location.origin + '/callback',\n // ✅ Auto-detect router mode\n isRouter: customConfig.isRouter || customConfig.clientKey === 'account-ui'\n };\n\n // ✅ Wire persistRefreshToken to token.js\n if (config.persistRefreshToken) {\n enableRefreshTokenPersistence(true);\n console.log('📦 Refresh token persistence ENABLED (localStorage on HTTPS)');\n }\n\n console.log(`🔧 Auth Client Mode: ${config.isRouter ? 'ROUTER' : 'CLIENT'}`, {\n clientKey: config.clientKey,\n isRouter: config.isRouter,\n persistRefreshToken: config.persistRefreshToken\n });\n}\n\nexport function getConfig() {\n return { ...config };\n}\n\n// ✅ Helper function\nexport function isRouterMode() {\n return config.isRouter;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,gBAA2B;;;ACC3B,mBAAkE;;;ACClE,wBAA0B;AAM1B,IAAM,iBAAiB,IAAI,KAAK,KAAK;;;ACDrC,IAAI,SAAS;AAAA,EACX,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,cAAc;AAAA,EACd,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,EAKV,oBAAoB;AAAA;AAAA;AAAA;AAAA,EAKpB,2BAA2B,KAAK,KAAK;AAAA;AAAA;AAAA,EAIrC,yBAAyB;AAAA;AAAA;AAAA,EAIzB,wBAAwB;AAAA;AAAA;AAAA,EAIxB,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtB,qBAAqB;AACvB;;;AF9BO,IAAM,kBAAc,4BAAc;;;ADTlC,SAAS,UAAU;AACxB,QAAM,cAAU,0BAAW,WAAW;AACtC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AACA,SAAO;AACT;","names":["import_react"]}
|
package/dist/react/useAuth.js
CHANGED
|
@@ -22,8 +22,8 @@ var config = {
|
|
|
22
22
|
tokenRefreshBuffer: 60,
|
|
23
23
|
// Interval (in milliseconds) for periodic session validation
|
|
24
24
|
// Validates that the session still exists in Keycloak (not deleted by admin)
|
|
25
|
-
// Default:
|
|
26
|
-
sessionValidationInterval:
|
|
25
|
+
// Default: 15 minutes (900000ms) - Increased from 2m to avoid frequent checks
|
|
26
|
+
sessionValidationInterval: 15 * 60 * 1e3,
|
|
27
27
|
// Enable/disable periodic session validation
|
|
28
28
|
// When enabled, the client will ping the server to verify session is still active
|
|
29
29
|
enableSessionValidation: true,
|
|
@@ -32,7 +32,13 @@ var config = {
|
|
|
32
32
|
enableProactiveRefresh: true,
|
|
33
33
|
// Validate session when browser tab becomes visible again
|
|
34
34
|
// Catches session deletions that happened while the tab was inactive
|
|
35
|
-
validateOnVisibility: true
|
|
35
|
+
validateOnVisibility: true,
|
|
36
|
+
// ========== REFRESH TOKEN PERSISTENCE ==========
|
|
37
|
+
// When true, stores refresh token in localStorage even on HTTPS
|
|
38
|
+
// Required for local dev with mkcert/self-signed certs where httpOnly cookies
|
|
39
|
+
// may not work reliably across origins
|
|
40
|
+
// ⚠️ In true production, set to false and rely on httpOnly cookies
|
|
41
|
+
persistRefreshToken: false
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
// react/AuthProvider.jsx
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../react/useAuth.js","../../react/AuthProvider.jsx","../../token.js","../../config.js"],"sourcesContent":["import { useContext } from 'react';\nimport { AuthContext } from './AuthProvider';\n\nexport function useAuth() {\n const context = useContext(AuthContext);\n if (!context) {\n throw new Error('useAuth must be used within an AuthProvider');\n }\n return context;\n}","// auth-client/react/AuthProvider.jsx\nimport React, { createContext, useState, useEffect, useRef } from 'react';\nimport { getToken, setToken, clearToken } from '../token';\nimport { getConfig } from '../config';\nimport { \n login as coreLogin, \n logout as coreLogout,\n startSessionSecurity,\n stopSessionSecurity,\n onSessionInvalid\n} from '../core';\n\nexport const AuthContext = createContext();\n\nexport function AuthProvider({ children, onSessionExpired }) {\n const [token, setTokenState] = useState(getToken());\n const [user, setUser] = useState(null);\n const [loading, setLoading] = useState(!!token); // Loading if we have a token to validate\n const [sessionValid, setSessionValid] = useState(true);\n const sessionSecurityRef = useRef(null);\n\n // Handle session invalidation (from Keycloak admin deletion or expiry)\n const handleSessionInvalid = (reason) => {\n console.log('🚨 AuthProvider: Session invalidated -', reason);\n setSessionValid(false);\n setUser(null);\n setTokenState(null);\n \n // Call custom callback if provided\n if (onSessionExpired && typeof onSessionExpired === 'function') {\n onSessionExpired(reason);\n }\n };\n\n // Start session security on mount (when we have a token)\n useEffect(() => {\n if (token && !sessionSecurityRef.current) {\n console.log('🔐 AuthProvider: Starting session security');\n \n // Register session invalid handler\n const unsubscribe = onSessionInvalid(handleSessionInvalid);\n \n // Start proactive refresh + session monitoring\n sessionSecurityRef.current = startSessionSecurity(handleSessionInvalid);\n \n return () => {\n unsubscribe();\n if (sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n };\n }\n \n // Cleanup when token is removed\n if (!token && sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n }, [token]);\n\n useEffect(() => {\n console.log('🔍 AuthProvider useEffect triggered:', { \n hasToken: !!token, \n tokenLength: token?.length \n });\n \n if (!token) {\n console.log('⚠️ AuthProvider: No token, setting loading=false');\n setLoading(false);\n return;\n }\n \n const { authBaseUrl } = getConfig();\n if (!authBaseUrl) {\n console.warn('AuthProvider: No authBaseUrl configured');\n setLoading(false);\n return;\n }\n\n console.log('🌐 AuthProvider: Fetching profile with token...', {\n authBaseUrl,\n tokenPreview: token.slice(0, 50) + '...'\n });\n\n fetch(`${authBaseUrl}/account/profile`, {\n headers: { Authorization: `Bearer ${token}` },\n credentials: 'include',\n })\n .then(res => {\n console.log('📥 Profile response status:', res.status);\n if (!res.ok) throw new Error('Failed to fetch user');\n return res.json();\n })\n .then(userData => {\n console.log('✅ Profile fetched successfully:', userData.email);\n setUser(userData);\n setSessionValid(true);\n setLoading(false);\n })\n .catch(err => {\n console.error('❌ Fetch user error:', err);\n clearToken();\n setTokenState(null);\n setUser(null);\n setLoading(false);\n });\n }, [token]);\n\n const login = (clientKey, redirectUri, state) => {\n coreLogin(clientKey, redirectUri, state);\n };\n\n const logout = () => {\n // Stop session security before logout\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n \n coreLogout();\n setUser(null);\n setTokenState(null);\n setSessionValid(true);\n };\n\n const value = {\n token,\n user,\n loading,\n login,\n logout,\n isAuthenticated: !!token && !!user && sessionValid,\n sessionValid,\n setUser,\n setToken: (newToken) => {\n setToken(newToken);\n setTokenState(newToken);\n setSessionValid(true);\n },\n clearToken: () => {\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n clearToken();\n setTokenState(null);\n setUser(null);\n },\n };\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n","// auth-client/token.js - MINIMAL WORKING VERSION\n\nimport { jwtDecode } from 'jwt-decode';\n\nlet accessToken = null;\nconst listeners = new Set();\n\nconst REFRESH_COOKIE = 'account_refresh_token';\nconst COOKIE_MAX_AGE = 7 * 24 * 60 * 60;\n\nfunction secureAttribute() {\n try {\n return typeof window !== 'undefined' && window.location?.protocol === 'https:'\n ? '; Secure'\n : '';\n } catch (err) {\n return '';\n }\n}\n\n// ========== ACCESS TOKEN ==========\nfunction writeAccessToken(token) {\n if (!token) {\n try {\n localStorage.removeItem('authToken');\n } catch (err) {\n console.warn('Could not clear token from localStorage:', err);\n }\n return;\n }\n\n try {\n localStorage.setItem('authToken', token);\n } catch (err) {\n console.warn('Could not persist token to localStorage:', err);\n }\n}\n\nfunction readAccessToken() {\n try {\n return localStorage.getItem('authToken');\n } catch (err) {\n console.warn('Could not read token from localStorage:', err);\n return null;\n }\n}\n\n// ========== REFRESH TOKEN (KEEP SIMPLE) ==========\n// export function setRefreshToken(token) {\n// if (!token) {\n// clearRefreshToken();\n// return;\n// }\n\n// const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);\n\n// try {\n// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax${secureAttribute()}; Expires=${expires.toUTCString()}`;\n// } catch (err) {\n// console.warn('Could not set refresh token:', err);\n// }\n// }\n\n// export function getRefreshToken() {\n// try {\n// const match = document.cookie\n// ?.split('; ')\n// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));\n\n// if (match) {\n// return decodeURIComponent(match.split('=')[1]);\n// }\n// } catch (err) {\n// console.warn('Could not read refresh token:', err);\n// }\n\n// return null;\n// }\n\n// export function clearRefreshToken() {\n// try {\n// document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Lax${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n// } catch (err) {\n// console.warn('Could not clear refresh token:', err);\n// }\n// }\n\n// ========== ACCESS TOKEN FUNCTIONS ==========\nfunction decode(token) {\n try {\n return jwtDecode(token);\n } catch (err) {\n return null;\n }\n}\n\nfunction isExpired(token, bufferSeconds = 60) {\n if (!token) return true;\n const decoded = decode(token);\n if (!decoded?.exp) return true;\n const now = Date.now() / 1000;\n return decoded.exp < now + bufferSeconds;\n}\n\n// ========== TOKEN EXPIRY UTILITIES ==========\n// Get the exact expiry time of a token as a Date object\nexport function getTokenExpiryTime(token) {\n if (!token) return null;\n const decoded = decode(token);\n if (!decoded?.exp) return null;\n return new Date(decoded.exp * 1000);\n}\n\n// Get seconds until token expires (negative if already expired)\nexport function getTimeUntilExpiry(token) {\n if (!token) return -1;\n const decoded = decode(token);\n if (!decoded?.exp) return -1;\n const now = Date.now() / 1000;\n return Math.floor(decoded.exp - now);\n}\n\n// Check if token will expire within the next N seconds\nexport function willExpireSoon(token, withinSeconds = 60) {\n const timeLeft = getTimeUntilExpiry(token);\n return timeLeft >= 0 && timeLeft <= withinSeconds;\n}\n\nexport function setToken(token) {\n const previousToken = accessToken;\n accessToken = token || null;\n writeAccessToken(accessToken);\n\n if (previousToken !== accessToken) {\n listeners.forEach((listener) => {\n try {\n listener(accessToken, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n }\n}\n\nexport function getToken() {\n if (accessToken) return accessToken;\n accessToken = readAccessToken();\n return accessToken;\n}\n\nexport function clearToken() {\n if (!accessToken) {\n writeAccessToken(null);\n clearRefreshToken();\n return;\n }\n\n const previousToken = accessToken;\n accessToken = null;\n writeAccessToken(null);\n clearRefreshToken();\n\n listeners.forEach((listener) => {\n try {\n listener(null, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n}\n\n// ========== REFRESH TOKEN STORAGE FOR HTTP DEVELOPMENT ==========\n// In production (HTTPS), refresh tokens should ONLY be in httpOnly cookies set by server\n// For HTTP development (cross-origin cookies don't work), we store in localStorage\nconst REFRESH_TOKEN_KEY = 'auth_refresh_token';\n\nfunction isHttpDevelopment() {\n try {\n return typeof window !== 'undefined' &&\n window.location?.protocol === 'http:';\n } catch (err) {\n return false;\n }\n}\n\nexport function setRefreshToken(token) {\n if (!token) {\n clearRefreshToken();\n return;\n }\n\n // For HTTP development, store in localStorage (since httpOnly cookies don't work cross-origin)\n if (isHttpDevelopment()) {\n try {\n localStorage.setItem(REFRESH_TOKEN_KEY, token);\n console.log('📦 Refresh token stored in localStorage (HTTP dev mode)');\n } catch (err) {\n console.warn('Could not store refresh token:', err);\n }\n } else {\n // In production (HTTPS), refresh token should be in httpOnly cookie only\n console.log('🔒 Refresh token managed by server httpOnly cookie (production mode)');\n }\n}\n\nexport function getRefreshToken() {\n // For HTTP development, read from localStorage\n if (isHttpDevelopment()) {\n try {\n const token = localStorage.getItem(REFRESH_TOKEN_KEY);\n return token;\n } catch (err) {\n console.warn('Could not read refresh token:', err);\n return null;\n }\n }\n\n // In production, refresh token is in httpOnly cookie (not accessible via JS)\n // The refresh endpoint uses credentials: 'include' to send the cookie\n return null;\n}\n\nexport function clearRefreshToken() {\n // Clear localStorage (for HTTP dev)\n try {\n localStorage.removeItem(REFRESH_TOKEN_KEY);\n } catch (err) {\n // Ignore\n }\n\n // Clear cookie (for production)\n try {\n document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Strict${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n } catch (err) {\n console.warn('Could not clear refresh token cookie:', err);\n }\n\n // Clear sessionStorage\n try {\n sessionStorage.removeItem(REFRESH_COOKIE);\n } catch (err) {\n // Ignore\n }\n}\n\nexport function addTokenListener(listener) {\n if (typeof listener !== 'function') {\n throw new Error('Token listener must be a function');\n }\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nexport function removeTokenListener(listener) {\n listeners.delete(listener);\n}\n\nexport function getListenerCount() {\n return listeners.size;\n}\n\nexport function isAuthenticated() {\n const token = getToken();\n return !!token && !isExpired(token, 15);\n}\n\n\n\n\n// // auth-client/token.js\n// import { jwtDecode } from 'jwt-decode';\n\n// let accessToken = null;\n// const listeners = new Set();\n\n// const REFRESH_COOKIE = 'account_refresh_token';\n// const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days in seconds\n\n// function secureAttribute() {\n// try {\n// return typeof window !== 'undefined' && window.location?.protocol === 'https:'\n// ? '; Secure'\n// : '';\n// } catch (err) {\n// return '';\n// }\n// }\n\n// function writeAccessToken(token) {\n// if (!token) {\n// try {\n// localStorage.removeItem('authToken');\n// } catch (err) {\n// console.warn('Could not clear token from localStorage:', err);\n// }\n// return;\n// }\n\n// try {\n// localStorage.setItem('authToken', token);\n// } catch (err) {\n// console.warn('Could not persist token to localStorage:', err);\n// }\n// }\n\n// function readAccessToken() {\n// try {\n// return localStorage.getItem('authToken');\n// } catch (err) {\n// console.warn('Could not read token from localStorage:', err);\n// return null;\n// }\n// }\n\n// function decode(token) {\n// try {\n// return jwtDecode(token);\n// } catch (err) {\n// return null;\n// }\n// }\n\n// function isExpired(token, bufferSeconds = 60) {\n// if (!token) return true;\n// const decoded = decode(token);\n// if (!decoded?.exp) return true;\n// const now = Date.now() / 1000;\n// return decoded.exp < now + bufferSeconds;\n// }\n\n// export function setToken(token) {\n// const previousToken = accessToken;\n// accessToken = token || null;\n// writeAccessToken(accessToken);\n\n// if (previousToken !== accessToken) {\n// listeners.forEach((listener) => {\n// try {\n// listener(accessToken, previousToken);\n// } catch (err) {\n// console.warn('Token listener error:', err);\n// }\n// });\n// }\n// }\n\n// export function getToken() {\n// if (accessToken) return accessToken;\n// accessToken = readAccessToken();\n// return accessToken;\n// }\n\n// export function clearToken() {\n// if (!accessToken) {\n// writeAccessToken(null);\n// clearRefreshToken();\n// return;\n// }\n\n// const previousToken = accessToken;\n// accessToken = null;\n// writeAccessToken(null);\n// clearRefreshToken();\n\n// listeners.forEach((listener) => {\n// try {\n// listener(null, previousToken);\n// } catch (err) {\n// console.warn('Token listener error:', err);\n// }\n// });\n// }\n\n// export function setRefreshToken(token) {\n// if (!token) {\n// clearRefreshToken();\n// return;\n// }\n\n// const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);\n// try {\n// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict${secureAttribute()}; Expires=${expires.toUTCString()}`;\n// } catch (err) {\n// console.warn('Could not persist refresh token cookie:', err);\n// }\n\n// try {\n// sessionStorage.setItem(REFRESH_COOKIE, token);\n// } catch (err) {\n// console.warn('Could not persist refresh token to sessionStorage:', err);\n// }\n// }\n\n// export function getRefreshToken() {\n// // Prefer cookie to align with server expectations\n// let cookieMatch = null;\n\n// try {\n// cookieMatch = document.cookie\n// ?.split('; ')\n// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));\n// } catch (err) {\n// cookieMatch = null;\n// }\n\n// if (cookieMatch) {\n// return decodeURIComponent(cookieMatch.split('=')[1]);\n// }\n\n// try {\n// return sessionStorage.getItem(REFRESH_COOKIE);\n// } catch (err) {\n// console.warn('Could not read refresh token from sessionStorage:', err);\n// return null;\n// }\n// }\n\n// export function clearRefreshToken() {\n// try {\n// document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Strict${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n// } catch (err) {\n// console.warn('Could not clear refresh token cookie:', err);\n// }\n// try {\n// sessionStorage.removeItem(REFRESH_COOKIE);\n// } catch (err) {\n// console.warn('Could not clear refresh token from sessionStorage:', err);\n// }\n// }\n\n// export function addTokenListener(listener) {\n// if (typeof listener !== 'function') {\n// throw new Error('Token listener must be a function');\n// }\n// listeners.add(listener);\n// return () => {\n// listeners.delete(listener);\n// };\n// }\n\n// export function removeTokenListener(listener) {\n// listeners.delete(listener);\n// }\n\n// export function getListenerCount() {\n// return listeners.size;\n// }\n\n// export function isAuthenticated() {\n// const token = getToken();\n// return !!token && !isExpired(token, 15);\n// }\n\n","// auth-client/config.js\n\n// ========== SESSION SECURITY CONFIGURATION ==========\n// These settings control how the auth-client handles token refresh and session validation\n// to ensure deleted sessions in Keycloak are detected quickly.\n\nlet config = {\n clientKey: null,\n authBaseUrl: null,\n redirectUri: null,\n accountUiUrl: null,\n isRouter: false, // ✅ Add router flag\n\n // ========== SESSION SECURITY SETTINGS ==========\n // Buffer time (in seconds) before token expiry to trigger proactive refresh\n // With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX\n tokenRefreshBuffer: 60,\n\n // Interval (in milliseconds) for periodic session validation\n // Validates that the session still exists in Keycloak (not deleted by admin)\n // Default: 2 minutes (120000ms) - balances responsiveness vs server load\n sessionValidationInterval: 2 * 60 * 1000,\n\n // Enable/disable periodic session validation\n // When enabled, the client will ping the server to verify session is still active\n enableSessionValidation: true,\n\n // Enable/disable proactive token refresh\n // When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)\n enableProactiveRefresh: true,\n\n // Validate session when browser tab becomes visible again\n // Catches session deletions that happened while the tab was inactive\n validateOnVisibility: true,\n};\n\nexport function setConfig(customConfig = {}) {\n if (!customConfig.clientKey || !customConfig.authBaseUrl) {\n throw new Error('Missing required config: clientKey and authBaseUrl are required');\n }\n\n config = {\n ...config,\n ...customConfig,\n redirectUri: customConfig.redirectUri || window.location.origin + '/callback',\n // ✅ Auto-detect router mode\n isRouter: customConfig.isRouter || customConfig.clientKey === 'account-ui'\n };\n\n console.log(`🔧 Auth Client Mode: ${config.isRouter ? 'ROUTER' : 'CLIENT'}`, {\n clientKey: config.clientKey,\n isRouter: config.isRouter\n });\n}\n\nexport function getConfig() {\n return { ...config };\n}\n\n// ✅ Helper function\nexport function isRouterMode() {\n return config.isRouter;\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;;;ACC3B,OAAO,SAAS,eAAe,UAAU,WAAW,cAAc;;;ACClE,SAAS,iBAAiB;AAM1B,IAAM,iBAAiB,IAAI,KAAK,KAAK;;;ACFrC,IAAI,SAAS;AAAA,EACX,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,cAAc;AAAA,EACd,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,EAKV,oBAAoB;AAAA;AAAA;AAAA;AAAA,EAKpB,2BAA2B,IAAI,KAAK;AAAA;AAAA;AAAA,EAIpC,yBAAyB;AAAA;AAAA;AAAA,EAIzB,wBAAwB;AAAA;AAAA;AAAA,EAIxB,sBAAsB;AACxB;;;AFtBO,IAAM,cAAc,cAAc;;;ADTlC,SAAS,UAAU;AACxB,QAAM,UAAU,WAAW,WAAW;AACtC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AACA,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../react/useAuth.js","../../react/AuthProvider.jsx","../../token.js","../../config.js"],"sourcesContent":["import { useContext } from 'react';\nimport { AuthContext } from './AuthProvider';\n\nexport function useAuth() {\n const context = useContext(AuthContext);\n if (!context) {\n throw new Error('useAuth must be used within an AuthProvider');\n }\n return context;\n}","// auth-client/react/AuthProvider.jsx\nimport React, { createContext, useState, useEffect, useRef } from 'react';\nimport { getToken, setToken, clearToken } from '../token';\nimport { getConfig } from '../config';\nimport { \n login as coreLogin, \n logout as coreLogout,\n startSessionSecurity,\n stopSessionSecurity,\n onSessionInvalid\n} from '../core';\n\nexport const AuthContext = createContext();\n\nexport function AuthProvider({ children, onSessionExpired }) {\n const [token, setTokenState] = useState(getToken());\n const [user, setUser] = useState(null);\n const [loading, setLoading] = useState(!!token); // Loading if we have a token to validate\n const [sessionValid, setSessionValid] = useState(true);\n const sessionSecurityRef = useRef(null);\n\n // Handle session invalidation (from Keycloak admin deletion or expiry)\n const handleSessionInvalid = (reason) => {\n console.log('🚨 AuthProvider: Session invalidated -', reason);\n setSessionValid(false);\n setUser(null);\n setTokenState(null);\n \n // Call custom callback if provided\n if (onSessionExpired && typeof onSessionExpired === 'function') {\n onSessionExpired(reason);\n }\n };\n\n // Start session security on mount (when we have a token)\n useEffect(() => {\n if (token && !sessionSecurityRef.current) {\n console.log('🔐 AuthProvider: Starting session security');\n \n // Register session invalid handler\n const unsubscribe = onSessionInvalid(handleSessionInvalid);\n \n // Start proactive refresh + session monitoring\n sessionSecurityRef.current = startSessionSecurity(handleSessionInvalid);\n \n return () => {\n unsubscribe();\n if (sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n };\n }\n \n // Cleanup when token is removed\n if (!token && sessionSecurityRef.current) {\n sessionSecurityRef.current.stopAll();\n sessionSecurityRef.current = null;\n }\n }, [token]);\n\n useEffect(() => {\n console.log('🔍 AuthProvider useEffect triggered:', { \n hasToken: !!token, \n tokenLength: token?.length \n });\n \n if (!token) {\n console.log('⚠️ AuthProvider: No token, setting loading=false');\n setLoading(false);\n return;\n }\n \n const { authBaseUrl } = getConfig();\n if (!authBaseUrl) {\n console.warn('AuthProvider: No authBaseUrl configured');\n setLoading(false);\n return;\n }\n\n console.log('🌐 AuthProvider: Fetching profile with token...', {\n authBaseUrl,\n tokenPreview: token.slice(0, 50) + '...'\n });\n\n fetch(`${authBaseUrl}/account/profile`, {\n headers: { Authorization: `Bearer ${token}` },\n credentials: 'include',\n })\n .then(res => {\n console.log('📥 Profile response status:', res.status);\n if (!res.ok) throw new Error('Failed to fetch user');\n return res.json();\n })\n .then(userData => {\n console.log('✅ Profile fetched successfully:', userData.email);\n setUser(userData);\n setSessionValid(true);\n setLoading(false);\n })\n .catch(err => {\n console.error('❌ Fetch user error:', err);\n clearToken();\n setTokenState(null);\n setUser(null);\n setLoading(false);\n });\n }, [token]);\n\n const login = (clientKey, redirectUri, state) => {\n coreLogin(clientKey, redirectUri, state);\n };\n\n const logout = () => {\n // Stop session security before logout\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n \n coreLogout();\n setUser(null);\n setTokenState(null);\n setSessionValid(true);\n };\n\n const value = {\n token,\n user,\n loading,\n login,\n logout,\n isAuthenticated: !!token && !!user && sessionValid,\n sessionValid,\n setUser,\n setToken: (newToken) => {\n setToken(newToken);\n setTokenState(newToken);\n setSessionValid(true);\n },\n clearToken: () => {\n stopSessionSecurity();\n sessionSecurityRef.current = null;\n clearToken();\n setTokenState(null);\n setUser(null);\n },\n };\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n","// auth-client/token.js - MINIMAL WORKING VERSION\n\nimport { jwtDecode } from 'jwt-decode';\n\nlet accessToken = null;\nconst listeners = new Set();\n\nconst REFRESH_COOKIE = 'account_refresh_token';\nconst COOKIE_MAX_AGE = 7 * 24 * 60 * 60;\n\nfunction secureAttribute() {\n try {\n return typeof window !== 'undefined' && window.location?.protocol === 'https:'\n ? '; Secure'\n : '';\n } catch (err) {\n return '';\n }\n}\n\n// ========== ACCESS TOKEN ==========\nfunction writeAccessToken(token) {\n if (!token) {\n try {\n localStorage.removeItem('authToken');\n } catch (err) {\n console.warn('Could not clear token from localStorage:', err);\n }\n return;\n }\n\n try {\n localStorage.setItem('authToken', token);\n } catch (err) {\n console.warn('Could not persist token to localStorage:', err);\n }\n}\n\nfunction readAccessToken() {\n try {\n return localStorage.getItem('authToken');\n } catch (err) {\n console.warn('Could not read token from localStorage:', err);\n return null;\n }\n}\n\n// ========== REFRESH TOKEN (KEEP SIMPLE) ==========\n// export function setRefreshToken(token) {\n// if (!token) {\n// clearRefreshToken();\n// return;\n// }\n\n// const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);\n\n// try {\n// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax${secureAttribute()}; Expires=${expires.toUTCString()}`;\n// } catch (err) {\n// console.warn('Could not set refresh token:', err);\n// }\n// }\n\n// export function getRefreshToken() {\n// try {\n// const match = document.cookie\n// ?.split('; ')\n// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));\n\n// if (match) {\n// return decodeURIComponent(match.split('=')[1]);\n// }\n// } catch (err) {\n// console.warn('Could not read refresh token:', err);\n// }\n\n// return null;\n// }\n\n// export function clearRefreshToken() {\n// try {\n// document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Lax${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n// } catch (err) {\n// console.warn('Could not clear refresh token:', err);\n// }\n// }\n\n// ========== ACCESS TOKEN FUNCTIONS ==========\nfunction decode(token) {\n try {\n return jwtDecode(token);\n } catch (err) {\n return null;\n }\n}\n\nfunction isExpired(token, bufferSeconds = 60) {\n if (!token) return true;\n const decoded = decode(token);\n if (!decoded?.exp) return true;\n const now = Date.now() / 1000;\n return decoded.exp < now + bufferSeconds;\n}\n\n// ========== TOKEN EXPIRY UTILITIES ==========\n// Get the exact expiry time of a token as a Date object\nexport function getTokenExpiryTime(token) {\n if (!token) return null;\n const decoded = decode(token);\n if (!decoded?.exp) return null;\n return new Date(decoded.exp * 1000);\n}\n\n// Get seconds until token expires (negative if already expired)\nexport function getTimeUntilExpiry(token) {\n if (!token) return -1;\n const decoded = decode(token);\n if (!decoded?.exp) return -1;\n const now = Date.now() / 1000;\n return Math.floor(decoded.exp - now);\n}\n\n// Check if token will expire within the next N seconds\nexport function willExpireSoon(token, withinSeconds = 60) {\n const timeLeft = getTimeUntilExpiry(token);\n return timeLeft >= 0 && timeLeft <= withinSeconds;\n}\n\nexport function setToken(token) {\n const previousToken = accessToken;\n accessToken = token || null;\n writeAccessToken(accessToken);\n\n if (previousToken !== accessToken) {\n listeners.forEach((listener) => {\n try {\n listener(accessToken, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n }\n}\n\nexport function getToken() {\n if (accessToken) return accessToken;\n accessToken = readAccessToken();\n return accessToken;\n}\n\nexport function clearToken() {\n if (!accessToken) {\n writeAccessToken(null);\n clearRefreshToken();\n return;\n }\n\n const previousToken = accessToken;\n accessToken = null;\n writeAccessToken(null);\n clearRefreshToken();\n\n listeners.forEach((listener) => {\n try {\n listener(null, previousToken);\n } catch (err) {\n console.warn('Token listener error:', err);\n }\n });\n}\n\n// ========== REFRESH TOKEN STORAGE ==========\n// By default:\n// HTTP → localStorage (cookies don't work cross-origin in dev)\n// HTTPS → httpOnly cookies (secure, managed by server)\n// When persistRefreshToken is enabled:\n// Always use localStorage (for local HTTPS with mkcert/self-signed certs)\nconst REFRESH_TOKEN_KEY = 'auth_refresh_token';\n\n// ✅ Persistence flag - controlled by config.persistRefreshToken\nlet _persistRefreshToken = false;\n\nexport function enableRefreshTokenPersistence(enabled) {\n _persistRefreshToken = !!enabled;\n console.log(`🔧 Refresh token persistence: ${_persistRefreshToken ? 'ENABLED' : 'DISABLED'}`);\n}\n\nfunction shouldUseLocalStorage() {\n // If persistence is forced, always use localStorage\n if (_persistRefreshToken) return true;\n // Otherwise, only use localStorage on HTTP (dev mode)\n try {\n return typeof window !== 'undefined' &&\n window.location?.protocol === 'http:';\n } catch (err) {\n return false;\n }\n}\n\nexport function setRefreshToken(token) {\n if (!token) {\n clearRefreshToken();\n return;\n }\n\n if (shouldUseLocalStorage()) {\n try {\n localStorage.setItem(REFRESH_TOKEN_KEY, token);\n console.log(`📦 Refresh token stored in localStorage (${_persistRefreshToken ? 'persistence enabled' : 'HTTP dev mode'})`);\n } catch (err) {\n console.warn('Could not store refresh token:', err);\n }\n } else {\n // HTTPS without persistence: refresh token is in httpOnly cookie only\n console.log('🔒 Refresh token managed by server httpOnly cookie (production mode)');\n }\n}\n\nexport function getRefreshToken() {\n if (shouldUseLocalStorage()) {\n try {\n const token = localStorage.getItem(REFRESH_TOKEN_KEY);\n return token;\n } catch (err) {\n console.warn('Could not read refresh token:', err);\n return null;\n }\n }\n\n // HTTPS without persistence: refresh token is in httpOnly cookie (not accessible via JS)\n // The refresh endpoint uses credentials: 'include' to send the cookie\n return null;\n}\n\nexport function clearRefreshToken() {\n // Clear localStorage (for HTTP dev)\n try {\n localStorage.removeItem(REFRESH_TOKEN_KEY);\n } catch (err) {\n // Ignore\n }\n\n // Clear cookie (for production)\n try {\n document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Strict${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n } catch (err) {\n console.warn('Could not clear refresh token cookie:', err);\n }\n\n // Clear sessionStorage\n try {\n sessionStorage.removeItem(REFRESH_COOKIE);\n } catch (err) {\n // Ignore\n }\n}\n\nexport function addTokenListener(listener) {\n if (typeof listener !== 'function') {\n throw new Error('Token listener must be a function');\n }\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nexport function removeTokenListener(listener) {\n listeners.delete(listener);\n}\n\nexport function getListenerCount() {\n return listeners.size;\n}\n\nexport function isAuthenticated() {\n const token = getToken();\n return !!token && !isExpired(token, 15);\n}\n\n\n\n","// auth-client/config.js\nimport { enableRefreshTokenPersistence } from './token';\n\n// ========== SESSION SECURITY CONFIGURATION ==========\n// These settings control how the auth-client handles token refresh and session validation\n// to ensure deleted sessions in Keycloak are detected quickly.\n\nlet config = {\n clientKey: null,\n authBaseUrl: null,\n redirectUri: null,\n accountUiUrl: null,\n isRouter: false, // ✅ Add router flag\n\n // ========== SESSION SECURITY SETTINGS ==========\n // Buffer time (in seconds) before token expiry to trigger proactive refresh\n // With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX\n tokenRefreshBuffer: 60,\n\n // Interval (in milliseconds) for periodic session validation\n // Validates that the session still exists in Keycloak (not deleted by admin)\n // Default: 15 minutes (900000ms) - Increased from 2m to avoid frequent checks\n sessionValidationInterval: 15 * 60 * 1000,\n\n // Enable/disable periodic session validation\n // When enabled, the client will ping the server to verify session is still active\n enableSessionValidation: true,\n\n // Enable/disable proactive token refresh\n // When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)\n enableProactiveRefresh: true,\n\n // Validate session when browser tab becomes visible again\n // Catches session deletions that happened while the tab was inactive\n validateOnVisibility: true,\n\n // ========== REFRESH TOKEN PERSISTENCE ==========\n // When true, stores refresh token in localStorage even on HTTPS\n // Required for local dev with mkcert/self-signed certs where httpOnly cookies\n // may not work reliably across origins\n // ⚠️ In true production, set to false and rely on httpOnly cookies\n persistRefreshToken: false,\n};\n\nexport function setConfig(customConfig = {}) {\n if (!customConfig.clientKey || !customConfig.authBaseUrl) {\n throw new Error('Missing required config: clientKey and authBaseUrl are required');\n }\n\n config = {\n ...config,\n ...customConfig,\n redirectUri: customConfig.redirectUri || window.location.origin + '/callback',\n // ✅ Auto-detect router mode\n isRouter: customConfig.isRouter || customConfig.clientKey === 'account-ui'\n };\n\n // ✅ Wire persistRefreshToken to token.js\n if (config.persistRefreshToken) {\n enableRefreshTokenPersistence(true);\n console.log('📦 Refresh token persistence ENABLED (localStorage on HTTPS)');\n }\n\n console.log(`🔧 Auth Client Mode: ${config.isRouter ? 'ROUTER' : 'CLIENT'}`, {\n clientKey: config.clientKey,\n isRouter: config.isRouter,\n persistRefreshToken: config.persistRefreshToken\n });\n}\n\nexport function getConfig() {\n return { ...config };\n}\n\n// ✅ Helper function\nexport function isRouterMode() {\n return config.isRouter;\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;;;ACC3B,OAAO,SAAS,eAAe,UAAU,WAAW,cAAc;;;ACClE,SAAS,iBAAiB;AAM1B,IAAM,iBAAiB,IAAI,KAAK,KAAK;;;ACDrC,IAAI,SAAS;AAAA,EACX,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,cAAc;AAAA,EACd,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,EAKV,oBAAoB;AAAA;AAAA;AAAA;AAAA,EAKpB,2BAA2B,KAAK,KAAK;AAAA;AAAA;AAAA,EAIrC,yBAAyB;AAAA;AAAA;AAAA,EAIzB,wBAAwB;AAAA;AAAA;AAAA,EAIxB,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtB,qBAAqB;AACvB;;;AF9BO,IAAM,cAAc,cAAc;;;ADTlC,SAAS,UAAU;AACxB,QAAM,UAAU,WAAW,WAAW;AACtC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AACA,SAAO;AACT;","names":[]}
|
|
@@ -35,55 +35,6 @@ module.exports = __toCommonJS(useSessionMonitor_exports);
|
|
|
35
35
|
var import_react_query = require("@tanstack/react-query");
|
|
36
36
|
var import_react3 = require("react");
|
|
37
37
|
|
|
38
|
-
// config.js
|
|
39
|
-
var config = {
|
|
40
|
-
clientKey: null,
|
|
41
|
-
authBaseUrl: null,
|
|
42
|
-
redirectUri: null,
|
|
43
|
-
accountUiUrl: null,
|
|
44
|
-
isRouter: false,
|
|
45
|
-
// ✅ Add router flag
|
|
46
|
-
// ========== SESSION SECURITY SETTINGS ==========
|
|
47
|
-
// Buffer time (in seconds) before token expiry to trigger proactive refresh
|
|
48
|
-
// With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX
|
|
49
|
-
tokenRefreshBuffer: 60,
|
|
50
|
-
// Interval (in milliseconds) for periodic session validation
|
|
51
|
-
// Validates that the session still exists in Keycloak (not deleted by admin)
|
|
52
|
-
// Default: 2 minutes (120000ms) - balances responsiveness vs server load
|
|
53
|
-
sessionValidationInterval: 2 * 60 * 1e3,
|
|
54
|
-
// Enable/disable periodic session validation
|
|
55
|
-
// When enabled, the client will ping the server to verify session is still active
|
|
56
|
-
enableSessionValidation: true,
|
|
57
|
-
// Enable/disable proactive token refresh
|
|
58
|
-
// When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)
|
|
59
|
-
enableProactiveRefresh: true,
|
|
60
|
-
// Validate session when browser tab becomes visible again
|
|
61
|
-
// Catches session deletions that happened while the tab was inactive
|
|
62
|
-
validateOnVisibility: true
|
|
63
|
-
};
|
|
64
|
-
function setConfig(customConfig = {}) {
|
|
65
|
-
if (!customConfig.clientKey || !customConfig.authBaseUrl) {
|
|
66
|
-
throw new Error("Missing required config: clientKey and authBaseUrl are required");
|
|
67
|
-
}
|
|
68
|
-
config = {
|
|
69
|
-
...config,
|
|
70
|
-
...customConfig,
|
|
71
|
-
redirectUri: customConfig.redirectUri || window.location.origin + "/callback",
|
|
72
|
-
// ✅ Auto-detect router mode
|
|
73
|
-
isRouter: customConfig.isRouter || customConfig.clientKey === "account-ui"
|
|
74
|
-
};
|
|
75
|
-
console.log(`\u{1F527} Auth Client Mode: ${config.isRouter ? "ROUTER" : "CLIENT"}`, {
|
|
76
|
-
clientKey: config.clientKey,
|
|
77
|
-
isRouter: config.isRouter
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
function getConfig() {
|
|
81
|
-
return { ...config };
|
|
82
|
-
}
|
|
83
|
-
function isRouterMode() {
|
|
84
|
-
return config.isRouter;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
38
|
// token.js
|
|
88
39
|
var import_jwt_decode = require("jwt-decode");
|
|
89
40
|
var accessToken = null;
|
|
@@ -183,8 +134,14 @@ function clearToken() {
|
|
|
183
134
|
});
|
|
184
135
|
}
|
|
185
136
|
var REFRESH_TOKEN_KEY = "auth_refresh_token";
|
|
186
|
-
|
|
137
|
+
var _persistRefreshToken = false;
|
|
138
|
+
function enableRefreshTokenPersistence(enabled) {
|
|
139
|
+
_persistRefreshToken = !!enabled;
|
|
140
|
+
console.log(`\u{1F527} Refresh token persistence: ${_persistRefreshToken ? "ENABLED" : "DISABLED"}`);
|
|
141
|
+
}
|
|
142
|
+
function shouldUseLocalStorage() {
|
|
187
143
|
var _a;
|
|
144
|
+
if (_persistRefreshToken) return true;
|
|
188
145
|
try {
|
|
189
146
|
return typeof window !== "undefined" && ((_a = window.location) == null ? void 0 : _a.protocol) === "http:";
|
|
190
147
|
} catch (err) {
|
|
@@ -196,10 +153,10 @@ function setRefreshToken(token) {
|
|
|
196
153
|
clearRefreshToken();
|
|
197
154
|
return;
|
|
198
155
|
}
|
|
199
|
-
if (
|
|
156
|
+
if (shouldUseLocalStorage()) {
|
|
200
157
|
try {
|
|
201
158
|
localStorage.setItem(REFRESH_TOKEN_KEY, token);
|
|
202
|
-
console.log(
|
|
159
|
+
console.log(`\u{1F4E6} Refresh token stored in localStorage (${_persistRefreshToken ? "persistence enabled" : "HTTP dev mode"})`);
|
|
203
160
|
} catch (err) {
|
|
204
161
|
console.warn("Could not store refresh token:", err);
|
|
205
162
|
}
|
|
@@ -208,7 +165,7 @@ function setRefreshToken(token) {
|
|
|
208
165
|
}
|
|
209
166
|
}
|
|
210
167
|
function getRefreshToken() {
|
|
211
|
-
if (
|
|
168
|
+
if (shouldUseLocalStorage()) {
|
|
212
169
|
try {
|
|
213
170
|
const token = localStorage.getItem(REFRESH_TOKEN_KEY);
|
|
214
171
|
return token;
|
|
@@ -250,6 +207,66 @@ function getListenerCount() {
|
|
|
250
207
|
return listeners.size;
|
|
251
208
|
}
|
|
252
209
|
|
|
210
|
+
// config.js
|
|
211
|
+
var config = {
|
|
212
|
+
clientKey: null,
|
|
213
|
+
authBaseUrl: null,
|
|
214
|
+
redirectUri: null,
|
|
215
|
+
accountUiUrl: null,
|
|
216
|
+
isRouter: false,
|
|
217
|
+
// ✅ Add router flag
|
|
218
|
+
// ========== SESSION SECURITY SETTINGS ==========
|
|
219
|
+
// Buffer time (in seconds) before token expiry to trigger proactive refresh
|
|
220
|
+
// With 5-minute access tokens, refreshing 60s before expiry ensures seamless UX
|
|
221
|
+
tokenRefreshBuffer: 60,
|
|
222
|
+
// Interval (in milliseconds) for periodic session validation
|
|
223
|
+
// Validates that the session still exists in Keycloak (not deleted by admin)
|
|
224
|
+
// Default: 15 minutes (900000ms) - Increased from 2m to avoid frequent checks
|
|
225
|
+
sessionValidationInterval: 15 * 60 * 1e3,
|
|
226
|
+
// Enable/disable periodic session validation
|
|
227
|
+
// When enabled, the client will ping the server to verify session is still active
|
|
228
|
+
enableSessionValidation: true,
|
|
229
|
+
// Enable/disable proactive token refresh
|
|
230
|
+
// When enabled, tokens are refreshed before they expire (using tokenRefreshBuffer)
|
|
231
|
+
enableProactiveRefresh: true,
|
|
232
|
+
// Validate session when browser tab becomes visible again
|
|
233
|
+
// Catches session deletions that happened while the tab was inactive
|
|
234
|
+
validateOnVisibility: true,
|
|
235
|
+
// ========== REFRESH TOKEN PERSISTENCE ==========
|
|
236
|
+
// When true, stores refresh token in localStorage even on HTTPS
|
|
237
|
+
// Required for local dev with mkcert/self-signed certs where httpOnly cookies
|
|
238
|
+
// may not work reliably across origins
|
|
239
|
+
// ⚠️ In true production, set to false and rely on httpOnly cookies
|
|
240
|
+
persistRefreshToken: false
|
|
241
|
+
};
|
|
242
|
+
function setConfig(customConfig = {}) {
|
|
243
|
+
if (!customConfig.clientKey || !customConfig.authBaseUrl) {
|
|
244
|
+
throw new Error("Missing required config: clientKey and authBaseUrl are required");
|
|
245
|
+
}
|
|
246
|
+
config = {
|
|
247
|
+
...config,
|
|
248
|
+
...customConfig,
|
|
249
|
+
redirectUri: customConfig.redirectUri || window.location.origin + "/callback",
|
|
250
|
+
// ✅ Auto-detect router mode
|
|
251
|
+
isRouter: customConfig.isRouter || customConfig.clientKey === "account-ui"
|
|
252
|
+
};
|
|
253
|
+
if (config.persistRefreshToken) {
|
|
254
|
+
enableRefreshTokenPersistence(true);
|
|
255
|
+
console.log("\u{1F4E6} Refresh token persistence ENABLED (localStorage on HTTPS)");
|
|
256
|
+
}
|
|
257
|
+
console.log(`\u{1F527} Auth Client Mode: ${config.isRouter ? "ROUTER" : "CLIENT"}`, {
|
|
258
|
+
clientKey: config.clientKey,
|
|
259
|
+
isRouter: config.isRouter,
|
|
260
|
+
persistRefreshToken: config.persistRefreshToken
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function getConfig() {
|
|
264
|
+
return { ...config };
|
|
265
|
+
}
|
|
266
|
+
function isRouterMode() {
|
|
267
|
+
return config.isRouter;
|
|
268
|
+
}
|
|
269
|
+
|
|
253
270
|
// core.js
|
|
254
271
|
var callbackProcessed = false;
|
|
255
272
|
function login(clientKeyArg, redirectUriArg) {
|
|
@@ -385,9 +402,10 @@ function handleCallback() {
|
|
|
385
402
|
setToken(accessToken2);
|
|
386
403
|
const refreshTokenInUrl = params.get("refresh_token");
|
|
387
404
|
if (refreshTokenInUrl) {
|
|
405
|
+
const { persistRefreshToken } = getConfig();
|
|
388
406
|
const isHttpDev = typeof window !== "undefined" && ((_a = window.location) == null ? void 0 : _a.protocol) === "http:";
|
|
389
|
-
if (isHttpDev) {
|
|
390
|
-
console.log(
|
|
407
|
+
if (persistRefreshToken || isHttpDev) {
|
|
408
|
+
console.log(`\u{1F4E6} Storing refresh token from callback URL (${persistRefreshToken ? "persistence enabled" : "HTTP dev mode"})`);
|
|
391
409
|
setRefreshToken(refreshTokenInUrl);
|
|
392
410
|
} else {
|
|
393
411
|
console.log("\u{1F512} HTTPS mode: Refresh token is in httpOnly cookie (ignoring URL param)");
|