Skip to content

Commit

Permalink
STCOR-817 add token checks to XHR prior to rotating token (#1430)
Browse files Browse the repository at this point in the history
* add token checks to XHR prior to rotating token

* add tests for *not rotating and not error-swallowing

* lint

* log changes

* rtr XHR send logic: check, update local, check, maybe send, maybe rotate, maybe send...

* more genuine RTR mock in FXHR test

* refactor FXHR to use async/await and try/catch vs then...catch
  • Loading branch information
JohnC-80 authored Mar 7, 2024
1 parent 4ac21be commit fed7d78
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* `useOkapiKy` uses || instead of ?? to apply current tenant id when override was not provided. Refs STCOR-814.
* Correctly parse `.../_self` permissions object. Refs STCOR-813.
* Add `idName` and `limit` as passable props to `useChunkedCQLFetch`. Refs STCOR-821.
* Check for valid token before rotating during XHR send. Refs STCOR-817.

## [10.0.0](https://github.com/folio-org/stripes-core/tree/v10.0.0) (2023-10-11)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v9.0.0...v10.0.0)
Expand Down
38 changes: 33 additions & 5 deletions src/components/Root/FXHR.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { okapi } from 'stripes-config';
import { isFolioApiRequest, rtr } from './token-util';
import { isFolioApiRequest, rtr, isValidAT, isValidRT } from './token-util';
import { getTokenExpiry } from '../../loginServices';
import {
RTR_ERROR_EVENT,
} from './Events';
import { RTRError } from './Errors';

export default (deps) => {
return class FXHRClass extends XMLHttpRequest {
Expand All @@ -16,15 +21,38 @@ export default (deps) => {
}

send = async (payload) => {
const { logger } = this.FFetchContext;
this.FFetchContext.logger?.log('rtr', 'capture XHR send');
if (this.shouldEnsureToken) {
await rtr(this.FFetchContext)
.then(() => {
if (!isValidAT(this.FFetchContext.tokenExpiration, logger)) {
logger.log('rtr', 'local tokens expired; fetching from storage for XHR..');
this.FFetchContext.tokenExpiration = await getTokenExpiry();
}

if (isValidAT(this.FFetchContext.tokenExpiration, logger)) {
logger.log('rtr', 'local AT valid, sending XHR...');
super.send(payload);
} else if (isValidRT(this.FFetchContext.tokenExpiration, logger)) {
logger.log('rtr', 'local RT valid, sending XHR...');
try {
await rtr(this.FFetchContext);
logger.log('rtr', 'local RTtoken refreshed, sending XHR...');
super.send(payload);
});
} catch (err) {
if (err instanceof RTRError) {
console.error('RTR failure while attempting XHR', err); // eslint-disable-line no-console
document.dispatchEvent(new Event(RTR_ERROR_EVENT, { detail: err }));
}
throw err;
}
} else {
logger.log('rtr', 'All tokens expired when attempting to send XHR');
document.dispatchEvent(new Event(RTR_ERROR_EVENT, { detail: 'All tokens expired when sending XHR' }));
}
} else {
logger.log('rtr', 'request passed through, sending XHR...');
super.send(payload);
}
}
};
};
};
56 changes: 55 additions & 1 deletion src/components/Root/FXHR.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { rtr } from './token-util';
import { getTokenExpiry } from '../../loginServices';
import { RTRError } from './Errors';
import FXHR from './FXHR';

jest.mock('./token-util', () => ({
...(jest.requireActual('./token-util')),
rtr: jest.fn(() => new Promise()),
}));

jest.mock('../../loginServices', () => ({
...(jest.requireActual('../../loginServices')),
setTokenExpiry: jest.fn(() => Promise.resolve()),
getTokenExpiry: jest.fn(() => Promise.resolve())
}));

const openSpy = jest.spyOn(XMLHttpRequest.prototype, 'open').mockImplementation();
const sendSpy = jest.spyOn(XMLHttpRequest.prototype, 'send').mockImplementation(() => {});
const aelSpy = jest.spyOn(XMLHttpRequest.prototype, 'addEventListener').mockImplementation();
Expand All @@ -17,7 +25,7 @@ describe('FXHR', () => {
let testXHR;
beforeEach(() => {
jest.clearAllMocks();
FakeXHR = FXHR({ logger: { log: () => {} } });
FakeXHR = FXHR({ tokenExpiration: { atExpires: Date.now(), rtExpires: Date.now() + 5000 }, logger: { log: () => {} } });
testXHR = new FakeXHR();
});

Expand Down Expand Up @@ -47,4 +55,50 @@ describe('FXHR', () => {
expect(openSpy.mock.calls).toHaveLength(1);
expect(aelSpy.mock.calls).toHaveLength(1);
});

it('Does not rotate if token is valid', () => {
getTokenExpiry.mockResolvedValue({
atExpires: Date.now() + (10 * 60 * 1000),
rtExpires: Date.now() + (10 * 60 * 1000),
});

testXHR.addEventListener('abort', mockHandler);
testXHR.open('POST', 'okapiUrl');
testXHR.send(new ArrayBuffer(8));
expect(openSpy.mock.calls).toHaveLength(1);
expect(aelSpy.mock.calls).toHaveLength(1);
expect(rtr.mock.calls).toHaveLength(0);
});

it('If AT is invalid, but RT is valid, refresh the token before sending...', async () => {
getTokenExpiry.mockResolvedValue({
atExpires: Date.now() - (10 * 60 * 1000),
rtExpires: Date.now() + (10 * 60 * 1000),
});
testXHR.addEventListener('abort', mockHandler);
testXHR.open('POST', 'okapiUrl');
await testXHR.send(new ArrayBuffer(8));
expect(openSpy.mock.calls).toHaveLength(1);
expect(aelSpy.mock.calls).toHaveLength(1);
expect(rtr.mock.calls).toHaveLength(1);
});


it('Handles Errors during token rotation', async () => {
rtr.mockRejectedValueOnce(new RTRError('rtr test failure'));
getTokenExpiry.mockResolvedValue({
atExpires: Date.now() - (10 * 60 * 1000),
rtExpires: Date.now() + (10 * 60 * 1000),
});
let error = null;
try {
testXHR.addEventListener('abort', mockHandler);
testXHR.open('POST', 'okapiUrl');
await testXHR.send(new ArrayBuffer(8));
} catch (err) {
error = err;
} finally {
expect(error instanceof RTRError).toBe(true);
}
});
});

0 comments on commit fed7d78

Please sign in to comment.