UNPKG

bento-auth-js

Version:

Authentication library for web applications of Bento-Platform

206 lines 10.5 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()); }); }; import { createAction, createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { decodeJwt } from "jose"; import { buildUrlEncodedData, makeAuthorizationHeader } from "../utils"; import { LS_BENTO_WAS_SIGNED_IN, setLSNotSignedIn } from "../performAuth"; import { makeResourceKey } from "../resources"; const missingOpenIdConfig = { error: "Error while attempting to perform the token handoff", error_description: "No token endpoint available/No openIdConfiguration data", }; export const tokenHandoff = createAsyncThunk("auth/TOKEN_HANDOFF", (handoffParams_1, _a) => __awaiter(void 0, [handoffParams_1, _a], void 0, function* (handoffParams, { getState, rejectWithValue }) { var _b; const state = getState(); const url = (_b = state.openIdConfiguration.data) === null || _b === void 0 ? void 0 : _b.token_endpoint; if (!url) return rejectWithValue(missingOpenIdConfig); const response = yield fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: buildUrlEncodedData({ grant_type: "authorization_code", code: handoffParams.code, client_id: handoffParams.clientId, redirect_uri: handoffParams.authCallbackUrl, code_verifier: handoffParams.verifier, }), }); return (yield response.json()); })); export const refreshTokens = createAsyncThunk("auth/REFRESH_TOKENS", (clientId_1, _a) => __awaiter(void 0, [clientId_1, _a], void 0, function* (clientId, { getState }) { var _b; const state = getState(); const url = (_b = state.openIdConfiguration.data) === null || _b === void 0 ? void 0 : _b.token_endpoint; if (!url) return; const { refreshToken } = state.auth; if (!refreshToken) { // We shouldn't execute a request that we know will fail. If no refresh token is set, this action errors - // the user is (should already be) signed out with no permissions. throw new Error("No refresh token present"); // Throw an error to definitively reset auth slice state. } const response = yield fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: buildUrlEncodedData({ grant_type: "refresh_token", client_id: clientId, refresh_token: refreshToken, }), }); const body = yield response.json(); if (!response.ok) { const errorDetail = body && (body.error || body.error_description) ? `: ${body === null || body === void 0 ? void 0 : body.error}${body === null || body === void 0 ? void 0 : body.error_description}` : ""; throw new Error(`Error encountered while refreshing token${errorDetail}`); } return yield body; }), { condition: (_, { getState }) => { var _a; const { auth, openIdConfiguration } = getState(); if (!((_a = openIdConfiguration.data) === null || _a === void 0 ? void 0 : _a["token_endpoint"])) { console.error("No token endpoint available/No openIdConfiguration data"); return false; } const { isRefreshingTokens, refreshToken } = auth; return !isRefreshingTokens && refreshToken !== null; }, }); export const setIsAutoAuthenticating = createAction("auth/SET_IS_AUTO_AUTHENTICATING"); export const fetchResourcesPermissions = createAsyncThunk("auth/FETCH_RESOURCES_PERMISSIONS", (_a, _b) => __awaiter(void 0, [_a, _b], void 0, function* ({ resources, authzUrl }, { getState }) { const url = `${authzUrl}/policy/permissions`; const { auth } = getState(); const response = yield fetch(url, { method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, makeAuthorizationHeader(auth.accessToken)), body: JSON.stringify({ resources }), }); return yield response.json(); }), { condition: ({ resources }, { getState }) => { // allow action to fire if: // - token handoff is not currently occurring // - all requested resource permission-sets are not being fetched right now const { auth } = getState(); return (!auth.isHandingOffCodeForToken && resources.every((resource) => { var _a; const key = makeResourceKey(resource); const rp = (_a = auth.resourcePermissions) === null || _a === void 0 ? void 0 : _a[key]; return !(rp === null || rp === void 0 ? void 0 : rp.isFetching); })); }, }); const nullSession = { sessionExpiry: undefined, idToken: undefined, idTokenContents: undefined, accessToken: undefined, refreshToken: undefined, }; const initialState = { loading: false, hasAttempted: false, isHandingOffCodeForToken: false, handoffError: "", isRefreshingTokens: false, tokensRefreshError: "", isAutoAuthenticating: false, resourcePermissions: {}, }; const setTokenStateFromPayload = (state, payload) => { const { access_token: accessToken, expires_in: exp, id_token: idToken, refresh_token: refreshToken } = payload; state.sessionExpiry = new Date().getTime() / 1000 + exp; state.idToken = idToken; state.idTokenContents = decodeJwt(idToken); state.accessToken = accessToken; state.refreshToken = refreshToken !== null && refreshToken !== void 0 ? refreshToken : state.refreshToken; }; export const authSlice = createSlice({ name: "auth", initialState, reducers: { signOut: (state) => { setLSNotSignedIn(); Object.assign(state, Object.assign(Object.assign(Object.assign({}, state), nullSession), { tokensRefreshError: "", resourcePermissions: {} })); }, }, extraReducers: (builder) => { builder .addCase(tokenHandoff.pending, (state) => { state.isHandingOffCodeForToken = true; }) .addCase(tokenHandoff.fulfilled, (state, { payload }) => { state.loading = false; // Reset hasAttempted for user-dependent data if we just signed in state.hasAttempted = !state.idTokenContents && payload.id_token ? false : state.hasAttempted; setTokenStateFromPayload(state, payload); state.isHandingOffCodeForToken = false; localStorage.setItem(LS_BENTO_WAS_SIGNED_IN, "true"); }) .addCase(tokenHandoff.rejected, (state, { error }) => { var _a; const handoffError = (_a = error.message) !== null && _a !== void 0 ? _a : "Error handing off authorization code for token"; console.error(handoffError); Object.assign(state, Object.assign(Object.assign(Object.assign({}, state), nullSession), { loading: false, isHandingOffCodeForToken: false, handoffError: handoffError, resourcePermissions: {} })); setLSNotSignedIn(); }) .addCase(refreshTokens.pending, (state) => { state.isRefreshingTokens = true; }) .addCase(refreshTokens.fulfilled, (state, { payload }) => { if (payload) { setTokenStateFromPayload(state, payload); state.isRefreshingTokens = false; localStorage.setItem(LS_BENTO_WAS_SIGNED_IN, "true"); } }) .addCase(refreshTokens.rejected, (state, { error }) => { var _a; console.error(error); const refreshError = (_a = error.message) !== null && _a !== void 0 ? _a : "Error refreshing tokens"; Object.assign(state, Object.assign(Object.assign(Object.assign({}, state), nullSession), { tokensRefreshError: refreshError, resourcePermissions: {}, isRefreshingTokens: false })); setLSNotSignedIn(); }) .addCase(setIsAutoAuthenticating, (state, { payload }) => { state.isAutoAuthenticating = payload; }) .addCase(fetchResourcesPermissions.pending, (state, { meta }) => { for (const resource of meta.arg.resources) { const key = makeResourceKey(resource); state.resourcePermissions[key] = Object.assign(Object.assign({}, state.resourcePermissions[key]), { isFetching: true, hasAttempted: false, permissions: [], error: "" }); } }) .addCase(fetchResourcesPermissions.fulfilled, (state, { meta, payload }) => { var _a, _b; const resources = meta.arg.resources; for (const r in resources) { const key = makeResourceKey(resources[r]); state.resourcePermissions[key] = Object.assign(Object.assign({}, state.resourcePermissions[key]), { isFetching: false, hasAttempted: true, permissions: (_b = (_a = payload === null || payload === void 0 ? void 0 : payload.result) === null || _a === void 0 ? void 0 : _a[r]) !== null && _b !== void 0 ? _b : [] }); } }) .addCase(fetchResourcesPermissions.rejected, (state, { meta, error }) => { var _a; if (error) console.error(error); for (const resource of meta.arg.resources) { const key = makeResourceKey(resource); const permissionsError = (_a = error.message) !== null && _a !== void 0 ? _a : "An error occurred while fetching permissions for a resource"; state.resourcePermissions[key] = Object.assign(Object.assign({}, state.resourcePermissions[key]), { isFetching: false, hasAttempted: true, error: permissionsError }); } }); }, }); export const { signOut } = authSlice.actions; export default authSlice.reducer; //# sourceMappingURL=authSlice.js.map