bento-auth-js
Version:
Authentication library for web applications of Bento-Platform
201 lines • 10.9 kB
JavaScript
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