bento-auth-js
Version:
Authentication library for web applications of Bento-Platform
206 lines • 10.5 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());
});
};
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