Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[STCOR-888] RTR dynamic configuration, debugging event #1535

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export { userLocaleConfig } from './src/loginServices';
export * from './src/consortiaServices';
export { default as queryLimit } from './src/queryLimit';
export { default as init } from './src/init';
export * as RTR_CONSTANTS from './src/components/Root/constants';

/* localforage wrappers hide the session key */
export { getOkapiSession, getTokenExpiry, setTokenExpiry } from './src/loginServices';
Expand Down
22 changes: 20 additions & 2 deletions src/components/Root/FFetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ import {
} from './Errors';
import {
RTR_AT_EXPIRY_IF_UNKNOWN,
RTR_AT_TTL_FRACTION,
RTR_ERROR_EVENT,
RTR_FORCE_REFRESH_EVENT,
RTR_FLS_TIMEOUT_EVENT,
RTR_TIME_MARGIN_IN_MS,
RTR_FLS_WARNING_EVENT,
Expand All @@ -83,6 +83,24 @@ export class FFetch {
this.rtrConfig = rtrConfig;
}

/**
* registers a listener for the RTR_FORCE_REFRESH_EVENT
*/
registerEventListener = () => {
this.globalEventCallback = () => {
this.logger.log('rtr', 'forcing rotation due to RTR_FORCE_REFRESH_EVENT');
rtr(this.nativeFetch, this.logger, this.rotateCallback, this.store.getState().okapi);
};
window.addEventListener(RTR_FORCE_REFRESH_EVENT, this.globalEventCallback);
}

/**
* unregister the listener for the RTR_FORCE_REFRESH_EVENT
*/
unregisterEventListener = () => {
window.removeEventListener(RTR_FORCE_REFRESH_EVENT, this.globalEventCallback);
}

/**
* save a reference to fetch, and then reassign the global :scream:
*/
Expand Down Expand Up @@ -112,7 +130,7 @@ export class FFetch {
scheduleRotation = (rotationP) => {
rotationP.then((rotationInterval) => {
// AT refresh interval: a large fraction of the actual AT TTL
const atInterval = (rotationInterval.accessTokenExpiration - Date.now()) * RTR_AT_TTL_FRACTION;
const atInterval = (rotationInterval.accessTokenExpiration - Date.now()) * this.rtrConfig.rotationIntervalFraction;

// RT timeout interval (session will end) and warning interval (warning that session will end)
const rtTimeoutInterval = (rotationInterval.refreshTokenExpiration - Date.now());
Expand Down
35 changes: 34 additions & 1 deletion src/components/Root/FFetch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
/* eslint-disable no-unused-vars */

import ms from 'ms';
import { waitFor } from '@testing-library/react';
import { okapi } from 'stripes-config';

import { getTokenExpiry } from '../../loginServices';
import { FFetch } from './FFetch';
import { RTRError, UnexpectedResourceError } from './Errors';
import {
RTR_AT_EXPIRY_IF_UNKNOWN,
RTR_AT_TTL_FRACTION,
RTR_FORCE_REFRESH_EVENT,
RTR_FLS_WARNING_TTL,
RTR_TIME_MARGIN_IN_MS,
} from './constants';
Expand All @@ -34,13 +37,18 @@ const log = jest.fn();

const mockFetch = jest.fn();

// to ensure we cleanup after each test
const instancesWithEventListeners = [];

describe('FFetch class', () => {
beforeEach(() => {
global.fetch = mockFetch;
getTokenExpiry.mockResolvedValue({
atExpires: Date.now() + (10 * 60 * 1000),
rtExpires: Date.now() + (10 * 60 * 1000),
});
instancesWithEventListeners.forEach(instance => instance.unregisterEventListener());
instancesWithEventListeners.length = 0;
});

afterEach(() => {
Expand Down Expand Up @@ -153,6 +161,23 @@ describe('FFetch class', () => {
});
});

describe('force refresh event', () => {
it('Invokes a refresh on RTR_FORCE_REFRESH_EVENT...', async () => {
mockFetch.mockResolvedValueOnce('okapi success');

const instance = new FFetch({ logger: { log }, store: { getState: () => ({ okapi }) } });
instance.replaceFetch();
instance.replaceXMLHttpRequest();

instance.registerEventListener();
instancesWithEventListeners.push(instance);

window.dispatchEvent(new Event(RTR_FORCE_REFRESH_EVENT));

await waitFor(() => expect(mockFetch.mock.calls).toHaveLength(1));
});
});

describe('calling authentication resources', () => {
it('handles RTR data in the response', async () => {
// a static timestamp representing "now"
Expand Down Expand Up @@ -187,6 +212,7 @@ describe('FFetch class', () => {
},
rtrConfig: {
fixedLengthSessionWarningTTL: '1m',
rotationIntervalFraction: 0.8,
},
});
testFfetch.replaceFetch();
Expand Down Expand Up @@ -243,6 +269,7 @@ describe('FFetch class', () => {
},
rtrConfig: {
fixedLengthSessionWarningTTL: '1m',
rotationIntervalFraction: 0.8,
},
});
testFfetch.replaceFetch();
Expand Down Expand Up @@ -281,6 +308,7 @@ describe('FFetch class', () => {
},
rtrConfig: {
fixedLengthSessionWarningTTL: '1m',
rotationIntervalFraction: 0.8,
},
});
testFfetch.replaceFetch();
Expand Down Expand Up @@ -317,7 +345,11 @@ describe('FFetch class', () => {
logger: { log },
store: {
dispatch: jest.fn(),
}
},
rtrConfig: {
fixedLengthSessionWarningTTL: '1m',
rotationIntervalFraction: 0.8,
},
});
testFfetch.replaceFetch();
testFfetch.replaceXMLHttpRequest();
Expand Down Expand Up @@ -360,6 +392,7 @@ describe('FFetch class', () => {
},
rtrConfig: {
fixedLengthSessionWarningTTL: '1m',
rotationIntervalFraction: 0.8,
},
});
testFfetch.replaceFetch();
Expand Down
4 changes: 4 additions & 0 deletions src/components/Root/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export const RTR_SUCCESS_EVENT = '@folio/stripes/core::RTRSuccess';
/** dispatched during RTR if RTR itself fails */
export const RTR_ERROR_EVENT = '@folio/stripes/core::RTRError';

/** dispatched by ui-developer to force a token rotation */
export const RTR_FORCE_REFRESH_EVENT = '@folio/stripes/core::RTRForceRefresh';

/**
* dispatched if the session is idle (without activity) for too long
*/
Expand Down Expand Up @@ -36,6 +39,7 @@ export const RTR_ACTIVITY_CHANNEL = '@folio/stripes/core::RTRActivityChannel';
* the RT is still good at that point. Since rotation happens in the background
* (i.e. it isn't a user-visible feature), rotating early has no user-visible
* impact.
* overridden in stripes.config.js::config.rtr.rotationIntervalFraction.
*/
export const RTR_AT_TTL_FRACTION = 0.8;

Expand Down
6 changes: 6 additions & 0 deletions src/components/Root/token-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getTokenExpiry, setTokenExpiry } from '../../loginServices';
import { RTRError, UnexpectedResourceError } from './Errors';
import {
RTR_ACTIVITY_EVENTS,
RTR_AT_TTL_FRACTION,
RTR_ERROR_EVENT,
RTR_FLS_WARNING_TTL,
RTR_IDLE_MODAL_TTL,
Expand Down Expand Up @@ -322,6 +323,11 @@ export const configureRtr = (config = {}) => {
conf.idleModalTTL = RTR_IDLE_MODAL_TTL;
}

// what fraction of the way through the session should we rotate?
if (!conf.rotationIntervalFraction) {
conf.rotationIntervalFraction = RTR_AT_TTL_FRACTION;
}

// what events constitute activity?
if (isEmpty(conf.activityEvents)) {
conf.activityEvents = RTR_ACTIVITY_EVENTS;
Expand Down
30 changes: 16 additions & 14 deletions src/components/Root/token-util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,19 +342,21 @@ describe('getPromise', () => {
});

describe('configureRtr', () => {
it('sets idleSessionTTL and idleModalTTL', () => {
const res = configureRtr({});
expect(res.idleSessionTTL).toBe('60m');
expect(res.idleModalTTL).toBe('1m');
});

it('leaves existing settings in place', () => {
const res = configureRtr({
idleSessionTTL: '5m',
idleModalTTL: '5m',
});

expect(res.idleSessionTTL).toBe('5m');
expect(res.idleModalTTL).toBe('5m');
it.each([
[
{},
{ idleSessionTTL: '60m', idleModalTTL: '1m', rotationIntervalFraction: 0.8, activityEvents: ['keydown', 'mousedown'] }
],
[
{ idleSessionTTL: '1s', idleModalTTL: '2m' },
{ idleSessionTTL: '1s', idleModalTTL: '2m', rotationIntervalFraction: 0.8, activityEvents: ['keydown', 'mousedown'] }
],
[
{ idleSessionTTL: '1s', idleModalTTL: '2m', rotationIntervalFraction: -1, activityEvents: ['cha-cha-slide'] },
{ idleSessionTTL: '1s', idleModalTTL: '2m', rotationIntervalFraction: -1, activityEvents: ['cha-cha-slide'] }
],
])('sets default values as applicable', (config, expected) => {
const res = configureRtr(config);
expect(res).toMatchObject(expected);
});
});
Loading