UNPKG

bento-auth-js

Version:

Authentication library for web applications of Bento-Platform

201 lines 10.9 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { useCallback, useEffect, useMemo, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useBentoAuthContext } from "./contexts"; import { makeResourceKey } from "./resources"; import { fetchResourcesPermissions, refreshTokens, tokenHandoff } from "./redux/authSlice"; import { LS_SIGN_IN_POPUP, createAuthURL } from "./performAuth"; import { fetchOpenIdConfigurationIfNecessary } from "./redux/openIdConfigSlice"; import { getIsAuthenticated, logMissingAuthContext, makeAuthorizationHeader } from "./utils"; const AUTH_RESULT_TYPE = "authResult"; export const useAuthState = () => useSelector((state) => state.auth); export const useIsAuthenticated = () => { const { idTokenContents } = useAuthState(); return getIsAuthenticated(idTokenContents); }; export const useAccessToken = () => useAuthState().accessToken; export const useAuthorizationHeader = () => { const accessToken = useAccessToken(); return useMemo(() => makeAuthorizationHeader(accessToken), [accessToken]); }; export const useIsAutoAuthenticating = () => useAuthState().isAutoAuthenticating; export const useResourcesPermissions = (resources, authzUrl) => { const dispatch = useDispatch(); const keys = useMemo(() => resources.map((resource) => makeResourceKey(resource)), [resources]); const { resourcePermissions, isHandingOffCodeForToken } = useAuthState(); useEffect(() => { const anyFetching = keys.some((key) => { var _a; return !!((_a = resourcePermissions[key]) === null || _a === void 0 ? void 0 : _a.isFetching); }); const allHavePermissions = keys.every((key) => { var _a, _b; return !!((_b = (_a = resourcePermissions[key]) === null || _a === void 0 ? void 0 : _a.permissions) === null || _b === void 0 ? void 0 : _b.length); }); const allAttempted = keys.every((key) => { var _a; return !!((_a = resourcePermissions[key]) === null || _a === void 0 ? void 0 : _a.hasAttempted); }); // If the sign-in process is ongoing (code-token exchange), or any permissions are currently fetching, or // all requested permissions have already been tried/returned, we don't need to dispatch the fetch action: if (!authzUrl || isHandingOffCodeForToken || anyFetching || allHavePermissions || allAttempted) return; dispatch(fetchResourcesPermissions({ resources, authzUrl })); }, [dispatch, keys, resources, resourcePermissions, authzUrl, isHandingOffCodeForToken]); // Construct an object with resource keys yielding an object containing the permissions on the object return useMemo(() => Object.fromEntries(keys.map((key) => { var _a; const { permissions, isFetching, hasAttempted, error } = (_a = resourcePermissions[key]) !== null && _a !== void 0 ? _a : {}; return [ key, { permissions: permissions !== null && permissions !== void 0 ? permissions : [], isFetching: isFetching !== null && isFetching !== void 0 ? isFetching : false, hasAttempted: hasAttempted !== null && hasAttempted !== void 0 ? hasAttempted : false, error: error !== null && error !== void 0 ? error : "", }, ]; })), [keys, resourcePermissions]); }; export const useResourcePermissions = (resource, authzUrl) => { const key = makeResourceKey(resource); const resourcesPermissions = useResourcesPermissions([resource], authzUrl); return resourcesPermissions[key]; }; export const useHasResourcePermission = (resource, authzUrl, permission) => { var _a; const _b = (_a = useResourcePermissions(resource, authzUrl)) !== null && _a !== void 0 ? _a : {}, { permissions } = _b, props = __rest(_b, ["permissions"]); return Object.assign(Object.assign({}, props), { hasPermission: permissions.includes(permission) }); }; export const useOpenIdConfig = () => { const dispatch = useDispatch(); const { openIdConfigUrl } = useBentoAuthContext(); useEffect(() => { if (!openIdConfigUrl) { logMissingAuthContext("openIdConfigUrl"); return; } dispatch(fetchOpenIdConfigurationIfNecessary(openIdConfigUrl)); }, [dispatch, openIdConfigUrl]); return useSelector((state) => state.openIdConfiguration); }; export const useSignInPopupTokenHandoff = (windowMessageHandler) => { const dispatch = useDispatch(); const { applicationUrl, authCallbackUrl, clientId } = useBentoAuthContext(); useEffect(() => { if (!applicationUrl || !authCallbackUrl || !clientId) { logMissingAuthContext("applicationUrl", "authCallbackUrl", "clientId"); } else { windowMessageHandler.current = (e) => { var _a, _b; if (e.origin !== applicationUrl) return; if (((_a = e.data) === null || _a === void 0 ? void 0 : _a.type) !== AUTH_RESULT_TYPE) return; const { code, verifier } = (_b = e.data) !== null && _b !== void 0 ? _b : {}; if (!code || !verifier) return; localStorage.removeItem(LS_SIGN_IN_POPUP); dispatch(tokenHandoff({ code, verifier, clientId, authCallbackUrl })); }; window.addEventListener("message", windowMessageHandler.current); } // Listener cleanup return () => { if (windowMessageHandler.current) { window.removeEventListener("message", windowMessageHandler.current); } }; }, [dispatch, applicationUrl, authCallbackUrl, clientId, windowMessageHandler]); }; export const useSessionWorkerTokenRefresh = (sessionWorkerRef, createWorker, fetchUserDependentData = undefined) => { const dispatch = useDispatch(); const { clientId } = useBentoAuthContext(); const { refreshToken } = useAuthState(); const refreshTokenRef = useRef(refreshToken); useEffect(() => { // A bit hacky: we use a ref to get the refreshToken into the worker event listener without triggering a // dependency change for the useEffect below. refreshTokenRef.current = refreshToken; }, [refreshToken]); useEffect(() => { if (!clientId) { logMissingAuthContext("clientId"); } else { if (!sessionWorkerRef.current) { const sw = createWorker(); sw.addEventListener("message", () => { // It would be nice to check if we have a refresh token here without refs, but doing so would mean // unbinding and re-binding the listener every time the effect is re-executed. Instead, we can use a // ref to access the token without triggering a hook dependency change. // While the action itself also handles the no refresh token case, it pollutes the Redux and console // logs and so it's nicer to re-check here. if (refreshTokenRef.current) dispatch(refreshTokens(clientId)); if (fetchUserDependentData) fetchUserDependentData(); }); sessionWorkerRef.current = sw; } } return () => { if (sessionWorkerRef.current) { sessionWorkerRef.current.terminate(); sessionWorkerRef.current = null; } }; }, [dispatch, createWorker, fetchUserDependentData, clientId, sessionWorkerRef]); }; export const useOpenSignInWindowCallback = (signInWindow, windowFeatures = "scrollbars=no, toolbar=no, menubar=no, width=800, height=600") => { const { clientId, authCallbackUrl } = useBentoAuthContext(); const { data: openIdConfig } = useOpenIdConfig(); return useCallback(() => { if (!clientId || !authCallbackUrl) { logMissingAuthContext("clientId", "authCallbackUrl"); return; } if (signInWindow.current && !signInWindow.current.closed) { signInWindow.current.focus(); return; } if (!openIdConfig || !window.top) return; const popupTop = window.top.outerHeight / 2 + window.top.screenY - 350; const popupLeft = window.top.outerWidth / 2 + window.top.screenX - 400; (() => __awaiter(void 0, void 0, void 0, function* () { localStorage.setItem(LS_SIGN_IN_POPUP, "true"); signInWindow.current = window.open(yield createAuthURL(openIdConfig["authorization_endpoint"], clientId, authCallbackUrl), "Bento Sign In", `${windowFeatures}, top=${popupTop}, left=${popupLeft}`); }))(); }, [openIdConfig, clientId, authCallbackUrl, windowFeatures, signInWindow]); }; export const usePopupOpenerAuthCallback = () => { const { applicationUrl } = useBentoAuthContext(); return useCallback((code, verifier) => __awaiter(void 0, void 0, void 0, function* () { if (!applicationUrl) { logMissingAuthContext("applicationUrl"); return; } if (!window.opener) return; // We're inside a popup window for authentication // Send the code and verifier to the main thread/page for authentication // IMPORTANT SECURITY: provide BENTO_URL as the target origin: window.opener.postMessage({ type: "authResult", code, verifier }, applicationUrl); // We're inside a popup window which has successfully re-authenticated the user, meaning we need to // close ourselves to return focus to the original window. window.close(); }), [applicationUrl]); }; //# sourceMappingURL=hooks.js.map