From 2e162f618287cd59f4c4937fbd04ac107aeb15e5 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Tue, 11 Jun 2024 13:30:55 -0400 Subject: [PATCH] STCOR-776 RTR adjustments for keycloak (#1490) There are many small differences in how keycloak and okapi respond to authentication related requests. * permissions are structured differently in Okapi between `login` and `_self` requests and depending on whether `expandPermissions=true` is present on the request; keycloak always responds with a flattened list. * token expiration data is nested in the login-response in Okapi but is a root-level element in the `/authn/token` response from keycloak. STCOR-776, STCOR-846 --- src/components/Root/FFetch.js | 13 +++++++++---- src/loginServices.js | 31 +++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/components/Root/FFetch.js b/src/components/Root/FFetch.js index ef84814b8..21c5b4c73 100644 --- a/src/components/Root/FFetch.js +++ b/src/components/Root/FFetch.js @@ -119,12 +119,12 @@ export class FFetch { // When starting a new session, the response from /bl-users/login-with-expiry // will contain AT expiration info, but when restarting an existing session, - // the response from /bl-users/_self will NOT, although that information will + // the response from /bl-users/_self will NOT, although that information should // have been cached in local-storage. // // This means there are many places we have to check to figure out when the // AT is likely to expire, and thus when we want to rotate. First inspect - // the response, otherwise the session. Default to 10 seconds. + // the response, then the session, then default to 10 seconds. if (res?.accessTokenExpiration) { this.logger.log('rtr', 'rotation scheduled with login response data'); const rotationPromise = Promise.resolve((new Date(res.accessTokenExpiration).getTime() - Date.now()) * RTR_AT_TTL_FRACTION); @@ -132,7 +132,7 @@ export class FFetch { scheduleRotation(rotationPromise); } else { const rotationPromise = getTokenExpiry().then((expiry) => { - if (expiry.atExpires) { + if (expiry?.atExpires) { this.logger.log('rtr', 'rotation scheduled with cached session data'); return (new Date(expiry.atExpires).getTime() - Date.now()) * RTR_AT_TTL_FRACTION; } @@ -189,7 +189,12 @@ export class FFetch { if (clone.ok) { this.logger.log('rtr', 'authn success!'); clone.json().then(json => { - this.rotateCallback(json.tokenExpiration); + // we want accessTokenExpiration. do we need to destructure? + // in community-folio, a /login-with-expiry response is shaped like + // { ..., tokenExpiration: { accessTokenExpiration, refreshTokenExpiration } } + // in eureka-folio, a /authn/token response is shaped like + // { accessTokenExpiration, refreshTokenExpiration } + this.rotateCallback(json.tokenExpiration ?? json); }); } diff --git a/src/loginServices.js b/src/loginServices.js index 57d7bc006..220eb2865 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -421,16 +421,31 @@ export function spreadUserWithPerms(userWithPerms) { ...userWithPerms?.user?.personal, }; - // remap userWithPerms.permissions.permissions from an array shaped like - // [{ "permissionName": "foo", ... }] - // to an object shaped like - // { foo: true, ...} - const perms = {}; + // remap data's array of permission-names to set with + // permission-names for keys and `true` for values. + // + // userWithPerms is shaped differently depending on the API call + // that generated it. + // in community-folio, /login sends data like [{ "permissionName": "foo" }] + // and includes both directly and indirectly assigned permissions + // in community-folio, /_self sends data like ["foo", "bar", "bat"] + // but only includes directly assigned permissions + // in community-folio, /_self?expandPermissions=true sends data like [{ "permissionName": "foo" }] + // and includes both directly and indirectly assigned permissions + // in eureka-folio, /_self sends data like ["foo", "bar", "bat"] + // and includes both directly and indirectly assigned permissions + // + // we'll parse it differently depending on what it looks like. + let perms = {}; const list = userWithPerms?.permissions?.permissions; if (list && Array.isArray(list) && list.length > 0) { - list.forEach(p => { - perms[p.permissionName] = true; - }); + // shaped like this ["foo", "bar", "bat"] or + // shaped like that [{ "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 }))); + } } return { user, perms };