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

[UID-159] Additional RTR debugging and configuration options #431

Merged
merged 12 commits into from
Oct 9, 2024
148 changes: 105 additions & 43 deletions src/settings/RefreshTokenRotation.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,79 @@
import { useEffect, useState } from 'react';
import { merge } from 'lodash';
import PropTypes from 'prop-types';
import { useCallback, useEffect, useState } from 'react';
import { Field, Form } from 'react-final-form';
import { FormattedMessage } from 'react-intl';

import {
getTokenExpiry,
setTokenExpiry,
} from '@folio/stripes/core';

import {
Button,
LoadingPane,
Pane,
PaneHeader,
} from '@folio/stripes/components';
import { getTokenExpiry } from '@folio/stripes/core';
import { Button, LoadingPane, Pane, PaneHeader, TextField } from '@folio/stripes/components';

/**
* manipulate AT/RT expiration dates in storage in order to test RTR.
* @returns
*/
const RefreshTokenRotation = () => {
const RefreshTokenRotation = ({ stripes }) => {
// why WHY copy this string here instead of importing it from stripes-core?
//
// RTR_FORCE_REFRESH_EVENT will be present in stripes-core 10.2.0 (stripes
// 9.2.0). Importing it would force the stripes peer depedency to bump from
// ^9.1.0 to ^9.2.0.If we copy the string instead of importing it, we can
// remain compatible with 9.1.0.
//
// OK, compatibility is nice. But it's still gross, right? Yep, super gross.
// Aren't you nauseated? Yes, yes I am. 🤢🧼🛁
const RTR_FORCE_REFRESH_EVENT = '@folio/stripes/core::RTRForceRefresh';

const [isLoading, setIsLoading] = useState(true);
const [tokenExpiration, setTokenExpiration] = useState({});

useEffect(() => {
getTokenExpiry()
.then(te => {
setTokenExpiration(te ?? { atExpires: -1, rtExpires: -1 });
setIsLoading(false);
});
setIsLoading(true);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is goofy, but eslint got mad and insisted it was always false

getTokenExpiry().then((te) => {
setTokenExpiration(te ?? { atExpires: -1, rtExpires: -1 });
setIsLoading(false);
});
}, []);

/**
* invalidateAT
* return a promise to expire the AT in local storage
* forceRefresh
* dispatch an event to force a token rotation
*/
const invalidateAT = () => {
return getTokenExpiry()
.then(te => {
const expiration = { ...te };
expiration.atExpires = -1;

return setTokenExpiry(expiration);
});
};
const forceRefresh = useCallback(
() => window.dispatchEvent(new Event(RTR_FORCE_REFRESH_EVENT)),
[],
);

/**
* invalidateRT
* return a promise to expire the AT and RT in local storage
* saveRtrConfig
* update stripes.config.rtr from form
*/
const invalidateRT = () => {
const expiration = {
atExpires: -1,
rtExpires: -1,
};
const saveRtrConfig = useCallback(
(values) => {
merge(stripes.config.rtr, {
idleSessionTTL: values.idleSessionTTL,
idleModalTTL: values.idleModalTTL,
fixedLengthSessionWarningTTL: values.fixedLengthSessionWarningTTL,
rotationIntervalFraction: Number(values.rotationIntervalFraction),
activityEvents: values.activityEvents.split(',').map((e) => e.trim()),
});

return setTokenExpiry(expiration);
};
forceRefresh();
},
[stripes, forceRefresh],
);

if (!isLoading) {
return (
<Pane
defaultWidth="fill"
renderHeader={renderProps => <PaneHeader {...renderProps} paneTitle={<FormattedMessage id="ui-developer.rtr" />} />}
renderHeader={(renderProps) => (
<PaneHeader {...renderProps} paneTitle={<FormattedMessage id="ui-developer.rtr" />} />
)}
>
<ul>
<li>stripes logs RTR events in the category <code>rtr</code></li>
<li>
stripes logs RTR events in the category <code>rtr</code>
</li>
</ul>
{!isLoading && (
<dl>
Expand All @@ -75,8 +84,53 @@ const RefreshTokenRotation = () => {
</dl>
)}
<div>
<Button onClick={invalidateAT}><FormattedMessage id="ui-developer.rtr.invalidateAT" /></Button>
<Button onClick={invalidateRT}><FormattedMessage id="ui-developer.rtr.invalidateRT" /></Button>
<Button onClick={forceRefresh}>
<FormattedMessage id="ui-developer.rtr.forceRefresh" />
</Button>

<Form
onSubmit={saveRtrConfig}
initialValues={{
...stripes.config.rtr,
activityEvents: stripes.config.rtr.activityEvents.join(','),
}}
>
{({ handleSubmit, pristine, submitting }) => (
<form onSubmit={handleSubmit}>
<Field
component={TextField}
name="idleSessionTTL"
label={<FormattedMessage id="ui-developer.rtr.idleSessionTTL" />}
/>
<Field
component={TextField}
name="idleModalTTL"
label={<FormattedMessage id="ui-developer.rtr.idleModalTTL" />}
/>
<Field
component={TextField}
name="fixedLengthSessionWarningTTL"
label={<FormattedMessage id="ui-developer.rtr.fixedLengthSessionWarningTTL" />}
/>
<Field
component={TextField}
name="rotationIntervalFraction"
label={<FormattedMessage id="ui-developer.rtr.rotationIntervalFraction" />}
type="number"
step={0.01}
min={0}
/>
<Field
component={TextField}
name="activityEvents"
label={<FormattedMessage id="ui-developer.rtr.activityEvents" />}
/>
<Button buttonStyle="primary" type="submit" disabled={pristine || submitting}>
<FormattedMessage id="stripes-core.button.save" />
</Button>
</form>
)}
</Form>
</div>
</Pane>
);
Expand All @@ -89,7 +143,15 @@ RefreshTokenRotation.propTypes = {
stripes: PropTypes.shape({
okapi: PropTypes.shape({
tenant: PropTypes.string,
})
}),
config: PropTypes.shape({
rtr: PropTypes.shape({
idleSessionTTL: PropTypes.string,
idleModalTTL: PropTypes.string,
rotationIntervalFraction: PropTypes.number,
activityEvents: PropTypes.arrayOf(PropTypes.string),
}),
}),
}).isRequired,
};

Expand Down
9 changes: 6 additions & 3 deletions translations/ui-developer/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,12 @@
"userLocale.numberingSystem": "Numbering system (override locale's default; <a>details</a>)",

"rtr": "Refresh token rotation",
"rtr.invalidateAT": "Invalidate access token",
"rtr.invalidateRT": "Invalidate refresh token",
"rtr.forceRefresh": "Force refresh",
"rtr.idleSessionTTL": "idleSessionTTL: duration an idle session lasts before being killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.idleModalTTL": "idleModalTTL: duration the idle modal should be shown before session is killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.fixedLengthSessionWarningTTL": "fixedLengthSessionWarningTTL: how long the “your session is going to die!” warning should be shown before the session is killed (e.g. 1h, 1m, 5s, 10ms)",
"rtr.rotationIntervalFraction": "rotationIntervalFraction: decimal fraction of how early to refresh the access token (e.g. 0.6 is 60% into the lifetime of the token)",
"rtr.activityEvents": "activityEvents: which DOM events constitute user activity (comma-separated, e.g. 'mousemove,keydown')",
"rtr.registerServiceWorker": "Register the service worker",
"rtr.unregisterServiceWorker": "Unregister the service worker",

Expand All @@ -192,5 +196,4 @@
"permission.settings.okapiConsole.modules": "Settings (developer): Can use the Okapi console's Modules tab",
"permission.settings.userLocale": "Settings (developer): Can edit locale entries for any user",
"permission.settings.okapiTimers": "Settings (developer): Can view okapi timers"

}