Skip to content

Commit

Permalink
STCOR-846 update session data with values from _self request (#1466)
Browse files Browse the repository at this point in the history
When restoring an existing session, i.e. pulling session values from
storage and merging them with results from the response to the `_self`
request, allow the `_self` values to overwrite the others, including
updates to the `user` and `permissions` objects. This allows name and
permission changes to take effect immediately, simply by reloading,
rather than requiring a sign-out/sign-in cycle.

There are two main aspects to this change:
* A bug fix: previously, the `_self` request omitted the
  `expandPermissions=true` param that is necessary to retrieve
  the expanded and flattened permissions, instead returning only those
  values that are directly assigned.
* User and permissions values from the `_self` response now overwrite
  those from the existing session in storage.

Additionally, the `tokenExpiration` value from the existing session is
used as-is. Previously, it was overwritten with dummy values with the
explanation that "the response from _self doesn't contain these values"
... but that's fine. The fact that the request to `_self` succeeded
tells us the tokens are valid and therefore that the values in the
existing session are valid.

Refs STCOR-846
  • Loading branch information
zburke authored May 29, 2024
1 parent 80bda77 commit c58e079
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Idle-session timeout and "Keep working?" modal. Refs STCOR-776.
* Implement password validation for Login Page. Refs STCOR-741.
* Avoid deprecated `defaultProps` for functional components. Refs STCOR-844..
* Update session data with values from `_self` request on reload. Refs STCOR-846.

## [10.1.0](https://github.com/folio-org/stripes-core/tree/v10.1.0) (2024-03-12)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.0.0...v10.1.0)
Expand Down
60 changes: 30 additions & 30 deletions src/loginServices.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,11 +390,16 @@ function loadResources(okapiUrl, store, tenant, userId) {

/**
* spreadUserWithPerms
* return an object { user, perms } based on response from bl-users/self.
* Restructure the response from `bl-users/self?expandPermissions=true`
* to return an object shaped like
* {
* user: { id, username, ...personal }
* perms: { foo: true, bar: true, ... }
* }
*
* @param {object} userWithPerms
*
* @returns {object}
* @returns {object} { user, perms }
*/
export function spreadUserWithPerms(userWithPerms) {
const user = {
Expand All @@ -403,23 +408,16 @@ export function spreadUserWithPerms(userWithPerms) {
...userWithPerms?.user?.personal,
};

// remap data's array of permission-names to set with
// permission-names for keys and `true` for values.
//
// userWithPerms is shaped differently depending on whether
// it comes from a login call or a `.../_self` call, which
// is just totally totally awesome. :|
// we'll parse it differently depending on what it looks like.
let perms = {};
// remap userWithPerms.permissions.permissions from an array shaped like
// [{ "permissionName": "foo", ... }]
// to an object shaped like
// { foo: true, ...}
const perms = {};
const list = userWithPerms?.permissions?.permissions;
if (list && Array.isArray(list) && list.length > 0) {
// _self sends data like ["foo", "bar", "bat"]
// login sends data like [{ "permissionName": "foo" }]
if (typeof list[0] === 'string') {
perms = Object.assign({}, ...list.map(p => ({ [p]: true })));
} else {
perms = Object.assign({}, ...list.map(p => ({ [p.permissionName]: true })));
}
list.forEach(p => {
perms[p.permissionName] = true;
});
}

return { user, perms };
Expand Down Expand Up @@ -694,8 +692,8 @@ export function processOkapiSession(okapiUrl, store, tenant, resp, ssoToken) {
* @returns {Promise}
*/
export function validateUser(okapiUrl, store, tenant, session) {
const { token, user, perms, tenant: sessionTenant = tenant } = session;
return fetch(`${okapiUrl}/bl-users/_self`, {
const { token, tenant: sessionTenant = tenant } = session;
return fetch(`${okapiUrl}/bl-users/_self?expandPermissions=true`, {
headers: getHeaders(sessionTenant, token),
credentials: 'include',
mode: 'cors',
Expand All @@ -706,23 +704,25 @@ export function validateUser(okapiUrl, store, tenant, session) {
store.dispatch(setAuthError(null));
store.dispatch(setLoginData(data));

// If the request succeeded, we know the AT must be valid, but the
// response body from this endpoint doesn't include token-expiration
// data. So ... we set a near-future RT and an already-expired AT.
// On the next request, the expired AT will prompt an RTR cycle and
// we'll get real expiration values then.
const tokenExpiration = {
atExpires: -1,
rtExpires: Date.now() + (10 * 60 * 1000),
};

const { user, perms } = spreadUserWithPerms(data);
store.dispatch(setCurrentPerms(perms));

// update the session data with values from the response from _self
// in case they have changed since the previous login. this allows
// permissions changes to take effect immediately, without needing to
// re-authenticate.
//
// tenant and tokenExpiration data are still pulled from the session,
// tenant because the user may have switched the session-tenant to
// something other than their default and tokenExpiration because that
// data isn't provided by _self.
store.dispatch(setSessionData({
isAuthenticated: true,
user: data.user,
perms,
tenant: sessionTenant,
token,
tokenExpiration,
tokenExpiration: session.tokenExpiration
}));

return loadResources(okapiUrl, store, sessionTenant, user.id);
Expand Down
46 changes: 45 additions & 1 deletion src/loginServices.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
setIsAuthenticated,
setOkapiReady,
setServerDown,
// setSessionData,
setSessionData,
// setTokenExpiration,
setLoginData,
updateCurrentUser,
Expand Down Expand Up @@ -311,6 +311,50 @@ describe('validateUser', () => {
mockFetchCleanUp();
});

it('overwrites session data with new values from _self', async () => {
const store = {
dispatch: jest.fn(),
};

const tenant = 'tenant';
const sessionTenant = 'sessionTenant';
const data = {
user: {
id: 'ego',
username: 'superego',
},
permissions: {
permissions: [{ permissionName: 'ask' }, { permissionName: 'tell' }],
}
};

const session = {
user: { id: 'id', username: 'username' },
perms: { foo: true },
tenant: sessionTenant,
token: 'token',
};

mockFetchSuccess(data);

await validateUser('url', store, tenant, session);

const updatedSession = {
user: data.user,
isAuthenticated: true,
perms: { ask: true, tell: true },
tenant: session.tenant,
token: session.token,
};

expect(store.dispatch).toHaveBeenNthCalledWith(1, setAuthError(null));
expect(store.dispatch).toHaveBeenNthCalledWith(2, setLoginData(data));
expect(store.dispatch).toHaveBeenNthCalledWith(3, setCurrentPerms({ ask: true, tell: true }));
expect(store.dispatch).toHaveBeenNthCalledWith(4, setSessionData(updatedSession));

mockFetchCleanUp();
});

it('handles invalid user', async () => {
const store = {
dispatch: jest.fn(),
Expand Down

0 comments on commit c58e079

Please sign in to comment.