Skip to content

Commit

Permalink
STCOR-671 handle access-control via cookies
Browse files Browse the repository at this point in the history
Handle access-control via HTTP-only cookies instead of storing the JWT
in local storage and providing it in the `X-Okapi-Token` header of fetch
requests. The `login-with-expiry` endpoint returns an access-token and
refresh-token in HTTP-only cookies, along with information about when
those cookies expire in the response body. Stripes-core sets up a
service worker to track the AT's expiration timestamp and transparently
request a replacement by intercepting the fetch request, replacing (i.e.
rotating) both the AT and the RT before passing along the original
request.

Notable changes:

* Sessions now timeout after a period of inactivity, determined by the
  lifespan of the RT, instead of remaining valid indefinitely.
* Authentication requests are sent to `/bl-users/login-with-expiry`
  instead of `/bl-users/login`.
* "Activity" is tracked by a document-level event handler that listens
  for mouse-down and key-down events.

Refs STCOR-671, FOLIO-3627
  • Loading branch information
zburke committed Oct 4, 2023
1 parent a2b1367 commit b2065cc
Show file tree
Hide file tree
Showing 19 changed files with 587 additions and 120 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* Forgot password and Forgot username : add placeholder to input box. Refs STCOR-728.
* Include `yarn.lock`. Refs STCOR-679.
* *BREAKING* bump `react-intl` to `v6.4.4`. Refs STCOR-744.
* *BREAKING* use cookies and RTR instead of directly handling the JWT. Refs STCOR-671.

## [9.0.0](https://github.com/folio-org/stripes-core/tree/v9.0.0) (2023-01-30)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v8.3.0...v9.0.0)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"graphql": "^16.0.0",
"history": "^4.6.3",
"hoist-non-react-statics": "^3.3.0",
"inactivity-timer": "^1.0.0",
"jwt-decode": "^3.1.2",
"ky": "^0.23.0",
"localforage": "^1.5.6",
Expand Down
8 changes: 4 additions & 4 deletions src/RootWithIntl.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ class RootWithIntl extends React.Component {
logger: PropTypes.object.isRequired,
clone: PropTypes.func.isRequired,
}).isRequired,
token: PropTypes.string,
isAuthenticated: PropTypes.bool,
disableAuth: PropTypes.bool.isRequired,
history: PropTypes.shape({}),
};

static defaultProps = {
token: '',
isAuthenticated: false,
history: {},
};

Expand All @@ -66,7 +66,7 @@ class RootWithIntl extends React.Component {

render() {
const {
token,
isAuthenticated,
disableAuth,
history,
} = this.props;
Expand All @@ -85,7 +85,7 @@ class RootWithIntl extends React.Component {
>
<Provider store={stripes.store}>
<Router history={history}>
{ token || disableAuth ?
{ isAuthenticated || disableAuth ?
<>
<MainContainer>
<AppCtxMenuProvider>
Expand Down
4 changes: 2 additions & 2 deletions src/Stripes.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,18 @@ export const stripesShape = PropTypes.shape({
]),
okapiReady: PropTypes.bool,
tenant: PropTypes.string.isRequired,
token: PropTypes.string,
isAuthenticated: PropTypes.bool,
translations: PropTypes.object,
url: PropTypes.string.isRequired,
withoutOkapi: PropTypes.bool,
}).isRequired,
plugins: PropTypes.object,
setBindings: PropTypes.func.isRequired,
setCurrency: PropTypes.func.isRequired,
setIsAuthenticated: PropTypes.func.isRequired,
setLocale: PropTypes.func.isRequired,
setSinglePlugin: PropTypes.func.isRequired,
setTimezone: PropTypes.func.isRequired,
setToken: PropTypes.func.isRequired,
store: PropTypes.shape({
dispatch: PropTypes.func.isRequired,
getState: PropTypes.func.isRequired,
Expand Down
13 changes: 3 additions & 10 deletions src/components/MainNav/MainNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@ import { isEqual, find } from 'lodash';
import { compose } from 'redux';
import { injectIntl } from 'react-intl';
import { withRouter } from 'react-router';
import localforage from 'localforage';

import { branding } from 'stripes-config';

import { Icon } from '@folio/stripes-components';

import { withModules } from '../Modules';
import { LastVisitedContext } from '../LastVisited';
import { clearOkapiToken, clearCurrentUser } from '../../okapiActions';
import { resetStore } from '../../mainActions';
import { getLocale } from '../../loginServices';
import { getLocale, logout as sessionLogout } from '../../loginServices';
import {
updateQueryResource,
getLocationQuery,
Expand Down Expand Up @@ -123,12 +120,8 @@ class MainNav extends Component {
returnToLogin() {
const { okapi } = this.store.getState();

return getLocale(okapi.url, this.store, okapi.tenant).then(() => {
this.store.dispatch(clearOkapiToken());
this.store.dispatch(clearCurrentUser());
this.store.dispatch(resetStore());
localforage.removeItem('okapiSess');
});
return getLocale(okapi.url, this.store, okapi.tenant)
.then(sessionLogout(this.store));
}

// return the user to the login screen, but after logging in they will be brought to the default screen.
Expand Down
17 changes: 10 additions & 7 deletions src/components/Root/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import initialReducers from '../../initialReducers';
import enhanceReducer from '../../enhanceReducer';
import createApolloClient from '../../createApolloClient';
import createReactQueryClient from '../../createReactQueryClient';
import { setSinglePlugin, setBindings, setOkapiToken, setTimezone, setCurrency, updateCurrentUser } from '../../okapiActions';
import { loadTranslations, checkOkapiSession } from '../../loginServices';
import { setSinglePlugin, setBindings, setIsAuthenticated, setTimezone, setCurrency, updateCurrentUser } from '../../okapiActions';
import { addDocumentListeners, loadTranslations, checkOkapiSession } from '../../loginServices';
import { getQueryResourceKey, getCurrentModule } from '../../locationService';
import Stripes from '../../Stripes';
import RootWithIntl from '../../RootWithIntl';
Expand Down Expand Up @@ -64,6 +64,9 @@ class Root extends Component {

this.apolloClient = createApolloClient(okapi);
this.reactQueryClient = createReactQueryClient();

// document-level event handlers
addDocumentListeners();
}

getChildContext() {
Expand Down Expand Up @@ -107,7 +110,7 @@ class Root extends Component {
}

render() {
const { logger, store, epics, config, okapi, actionNames, token, disableAuth, currentUser, currentPerms, locale, defaultTranslations, timezone, currency, plugins, bindings, discovery, translations, history, serverDown } = this.props;
const { logger, store, epics, config, okapi, actionNames, isAuthenticated, disableAuth, currentUser, currentPerms, locale, defaultTranslations, timezone, currency, plugins, bindings, discovery, translations, history, serverDown } = this.props;

if (serverDown) {
return <div>Error: server is down.</div>;
Expand All @@ -125,7 +128,7 @@ class Root extends Component {
config,
okapi,
withOkapi: this.withOkapi,
setToken: (val) => { store.dispatch(setOkapiToken(val)); },
setIsAuthenticated: (val) => { store.dispatch(setIsAuthenticated(val)); },
actionNames,
locale,
timezone,
Expand Down Expand Up @@ -166,7 +169,7 @@ class Root extends Component {
>
<RootWithIntl
stripes={stripes}
token={token}
isAuthenticated={isAuthenticated}
disableAuth={disableAuth}
history={history}
/>
Expand All @@ -191,7 +194,7 @@ Root.propTypes = {
getState: PropTypes.func.isRequired,
replaceReducer: PropTypes.func.isRequired,
}),
token: PropTypes.string,
isAuthenticated: PropTypes.bool,
disableAuth: PropTypes.bool.isRequired,
logger: PropTypes.object.isRequired,
currentPerms: PropTypes.object,
Expand Down Expand Up @@ -249,13 +252,13 @@ function mapStateToProps(state) {
currentPerms: state.okapi.currentPerms,
currentUser: state.okapi.currentUser,
discovery: state.discovery,
isAuthenticated: state.okapi.isAuthenticated,
locale: state.okapi.locale,
okapi: state.okapi,
okapiReady: state.okapi.okapiReady,
plugins: state.okapi.plugins,
serverDown: state.okapi.serverDown,
timezone: state.okapi.timezone,
token: state.okapi.token,
translations: state.okapi.translations,
};
}
Expand Down
4 changes: 2 additions & 2 deletions src/createApolloClient.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { InMemoryCache, ApolloClient } from '@apollo/client';

const createClient = ({ url, tenant, token }) => (new ApolloClient({
const createClient = ({ url, tenant }) => (new ApolloClient({
uri: `${url}/graphql`,
credentials: 'include',
headers: {
'X-Okapi-Tenant': tenant,
'X-Okapi-Token': token,
},
cache: new InMemoryCache(),
}));
Expand Down
11 changes: 7 additions & 4 deletions src/discoverServices.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { some } from 'lodash';

function getHeaders(tenant, token) {
function getHeaders(tenant) {
return {
'X-Okapi-Tenant': tenant,
'X-Okapi-Token': token,
'Content-Type': 'application/json'
};
}
Expand All @@ -12,7 +11,9 @@ function fetchOkapiVersion(store) {
const okapi = store.getState().okapi;

return fetch(`${okapi.url}/_/version`, {
headers: getHeaders(okapi.tenant, okapi.token)
headers: getHeaders(okapi.tenant),
credentials: 'include',
mode: 'cors',
}).then((response) => { // eslint-disable-line consistent-return
if (response.status >= 400) {
store.dispatch({ type: 'DISCOVERY_FAILURE', code: response.status });
Expand All @@ -31,7 +32,9 @@ function fetchModules(store) {
const okapi = store.getState().okapi;

return fetch(`${okapi.url}/_/proxy/tenants/${okapi.tenant}/modules?full=true`, {
headers: getHeaders(okapi.tenant, okapi.token)
headers: getHeaders(okapi.tenant),
credentials: 'include',
mode: 'cors',
}).then((response) => { // eslint-disable-line consistent-return
if (response.status >= 400) {
store.dispatch({ type: 'DISCOVERY_FAILURE', code: response.status });
Expand Down
5 changes: 5 additions & 0 deletions src/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import 'core-js/stable';
import 'regenerator-runtime/runtime';
import React from 'react';
import { createRoot } from 'react-dom/client';
import localforage from 'localforage';

import { okapi as okapiConfig } from 'stripes-config';

import { registerServiceWorker } from './serviceWorkerRegistration';
import App from './App';

export default function init() {
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
registerServiceWorker(okapiConfig.url, localforage);
}
Loading

0 comments on commit b2065cc

Please sign in to comment.