Skip to content

Commit

Permalink
STCOR-868 backport idle-session timeout and fixed-length-session time…
Browse files Browse the repository at this point in the history
…out (#1507)

Q shipped with a very minimal refresh-token-rotation implementation. In environments with long AT expirations, this doesn't pose much of a problem, but when sessions last less than, say, eight hours, the shortcomings become painfully apparent.

Here's a feature summary:

* STCOR-776 idle session timeout
* STCOR-787 use stripes-config::config.tenantOptions for all tenant-related data
* STCOR-864 do not render ModuleContainer until discovery is complete [bug fix]
* STCOR-865 consolidate /logout API calls into /logout and /logout-timeout UI routes
* STCOR-866 start the RTR cycle when restoring an existing session [bug fix]
* STCOR-862 fixed-length session timeout

And a commit summary:

* (cherry picked from commit 39d1fc9)
* (cherry picked from commit be7f076)
* (cherry picked from commit 99b8948)
* (cherry picked from commit e93a5af)
* (cherry picked from commit a9b860d)
* (cherry picked from commit eeaa34a)
* (cherry picked from commit 5bc64ce)
* (cherry picked from commit 2e162f6)
* (cherry picked from commit e738a2f)
* (cherry picked from commit eed1ba5)
* (cherry picked from commit 6201292)
* (cherry picked from commit 8daa267)
* (cherry picked from commit f93f21d)
* (cherry picked from commit 8b5274e)

See those commits for detailed explanations of the changes. Configure IST and FLST as follows in `stripes.config.js`:
```
config: {
  //...
  useSecureTokens: true,
  rtr: {
    // IST: how long before an idle session is killed? default: 60m.
    // this value must be shorter than the RT's TTL.
    // must be a string parseable by ms, e.g. 60s, 10m, 1h
    idleSessionTTL: '10m',

    // IST: how long to show the "warning, session is idle" modal? default: 1m.
    // this value must be shorter than the idleSessionTTL.
    // must be a string parseable by ms, e.g. 60s, 10m, 1h
    idleModalTTL: '30s',

    // IST: which events constitute "activity" that prolongs a session?
    // default: keydown, mousedown
    activityEvents: ['keydown', 'mousedown', 'wheel', 'touchstart', 'scroll'],

    // FLST: how long to show the "session ending" warning before it ends? default: 1m
    // must be a  string parseable by ms, e.g. 60s, 10m, 1h
    fixedLengthSessionWarningTTL: '30s',
  }
}
```

Turn on the logging channels `rtr` and `rtrv` (verbose) for IST logging, `rtr-fls` for FLST logging.

---------

Co-authored-by: Ryan Berger <rberger@ebsco.com>
  • Loading branch information
zburke and ryandberger authored Jul 26, 2024
1 parent 3fef2ba commit 3c10177
Show file tree
Hide file tree
Showing 55 changed files with 3,005 additions and 1,133 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
## [10.1.1](https://github.com/folio-org/stripes-core/tree/v10.1.1) (2024-03-25)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.1.0...v10.1.1)

* Utilize the `tenant` procured through the SSO login process. Refs STCOR-769.
* Use keycloak URLs in place of users-bl for tenant-switch. Refs US1153537.
* Idle-session timeout and "Keep working?" modal. Refs STCOR-776.
* Always retrieve `clientId` and `tenant` values from `config.tenantOptions` in stripes.config.js. Retires `okapi.tenant`, `okapi.clientId`, and `config.isSingleTenant`. Refs STCOR-787.
* Correctly evaluate `stripes.okapi` before rendering `<RootWithIntl>`. Refs STCOR-864.
* `/users-keycloak/_self` is an authentication request. Refs STCOR-866.
* Terminate the session when the fixed-length session expires. Refs STCOR-862.

## [10.1.0](https://github.com/folio-org/stripes-core/tree/v10.1.0) (2024-03-12)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.0.0...v10.1.0)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"localforage": "^1.5.6",
"lodash": "^4.17.21",
"moment-timezone": "^0.5.14",
"ms": "^2.1.3",
"prop-types": "^15.5.10",
"query-string": "^7.1.2",
"react-cookie": "^4.0.3",
Expand Down
2 changes: 1 addition & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default class StripesCore extends Component {
const parsedTenant = storedTenant ? JSON.parse(storedTenant) : undefined;

const okapi = (typeof okapiConfig === 'object' && Object.keys(okapiConfig).length > 0)
? { ...okapiConfig, tenant: parsedTenant?.tenantName || okapiConfig.tenant, clientId: parsedTenant?.clientId || okapiConfig.clientId } : { withoutOkapi: true };
? { ...okapiConfig, tenant: parsedTenant?.tenantName || okapiConfig.tenant, clientId: parsedTenant?.clientId } : { withoutOkapi: true };

const initialState = merge({}, { okapi }, props.initialState);

Expand Down
306 changes: 149 additions & 157 deletions src/RootWithIntl.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import React from 'react';
import { useState } from 'react';
import PropTypes from 'prop-types';
import {
Router,
Switch,
Redirect as InternalRedirect
} from 'react-router-dom';

import { Provider } from 'react-redux';
import { CookiesProvider } from 'react-cookie';

Expand All @@ -29,175 +27,169 @@ import {
Settings,
HandlerManager,
TitleManager,
Logout,
LogoutTimeout,
OverlayContainer,
CreateResetPassword,
CheckEmailStatusPage,
ForgotPasswordCtrl,
ForgotUserNameCtrl,
AppCtxMenuProvider,
SessionEventContainer,
} from './components';
import StaleBundleWarning from './components/StaleBundleWarning';
import { StripesContext } from './StripesContext';
import { CalloutContext } from './CalloutContext';
import AuthnLogin from './components/AuthnLogin';

export const renderLogoutComponent = () => {
return <InternalRedirect to="/" />;
};
const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAuth, history = {} }) => {
const connect = connectFor('@folio/core', stripes.epics, stripes.logger);
const connectedStripes = stripes.clone({ connect });

class RootWithIntl extends React.Component {
static propTypes = {
stripes: PropTypes.shape({
clone: PropTypes.func.isRequired,
config: PropTypes.object,
epics: PropTypes.object,
logger: PropTypes.object.isRequired,
okapi: PropTypes.object.isRequired,
store: PropTypes.object.isRequired
}).isRequired,
token: PropTypes.string,
isAuthenticated: PropTypes.bool,
disableAuth: PropTypes.bool.isRequired,
history: PropTypes.shape({}),
const [callout, setCallout] = useState(null);
const setCalloutDomRef = (ref) => {
setCallout(ref);
};

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

state = { callout: null };

setCalloutRef = (ref) => {
this.setState({
callout: ref,
});
}

render() {
const {
token,
isAuthenticated,
disableAuth,
history,
} = this.props;

const connect = connectFor('@folio/core', this.props.stripes.epics, this.props.stripes.logger);
const stripes = this.props.stripes.clone({ connect });
return (
<StripesContext.Provider value={connectedStripes}>
<CalloutContext.Provider value={callout}>
<ModuleTranslator>
<TitleManager>
<HotKeys
keyMap={connectedStripes.bindings}
noWrapper
>
<Provider store={connectedStripes.store}>
<Router history={history}>
{ isAuthenticated || token || disableAuth ?
<>
<MainContainer>
<AppCtxMenuProvider>
<MainNav stripes={connectedStripes} />
{typeof connectedStripes?.config?.staleBundleWarning === 'object' && <StaleBundleWarning />}
<HandlerManager
event={events.LOGIN}
stripes={connectedStripes}
/>
{ (typeof connectedStripes.okapi !== 'object' || connectedStripes.discovery.isFinished) && (
<ModuleContainer id="content">
<OverlayContainer />
{connectedStripes.config.useSecureTokens && <SessionEventContainer history={history} />}
<Switch>
<TitledRoute
name="home"
path="/"
key="root"
exact
component={<Front stripes={connectedStripes} />}
/>
<TitledRoute
name="ssoRedirect"
path="/sso-landing"
key="sso-landing"
component={<SSORedirect stripes={connectedStripes} />}
/>
<TitledRoute
name="oidcRedirect"
path="/oidc-landing"
key="oidc-landing"
component={<OIDCRedirect stripes={stripes} />}
/>
<TitledRoute
name="logoutTimeout"
path="/logout-timeout"
component={<LogoutTimeout />}
/>
<TitledRoute
name="settings"
path="/settings"
component={<Settings stripes={connectedStripes} />}
/>
<TitledRoute
name="logout"
path="/logout"
component={<Logout history={history} />}
/>
<ModuleRoutes stripes={connectedStripes} />
</Switch>
</ModuleContainer>
)}
</AppCtxMenuProvider>
</MainContainer>
<Callout ref={setCalloutDomRef} />
</> :
<Switch>
{/* The ? after :token makes that part of the path optional, so that token may optionally
be passed in via URL parameter to avoid length restrictions */}
<TitledRoute
name="CreateResetPassword"
path="/reset-password/:token?"
component={<CreateResetPassword stripes={connectedStripes} />}
/>
<TitledRoute
name="ssoLanding"
exact
path="/sso-landing"
component={<CookiesProvider><SSOLanding stripes={connectedStripes} /></CookiesProvider>}
key="sso-landing"
/>
<TitledRoute
name="oidcLanding"
exact
path="/oidc-landing"
component={<CookiesProvider><OIDCLanding stripes={stripes} /></CookiesProvider>}
key="oidc-landing"
/>
<TitledRoute
name="forgotPassword"
path="/forgot-password"
component={<ForgotPasswordCtrl stripes={connectedStripes} />}
/>
<TitledRoute
name="forgotUsername"
path="/forgot-username"
component={<ForgotUserNameCtrl stripes={connectedStripes} />}
/>
<TitledRoute
name="checkEmail"
path="/check-email"
component={<CheckEmailStatusPage />}
/>
<TitledRoute
name="logoutTimeout"
path="/logout-timeout"
component={<LogoutTimeout />}
/>
<TitledRoute
name="login"
component={<AuthnLogin stripes={connectedStripes} />}
/>
</Switch>
}
</Router>
</Provider>
</HotKeys>
</TitleManager>
</ModuleTranslator>
</CalloutContext.Provider>
</StripesContext.Provider>
);
};

return (
<StripesContext.Provider value={stripes}>
<CalloutContext.Provider value={this.state.callout}>
<ModuleTranslator>
<TitleManager>
<HotKeys
keyMap={stripes.bindings}
noWrapper
>
<Provider store={stripes.store}>
<Router history={history}>
{ isAuthenticated || token || disableAuth ?
<>
<MainContainer>
<AppCtxMenuProvider>
<MainNav stripes={stripes} />
{typeof stripes?.config?.staleBundleWarning === 'object' && <StaleBundleWarning />}
<HandlerManager
event={events.LOGIN}
stripes={stripes}
/>
{ (stripes.okapi !== 'object' || stripes.discovery.isFinished) && (
<ModuleContainer id="content">
<OverlayContainer />
<Switch>
<TitledRoute
name="home"
path="/"
key="root"
exact
component={<Front stripes={stripes} />}
/>
<TitledRoute
name="ssoRedirect"
path="/sso-landing"
key="sso-landing"
component={<SSORedirect stripes={stripes} />}
/>
<TitledRoute
name="oidcRedirect"
path="/oidc-landing"
key="oidc-landing"
component={<OIDCRedirect stripes={stripes} />}
/>
<TitledRoute
name="settings"
path="/settings"
component={<Settings stripes={stripes} />}
/>
<ModuleRoutes stripes={stripes} />
</Switch>
</ModuleContainer>
)}
</AppCtxMenuProvider>
</MainContainer>
<Callout ref={this.setCalloutRef} />
</> :
<Switch>
<TitledRoute
name="CreateResetPassword"
path="/reset-password/:token?"
component={<CreateResetPassword stripes={stripes} />}
/>
<TitledRoute
name="ssoLanding"
exact
path="/sso-landing"
component={<CookiesProvider><SSOLanding stripes={stripes} /></CookiesProvider>}
key="sso-landing"
/>
<TitledRoute
name="oidcLanding"
exact
path="/oidc-landing"
component={<CookiesProvider><OIDCLanding stripes={stripes} /></CookiesProvider>}
key="oidc-landing"
/>
<TitledRoute
name="forgotPassword"
path="/forgot-password"
component={<ForgotPasswordCtrl stripes={stripes} />}
/>
<TitledRoute
name="forgotUsername"
path="/forgot-username"
component={<ForgotUserNameCtrl stripes={stripes} />}
/>
<TitledRoute
name="checkEmail"
path="/check-email"
component={<CheckEmailStatusPage />}
/>
<TitledRoute
name="logout"
path="/logout"
component={renderLogoutComponent()}
/>
<TitledRoute
name="login"
component={<AuthnLogin stripes={this.props.stripes} />}
/>
</Switch>
}
</Router>
</Provider>
</HotKeys>
</TitleManager>
</ModuleTranslator>
</CalloutContext.Provider>
</StripesContext.Provider>
);
}
}
RootWithIntl.propTypes = {
stripes: PropTypes.shape({
clone: PropTypes.func.isRequired,
config: PropTypes.object,
epics: PropTypes.object,
logger: PropTypes.object.isRequired,
okapi: PropTypes.object.isRequired,
store: PropTypes.object.isRequired
}).isRequired,
token: PropTypes.string,
isAuthenticated: PropTypes.bool,
disableAuth: PropTypes.bool.isRequired,
history: PropTypes.shape({}),
};

export default RootWithIntl;
Loading

0 comments on commit 3c10177

Please sign in to comment.