@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
JavaScript
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
};