Skip to content

Commit

Permalink
story: token expiry (awslabs#563)
Browse files Browse the repository at this point in the history
* story: force logout component

* chore: Trigger Build

* fix: add flag for UI component test

* fix: adding afterall logic for force-logout test
  • Loading branch information
SanketD92 authored Jul 6, 2021
1 parent cb71d96 commit 3ca08e2
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 0 deletions.
12 changes: 12 additions & 0 deletions addons/addon-base-ui/packages/base-ui/src/AppContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,20 @@ class AppContainer extends Component {
return false; // This will stop lodash from continuing the forEach loop
});

plugins = _.reverse(pluginRegistry.getPluginsWithMethod('app-component', 'getForceLogoutComponent') || []);
let ForceLogout = () => <></>;
// We ask each plugin in reverse order if they have the ForceLogout component
_.forEach(plugins, plugin => {
const result = plugin.getForceLogoutComponent({ location, appContext: getEnv(app) });
if (_.isUndefined(result)) return;
ForceLogout = result;
// eslint-disable-next-line consistent-return
return false; // This will stop lodash from continuing the forEach loop
});

return (
<>
<ForceLogout />
<AutoLogout />
<App />
</>
Expand Down
119 changes: 119 additions & 0 deletions addons/addon-base-ui/packages/base-ui/src/parts/ForceLogout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import _ from 'lodash';
import React from 'react';
import { observable, action, decorate, computed, runInAction } from 'mobx';
import { observer, inject } from 'mobx-react';
import { Button, Modal } from 'semantic-ui-react';

const jwtDecode = require('jwt-decode');

// expected props
// - authentication
// - app
class ForceLogout extends React.Component {
constructor(props) {
super(props);
runInAction(() => {
this.tokenActive = true;
});
}

get app() {
return this.props.app;
}

get authentication() {
return this.props.authentication;
}

get modalOpen() {
return !this.tokenActive;
}

componentDidMount() {
this.timer = setInterval(() => {
runInAction(() => {
this.tokenActive = !this.hasTokenExpired();
});
}, 1000);
}

componentWillUnmount() {
clearInterval(this.timer);
}

doLogout = async () => {
clearInterval(this.timer);
return this.authentication.logout({ autoLogout: true });
};

hasTokenExpired = () => {
try {
const idToken = localStorage.getItem('appIdToken');
const decodedIdToken = jwtDecode(idToken);
const expiresAt = _.get(decodedIdToken, 'exp', 0) * 1000;
// The next line computes how many minutes are left until token expiration
return (expiresAt - Date.now()) / 60 / 1000 < 0;
} catch (e) {
return false;
}
};

handleLogout = async event => {
event.preventDefault();
event.stopPropagation();
return this.doLogout();
};

renderModal() {
if (this.tokenActive) {
return null;
}
return (
<>
<Modal open={this.modalOpen} closeOnEscape={false} closeOnDimmerClick={false} centered={false}>
<Modal.Header>Session expired</Modal.Header>
<Modal.Content className="center">
<div>Your session has expired. Close Service Workbench and log in again.</div>
</Modal.Content>
<Modal.Actions className="clearfix">
<Button floated="right" content="Log Out" onClick={this.handleLogout} />
</Modal.Actions>
</Modal>
</>
);
}

render() {
const authenticated = this.app.userAuthenticated;
if (!authenticated) return null;
return <>{this.renderModal()}</>;
}
}

// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
decorate(ForceLogout, {
app: computed,
authentication: computed,
modalOpen: computed,
tokenActive: observable,
doLogout: action,
handleLogout: action,
clearInterval: action,
});

export default inject('authentication', 'app')(observer(ForceLogout));
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import React from 'react';
import { shallow } from 'enzyme';
import { Button, Modal } from 'semantic-ui-react';
import ForceLogout from '../ForceLogout';

const AuthenticationProviderConfigsStore = { logout: jest.fn() };
const app = { userAuthenticated: true };
describe('ForceLogout', () => {
let wrapper = null;
let renderModalSnapshot = null;
let renderSnapshot = null;
beforeAll(() => {
wrapper = shallow(<ForceLogout.wrappedComponent authentication={AuthenticationProviderConfigsStore} app={app} />);
const component = wrapper.instance();
component.tokenActive = false;

renderModalSnapshot = (
<>
<Modal open={component.modalOpen} closeOnEscape={false} closeOnDimmerClick={false} centered={false}>
<Modal.Header>Session expired</Modal.Header>
<Modal.Content className="center">
<div>Your session has expired. Close Service Workbench and log in again.</div>
</Modal.Content>
<Modal.Actions className="clearfix">
<Button floated="right" content="Log Out" onClick={component.handleLogout} />
</Modal.Actions>
</Modal>
</>
);
renderSnapshot = (
<>
<>
<Modal open={component.modalOpen} closeOnEscape={false} closeOnDimmerClick={false} centered={false}>
<Modal.Header>Session expired</Modal.Header>
<Modal.Content className="center">
<div>Your session has expired. Close Service Workbench and log in again.</div>
</Modal.Content>
<Modal.Actions className="clearfix">
<Button floated="right" content="Log Out" onClick={component.handleLogout} />
</Modal.Actions>
</Modal>
</>
</>
);
});

it('should exist', () => {
expect(ForceLogout).not.toBeNull();
expect(ForceLogout.displayName).toBe('inject-with-authentication-app(ForceLogout)');
});

it('should make renderModal return null when tokenActive is true', () => {
const component = wrapper.instance();
component.tokenActive = true;
expect(component.renderModal()).toEqual(null);
});

it('should make renderModal return modal when tokenActive is false', () => {
const component = wrapper.instance();
component.tokenActive = false;
expect(component.renderModal()).toEqual(renderModalSnapshot);
});

it('should make render return modal when user is authenticated', () => {
const component = wrapper.instance();
expect(component.render()).toEqual(renderSnapshot);
});

it('should make render return null when user is not authenticated', () => {
const userAuth = { userAuthenticated: false };
const tempWrapper = shallow(
<ForceLogout.wrappedComponent authentication={AuthenticationProviderConfigsStore} app={userAuth} />,
);
const component = tempWrapper.instance();
expect(component.render()).not.toEqual(null);
});

afterAll(async () => {
await setTimeout(() => process.exit(0), 1000);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import App from '../App';
import AutoLogout from '../parts/AutoLogout';
import ForceLogout from '../parts/ForceLogout';

// eslint-disable-next-line no-unused-vars
function getAppComponent({ location, appContext }) {
Expand All @@ -26,9 +27,15 @@ function getAutoLogoutComponent({ location, appContext }) {
return AutoLogout;
}

// eslint-disable-next-line no-unused-vars
function getForceLogoutComponent({ location, appContext }) {
return ForceLogout;
}

const plugin = {
getAppComponent,
getAutoLogoutComponent,
getForceLogoutComponent,
};

export default plugin;

0 comments on commit 3ca08e2

Please sign in to comment.