UNPKG

@trimble-oss/trimble-id-react

Version:

> **Important Notice:** > > As of version 1.0.0, `PersistentOptions` have been removed. By default, the SDK now supports in-memory token storage. > > When you upgrade to version 1.x, storage options will no longer be available, resulting in a breaking

718 lines (717 loc) 24.7 kB
var $ = Object.defineProperty; var B = (i, e, t) => e in i ? $(i, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : i[e] = t; var l = (i, e, t) => (B(i, typeof e != "symbol" ? e + "" : e, t), t); import { OpenIdEndpointProvider as J, AuthorizationCodeGrantTokenProvider as R, AnalyticsHttpClient as H, BearerTokenHttpClientProvider as Q } from "@trimble-oss/trimble-id"; import * as T from "es-cookie"; import X from "jwt-decode"; import { createContext as q, useContext as L, useState as z, useReducer as Y, useRef as Z, useMemo as M, useEffect as N, useCallback as m } from "react"; import { jsx as V, Fragment as j } from "react/jsx-runtime"; class ee { constructor() { /** * This function generate a encapsulation function to store * the user and token information in a secure way disabled the * access from the outside * @see {https://medium.com/javascript-scene/encapsulation-in-javascript-26be60e325b4} Encapsulation * @return {CacheStorage} Key for the token */ l(this, "generateCache", function() { const e = { token: void 0, user: void 0 }; return { /** * Get token store in cache * @return {Promise<TIDAuthToken | undefined>} Token store in memory */ async getToken() { return e.token; }, /** * Get user store in cache * @return {Promise<TIDUser | undefined>} User store in memory */ async getUser() { return e.user; }, /** * Store token in cache * @param {TIDAuthToken} token - Token that you want to store in cache * @return {Promise<void>} Empty promise */ async storeToken(t) { e.token = t; }, /** * Store user in cache * @param {TIDUser} user - User that you want to store in cache * @return {Promise<void>} Empty promise */ async storeUser(t) { e.user = t; }, /** * The clear the cache from the storage * @return {Promise<void>} Empty promise */ clear() { return e.token = void 0, e.user = void 0, Promise.resolve(void 0); } }; }()); } } class te { constructor() { /** * Cache option selected * @type {CacheStorage} */ l(this, "cacheStorage"); this.cacheStorage = new ee().generateCache; } /** * Store token in cache * @param {TIDAuthToken} token - Token that you want to store in cache * @return {Promise<void>} Empty promise */ async setToken(e) { await this.cacheStorage.storeToken(e); } /** * Store user in cache * @param {TIDUser} user - User that you want to store in cache * @return {Promise<void>} Empty promise */ async setUser(e) { await this.cacheStorage.storeUser(e); } /** * Get user store in cache * @return {Promise<TIDUser | undefined>} Key for the user */ async getUser() { return this.cacheStorage.getUser(); } /** * Get token store in cache * @return {Promise<TIDAuthToken | undefined>} Key for the user */ async getToken() { return this.cacheStorage.getToken(); } /** * The clear the cache from the storage * @return {Promise<void>} Empty promise */ async clear() { await this.cacheStorage.clear(); } } const x = 5 * 6e4, ie = ["code", "state"], A = 10 * 60 * 1e3, U = (i) => i.split("?")[0], re = (i) => i.split("?")[1], b = (i) => { let e = i; if (!i.startsWith("?")) try { e = new URL(i).search; } catch { } return new URLSearchParams(e); }, ne = (i = window.location.href, e) => { if (e != null) { const r = U(e), n = U(i ?? ""); if (r !== n) return !1; } const t = b(i); for (const r of ie) if (!t.has(r)) return !1; return !0; }, ae = (i) => ({ id: i.sub, name: `${i.given_name} ${i.family_name}`, given_name: i.given_name, family_name: i.family_name, picture: i.picture, email: i.email, email_verified: i.email_verified }), oe = "@TID_COOKIE"; class se { /** * Retrieve a cookie from the browser * @param {string} key - Key to retrieve the cookies */ get(e) { const t = T.get(e); if (t == null) throw new Error("Cookie not found"); return JSON.parse(t); } /** * Store cookies in the browser * @param {string} key - Key to save and retrieve the cookies * @param {any} value - Information you want to store in cookies * @param {CookiesOptions} options - Additional options you want to add to the cookies. * @example Save cookies with one day of expiration * cookiesStorage.set('key',{data:{...}},{expires:1}) * @example Save cookies with a custom domain * cookiesStorage.set('key',{data:{...}},{domain:'https://example.com/subpath'}) */ set(e, t, r) { let n = {}; window.location.protocol === "https:" && (n = { secure: !0, sameSite: "none" }), r != null && r.expires && (n.expires = r.expires), r != null && r.domain && (n.domain = r.domain), T.set(e, JSON.stringify(t), n); } /** * Remove a cookie from the browser * @param {string} key - Key to remove the cookies from the browser * @param {CookiesOptions} options - Additional options used when the cookie was declared * @example Remove cookies without additional information * cookiesStorage.remove('key') * @example Remove cookies with custom domain * cookiesStorage.remove('key',{domain:'https://example.com/subpath'}) */ remove(e, t) { const r = {}; t != null && t.domain && (r.domain = t.domain), T.remove(e, r); } } class ce { /** * Create a cookies manager to extract or save cookies in the browser * @param {CookiesManagerOptions} options - Configuration for the managing the cookies */ constructor(e) { /** * Cookie full key to store and retrieve the information from cookies * @type {string} */ l(this, "cookieKey"); /** * Cookie storage. This object contain all functions necessary to retrieve and store tokens using cookies * @type {CookiesStorage} */ l(this, "cookiesStorage"); this.cookieKey = `${oe}.${e.clientId}`, this.cookiesStorage = new se(); } /** * Store cookies in the browser * @param {Partial<CookieValue>} value - information to store */ save(e) { const r = { ...this.get() || {}, ...e }; this.cookiesStorage.set(this.cookieKey, r, { expires: 1 }); } /** * Retrieve state payload from cookies * @return {string | undefined} - State payload from cookies */ getStatePayload() { const e = this.get(); return e == null ? void 0 : e.state_payload; } /** * Clear state payload from cookies */ clearStatePayload() { const e = this.get(); e && (delete e.state_payload, this.cookiesStorage.set(this.cookieKey, e, { expires: 1 })); } /** * Retrieve cookies in the browser * @return {CookieValue | undefined} - Cookies from the browser */ get() { try { return this.cookiesStorage.get(this.cookieKey); } catch { return; } } /** * Clear information about the cookies stored in the browser */ clear() { this.cookiesStorage.remove(this.cookieKey); } } class O extends Error { } class D extends Error { } class le extends Error { } const u = "@trimble-oss/trimble-id-react", g = "1.0.1", de = { configurationEndpoint: "", clientId: "", redirectUrl: "", logoutRedirectUrl: "", scopes: [] }; class he { /** * Create a TID client to handle manage all user authentication functions and information * @param {CacheManagerOptions} props - TID client configuration */ constructor(e) { /** * Token provider SDK. This object handles all necessary communication with TID * @type {AuthorizationCodeGrantTokenProvider} */ l(this, "tokenProvider"); /** * This object manage all caching, and all configurations necessary * @type {CacheManager} */ l(this, "cacheManager"); /** * This object manage all cookies administration, and all configurations necessary * @type {CookiesManager} */ l(this, "cookiesManager"); /** * Client id of the application created in trimble developer console * @type {string} */ l(this, "clientId"); /** * Callback url to redirect the user after the authentication is successful * @type {string} */ l(this, "redirectUrl"); /** * AnalyticsHttpClient for sending events * @type {AnalyticsHttpClient} */ l(this, "analyticshttpclient"); const { config: t = de } = e; if (this.redirectUrl = t.redirectUrl, t.configurationEndpoint == null || t.configurationEndpoint == "") throw new Error("Configuration endpoint not defined"); if (t.clientId == null || t.clientId == "") throw new Error("Consumer key is not defined"); this.cookiesManager = new ce({ clientId: t.clientId }); const r = this.cookiesManager.get(), n = new J(t.configurationEndpoint); let s = new R( n, t.clientId, t.redirectUrl ).WithScopes(t.scopes); t.logoutRedirectUrl && (s = s.WithLogoutRedirect(t.logoutRedirectUrl)), (r == null ? void 0 : r.code_verifier) != null && (s = s.WithProofKeyForCodeExchange(r.code_verifier)), this.tokenProvider = s, this.cacheManager = new te(), this.clientId = t.clientId, this.analyticshttpclient = H, this.analyticshttpclient.sendInitEvent("TIDClient", this.clientId, u, g), this.cleanupExpiredState(); } /** * Clean up expired state payloads to prevent storage bloat */ cleanupExpiredState() { try { const e = this.cookiesManager.getStatePayload(); if (e) { const t = atob(e), r = JSON.parse(t); Date.now() - r.timestamp > A && this.cookiesManager.clearStatePayload(); } } catch { this.cookiesManager.clearStatePayload(); } } /** * Redirect the user to TID using the browser * @param {LoginWithRedirectOptions} options - Custom configuration for the redirection * @return {Promise<void>} Empty promise * @example No configuration * loginWithRedirect() * // Automatically redirects the user to TID with all necessary parameters * // After authentication, user will be redirected back to the current page * @example Custom redirect * loginWithRedirect({onRedirect: (url) => router.navigate(url)}) * // Redirect calls onRedirect with the log-out url for TID * // So it can be handled by the developer */ async loginWithRedirect(e) { this.analyticshttpclient.sendMethodEvent(this.loginWithRedirect.name, this.clientId, u, g); const { onRedirect: t } = e || {}, r = window.location.pathname + window.location.search, n = this.createStatePayload(r); this.cookiesManager.save({ state_payload: n }); const s = R.GenerateCodeVerifier(); this.cookiesManager.save({ code_verifier: s }), this.tokenProvider = this.tokenProvider.WithProofKeyForCodeExchange(s); const c = await this.tokenProvider.GetOAuthRedirect(n); t != null ? t(c) : window.location.assign(c); } /** * Authenticated the user using the url callback params * @param {string} url - Custom configuration for the redirection * @return {Promise<AuthState>} Object contain the state returned from TID and redirect path * @throws {CodeVerifierNotFoundException} Will throw an exception if the session doesn't contain the code verifier * @throws {Error} Will throw an exception if state validation fails or replay attack is detected * @example No configuration * handleCallback() * // Will automatically take the url from the browser and try to log in the user * @example Custom url * handleCallback('https://example.com?code=code....') * // Will try to log in the user with the url assign by the developer */ async handleCallback(e = window.location.href) { const t = this.cookiesManager.get(); if (t == null || (t == null ? void 0 : t.code_verifier) == null) throw new le("Code verifier not available"); const r = re(e), n = b(e), s = n.get("identity_provider") ?? "", c = n.get("state") ?? "", d = this.validateStatePayload(c); if (!d.isValid) throw new Error(`State validation failed: ${d.error}`); return await this.tokenProvider.ValidateQuery(r), await this.generateToken(s), { authState: c, returnTo: d.redirectTo }; } /** * Function to generate and store the access token * @param {string} identityProvider - Type of identity provider used * @return {Promise<void>} Empty promise */ async generateToken(e) { var a, k; const t = await this.tokenProvider.RetrieveToken(), r = await this.tokenProvider.RetrieveRefreshToken(), n = await this.tokenProvider.RetrieveIdToken(), s = await this.tokenProvider.RetrieveTokenExpiry(), d = new Date(s).getTime(), f = (k = (a = this.tokenProvider) == null ? void 0 : a._scopes) == null ? void 0 : k.join(" "); await this.cacheManager.setToken({ scope: f, state: "", session_state: "", identity_provider: e, token_type: "bearer", access_token: t, refresh_token: r, id_token: n, expires_at: d }); const v = X(n), p = ae(v); await this.cacheManager.setUser(p), await this.reloadCodeVerifier(); } async reloadCodeVerifier() { const e = await this.tokenProvider.RetrieveCodeVerifier(); this.cookiesManager.save({ code_verifier: e }), this.tokenProvider = this.tokenProvider.WithProofKeyForCodeExchange(e); } /** * Create and encode state payload for replay attack protection * @param {string} redirectTo - Path to redirect to after authentication * @return {string} - Base64 encoded state payload */ createStatePayload(e) { const t = this.generateNonce(), r = Date.now(); return btoa(JSON.stringify({ redirectTo: e, timestamp: r, nonce: t })); } /** * Generate a random nonce for state validation * @return {string} - Random nonce */ generateNonce() { const e = new Uint8Array(16); return crypto.getRandomValues(e), Array.from(e, (t) => t.toString(16).padStart(2, "0")).join(""); } /** * Validate state payload to prevent replay attacks * @param {string} receivedState - State received from OAuth callback * @return {StateValidationResult} - Validation result with redirect path if valid */ validateStatePayload(e) { try { if (!e || e.trim() === "") return { isValid: !1, error: "Empty state parameter" }; const t = this.cookiesManager.getStatePayload(); if (!t) return { isValid: !1, error: "No stored state found" }; const r = atob(e), n = JSON.parse(r), s = atob(t), c = JSON.parse(s); if (!n.nonce || !n.timestamp || !n.redirectTo) return { isValid: !1, error: "Invalid state payload structure" }; if (n.nonce !== c.nonce) return { isValid: !1, error: "State nonce mismatch" }; const f = Date.now() - n.timestamp; return f > A ? { isValid: !1, error: "State expired - possible replay attack" } : f < 0 ? { isValid: !1, error: "State timestamp is in the future - possible replay attack" } : (this.cookiesManager.clearStatePayload(), { isValid: !0, redirectTo: n.redirectTo }); } catch { return { isValid: !1, error: "Invalid state format" }; } } /** * Return the user stored in cache * @return {Promise<TIDUser | undefined>} User in cache */ async getUser() { return this.analyticshttpclient.sendMethodEvent(this.getUser.name, this.clientId, u, g), await this.cacheManager.getUser(); } /** * Gets the access token from cache. If the token already expired, * will try to refresh it using the refresh token * @return {Promise<string>} Access token * @throws {TokenNotFoundException} Will throw an exception if the access token is not in cache * @throws {TokenExpiredException} Will throw an exception if the user token expired */ async getAccessTokenSilently() { this.analyticshttpclient.sendMethodEvent(this.getAccessTokenSilently.name, this.clientId, u, g); let e = await this.cacheManager.getToken(); if (e == null) throw this.analyticshttpclient.sendExceptionEvent(this.getAccessTokenSilently.name, "No token available", this.clientId, u, g), new D("No token available"); const t = new Date((/* @__PURE__ */ new Date()).getTime() + x); if ((e == null ? void 0 : e.expires_at) == null || (e == null ? void 0 : e.expires_at) < t.getTime()) { try { await this.tokenProvider.RetrieveToken(); } catch (r) { throw this.analyticshttpclient.sendExceptionEvent(this.getAccessTokenSilently.name, r.message, this.clientId, u, g), new O(r.message); } await this.generateToken((e == null ? void 0 : e.identity_provider) ?? ""), e = await this.cacheManager.getToken(); } return (e == null ? void 0 : e.access_token) || ""; } /** * Retrieves token details from the cache, including the access token, ID token, and expiration time. * If the token already expired, will try to refresh it using the refresh token. * @return {Promise<TokenResponse>} Token response * @throws {TokenNotFoundException} Will throw an exception if there are no tokens in cache * @throws {TokenExpiredException} Will throw an exception if the user token expired */ async getTokens() { this.analyticshttpclient.sendMethodEvent(this.getTokens.name, this.clientId, u, g); let e = await this.cacheManager.getToken(); if (e == null) throw this.analyticshttpclient.sendExceptionEvent(this.getTokens.name, "No token available", this.clientId, u, g), new D("No token available"); const t = new Date((/* @__PURE__ */ new Date()).getTime() + x); if ((e == null ? void 0 : e.expires_at) == null || (e == null ? void 0 : e.expires_at) < t.getTime()) { try { await this.tokenProvider.RetrieveToken(); } catch (r) { throw this.analyticshttpclient.sendExceptionEvent(this.getTokens.name, r.message, this.clientId, u, g), new O(r.message); } await this.generateToken((e == null ? void 0 : e.identity_provider) ?? ""), e = await this.cacheManager.getToken(); } return { access_token: (e == null ? void 0 : e.access_token) || "", expires_at: (e == null ? void 0 : e.expires_at) || 0, id_token: (e == null ? void 0 : e.id_token) || "" }; } /** * Redirect the user to TID using the browser * @param {LogoutOptions} options - Custom configuration for teh redirection * @return {Promise<void>} Empty promise * @example No configuration * logout() * // Automatically redirects the user to TID to log out * @example Custom redirect * logout({onRedirect: (url) => router.navigate(url)}) * // Redirect calls onRedirect with the log-out url for TID * // So it can be handled by the developer */ async logout(e) { this.analyticshttpclient.sendMethodEvent(this.logout.name, this.clientId, u, g); const { onRedirect: t, disabledAutoRedirect: r } = e || {}; this.cacheManager && await this.cacheManager.clear(), this.cookiesManager && this.cookiesManager.clear(); const n = await this.tokenProvider.GetOAuthLogoutRedirect("state"); if (t != null) return t(n); r || window.location.assign(n); } /** * Check if the user still has a valid session * @return {Promise<boolean>} True or false depending on if the user is session is valid or not */ async checkSession() { try { await this.getAccessTokenSilently(); } catch { return !1; } return !0; } /** * Check if the user has a session valid after a refresh * @return {Promise<void>} Empty promise */ async loadUserSession() { await this.loadCacheSessionIntoSDK(), await this.checkSession() || await this.cacheManager.clear(); } /** * Load the user session from cache into the SDK * @return {Promise<void>} Empty promise */ async loadCacheSessionIntoSDK() { const e = await this.cacheManager.getToken(); if (e == null) return; const t = e.access_token, r = e.refresh_token || "", n = e.id_token, s = e.expires_at; this.tokenProvider = this.tokenProvider.WithAccessToken(t, s).WithRefreshToken(r).WithIdToken(n); } /** * Get a http bearer token client to use it for another SDK (Ex: Processing framework) * @return {any} - BearerTokenHttpClientProvider */ getBearerTokenHttpClient(e) { return new Q(this.tokenProvider, e); } /** * Get the redirect url to TID * @return {string} - Callback redirect url */ getRedirectUrl() { return this.redirectUrl; } } const w = q(null), K = () => L(w) ?? {}, ue = (i, e) => { switch (e.type) { case "INIT": return { ...i, isLoading: !1, isAuthenticated: e.user != null, user: e.user, error: void 0 }; case "GET_TOKENS_COMPLETE": case "HANDLE_CALLBACK_COMPLETE": case "GET_ACCESS_TOKEN_COMPLETE": return { ...i, isLoading: !1, isAuthenticated: e.user != null, user: e.user, error: void 0 }; case "LOGOUT": return { ...i, isAuthenticated: !1, user: void 0 }; case "ERROR": return { ...i, isLoading: !1, error: e.error }; } }, ge = { isLoading: !0, isAuthenticated: !1 }, fe = (i) => { if (i == null || Object.keys(i).length <= 0) return !0; const e = Object.keys(i); for (const t of e) if (i[t] != null && i[t] !== "") return !1; return !0; }, ye = (i) => !fe(i.config), ke = (i) => { const { children: e, configurationEndpoint: t, clientId: r, redirectUrl: n, logoutRedirectUrl: s, scopes: c, onRedirectCallback: d, checkRedirectUrlMatch: f } = i; if (L(w) != null) throw new Error("TID Provider already defined"); const p = { config: { configurationEndpoint: t ?? "", clientId: r ?? "", redirectUrl: n ?? "", logoutRedirectUrl: s ?? "", scopes: c ?? [""] } }, [a] = z(i.tidClient ?? new he(p)), [k, y] = Y(ue, ge), E = Z(!1), W = M( () => f ? a.getRedirectUrl() : void 0, [f, a] ); N(() => { E.current || (i.tidClient != null && ye({ config: { configurationEndpoint: t, clientId: r, redirectUrl: n, logoutRedirectUrl: s, scopes: c } }) && console.warn( "When TID client is pass as prop, any client configuration property sent directly to the TID Provider component will be ignored" ), E.current = !0, (async () => { try { let o; if (ne(void 0, W)) { const h = await a.handleCallback(); o = await a.getUser(), d != null && d(h); } else await a.loadUserSession(), o = await a.getUser(); y({ type: "INIT", user: o }); } catch (o) { let h = o; typeof o == "string" && (h = new Error(o)), y({ type: "ERROR", error: h }); } })()); }, [a, d]); const S = m(async () => { const o = await a.getAccessTokenSilently(), h = await a.getUser(); return y({ type: "GET_ACCESS_TOKEN_COMPLETE", user: h }), o; }, [a]), P = m(async () => { const o = await a.getTokens(), h = await a.getUser(); return y({ type: "GET_TOKENS_COMPLETE", user: h }), o; }, [a]), _ = m( async (o) => { await a.loginWithRedirect(o); }, [a] ), C = m( async (o) => { await a.logout(o), (o == null ? void 0 : o.disabledAutoRedirect) != null && o.disabledAutoRedirect && y({ type: "LOGOUT" }); }, [a] ), I = m( async (o) => { const h = await a.handleCallback(o), F = await a.getUser(); return y({ type: "HANDLE_CALLBACK_COMPLETE", user: F }), h; }, [a] ), G = M( () => ({ ...k, getAccessTokenSilently: S, getTokens: P, loginWithRedirect: _, handleCallback: I, logout: C }), [k, S, P, _, I, C] ); return /* @__PURE__ */ V(w.Provider, { value: G, children: e }); }, Ee = K, Se = ke, Pe = ({ renderComponent: i, loader: e }) => { const { isAuthenticated: t, isLoading: r, loginWithRedirect: n } = K(); return N(() => { !r && !t && (async () => await n())(); }, [r, t, n]), t ? i : e || /* @__PURE__ */ V(j, {}); }; export { Pe as AuthenticationGuard, he as TIDClient, w as TIDContext, Se as TIDProvider, Ee as useAuth };