Skip to content

Commit

Permalink
STCOR-789: Restore original URL on login (#1442)
Browse files Browse the repository at this point in the history
Refs STCOR-789.
  • Loading branch information
aidynoJ committed Mar 18, 2024
1 parent ad24fa7 commit 7b90d22
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 34 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ artifacts
dist
junit.xml
.vscode/launch.json
.idea
31 changes: 2 additions & 29 deletions src/RootWithIntl.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { Callout, HotKeys } from '@folio/stripes-components';

import ModuleRoutes from './moduleRoutes';
import events from './events';
import Redirect from './components/Redirect';

import {
MainContainer,
Expand All @@ -30,7 +29,6 @@ import {
Settings,
HandlerManager,
TitleManager,
Login,
OverlayContainer,
CreateResetPassword,
CheckEmailStatusPage,
Expand All @@ -41,37 +39,12 @@ import {
import StaleBundleWarning from './components/StaleBundleWarning';
import { StripesContext } from './StripesContext';
import { CalloutContext } from './CalloutContext';
import PreLoginLanding from './components/PreLoginLanding';
import { setOkapiTenant } from './okapiActions';
import AuthnLogin from './components/AuthnLogin';

export const renderLogoutComponent = () => {
return <InternalRedirect to="/" />;
};

export const renderLoginComponent = (stripes) => {
const { config, okapi } = stripes;

if (okapi.authnUrl) {
if (config.isSingleTenant) {
const redirectUri = `${window.location.protocol}//${window.location.host}/oidc-landing`;
const authnUri = `${okapi.authnUrl}/realms/${okapi.tenant}/protocol/openid-connect/auth?client_id=${okapi.clientId}&response_type=code&redirect_uri=${redirectUri}&scope=openid`;
return <Redirect to={authnUri} />;
}

const handleSelectTenant = (tenant, clientId) => {
localStorage.setItem('tenant', JSON.stringify({ tenantName: tenant, clientId }));
stripes.store.dispatch(setOkapiTenant({ tenant, clientId }));
};

return <PreLoginLanding onSelectTenant={handleSelectTenant} />;
}

return <Login
autoLogin={config.autoLogin}
stripes={stripes}
/>;
};

class RootWithIntl extends React.Component {
static propTypes = {
stripes: PropTypes.shape({
Expand Down Expand Up @@ -212,7 +185,7 @@ class RootWithIntl extends React.Component {
/>
<TitledRoute
name="login"
component={renderLoginComponent(this.props.stripes)}
component={<AuthnLogin stripes={this.props.stripes} />}
/>
</Switch>
}
Expand Down
11 changes: 6 additions & 5 deletions src/RootWithIntl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import { Login } from './components';
import PreLoginLanding from './components/PreLoginLanding';

import {
renderLoginComponent,
renderLogoutComponent
} from './RootWithIntl';

import AuthnLogin from './components/AuthnLogin';

jest.mock('react-router-dom', () => ({
Redirect: () => '<internalredirect>',
withRouter: (Component) => Component,
Expand All @@ -22,10 +23,10 @@ jest.mock('./components/Login', () => () => '<login>');
jest.mock('./components/PreLoginLanding', () => () => '<preloginlanding>');

describe('RootWithIntl', () => {
describe('renderLoginComponent', () => {
describe('AuthnLogin', () => {
it('handles legacy login', () => {
const stripes = { okapi: {}, config: {} };
render(renderLoginComponent(stripes));
render(<AuthnLogin stripes={stripes} />);

expect(screen.getByText(/<login>/)).toBeInTheDocument();
});
Expand All @@ -36,7 +37,7 @@ describe('RootWithIntl', () => {
okapi: { authnUrl: 'https://barbie.com' },
config: { isSingleTenant: true }
};
render(renderLoginComponent(stripes));
render(<AuthnLogin stripes={stripes} />);

expect(screen.getByText(/<redirect>/)).toBeInTheDocument();
});
Expand All @@ -46,7 +47,7 @@ describe('RootWithIntl', () => {
okapi: { authnUrl: 'https://oppie.com' },
config: { },
};
render(renderLoginComponent(stripes));
render(<AuthnLogin stripes={stripes} />);

expect(screen.getByText(/<preloginlanding>/)).toBeInTheDocument();
});
Expand Down
49 changes: 49 additions & 0 deletions src/components/AuthnLogin/AuthnLogin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import Redirect from '../Redirect';
import PreLoginLanding from '../PreLoginLanding';
import Login from '../Login';

import { setOkapiTenant } from '../../okapiActions';
import { setUnauthorizedPathToSession } from '../../loginServices';

const AuthnLogin = ({ stripes }) => {
const { config, okapi } = stripes;

useEffect(() => {
if (okapi.authnUrl) {
/** Store unauthorized pathname to session storage. Refs STCOR-789
* @see OIDCRedirect
*/
setUnauthorizedPathToSession(window.location.pathname);
}
// we only want to run this effect once, on load.
// okapi.authnUrl are defined in stripes.config.js
}, []); // eslint-disable-line react-hooks/exhaustive-deps

if (okapi.authnUrl) {
if (config.isSingleTenant) {
const redirectUri = `${window.location.protocol}//${window.location.host}/oidc-landing`;
const authnUri = `${okapi.authnUrl}/realms/${okapi.tenant}/protocol/openid-connect/auth?client_id=${okapi.clientId}&response_type=code&redirect_uri=${redirectUri}&scope=openid`;
return <Redirect to={authnUri} />;
}

const handleSelectTenant = (tenant, clientId) => {
localStorage.setItem('tenant', JSON.stringify({ tenantName: tenant, clientId }));
stripes.store.dispatch(setOkapiTenant({ tenant, clientId }));
};

return <PreLoginLanding onSelectTenant={handleSelectTenant} />;
}

return <Login
autoLogin={config.autoLogin}
stripes={stripes}
/>;
};

AuthnLogin.propTypes = {
stripes: PropTypes.object
};

export default AuthnLogin;
1 change: 1 addition & 0 deletions src/components/AuthnLogin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './AuthnLogin';
13 changes: 13 additions & 0 deletions src/components/OIDCRedirect.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { withRouter, Redirect, useLocation } from 'react-router';
import queryString from 'query-string';
import { useStripes } from '../StripesContext';
import { getUnauthorizedPathFromSession } from '../loginServices';

/**
* OIDCRedirect authenticated route handler for /oidc-landing.
*
* Read unauthorized_path from session storage if keycloak authn provided
* if `fwd` provided into redirect url, it causes strange behavior - infinite login requests
* decided to use session storage for that case. Refs STCOR-789
*
* Reads `fwd` from URL params and redirects.
*
* @see RootWithIntl
* @see AuthnLogin
*
* @returns {Redirect}
*/
const OIDCRedirect = () => {
const location = useLocation();
const stripes = useStripes();

const getParams = () => {
const search = location.search;
Expand All @@ -20,6 +28,11 @@ const OIDCRedirect = () => {
};

const getUrl = () => {
if (stripes.okapi.authnUrl) {
const unauthorizedPath = getUnauthorizedPathFromSession();
if (unauthorizedPath) return unauthorizedPath;
}

const params = getParams();
return params?.fwd ?? '';
};
Expand Down
40 changes: 40 additions & 0 deletions src/components/OIDCRedirect.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { render, screen } from '@folio/jest-config-stripes/testing-library/react';
import OIDCRedirect from './OIDCRedirect';
import { useStripes } from '../StripesContext';

jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
Redirect: () => <div>internalredirect</div>,
withRouter: Component => Component,
useLocation: () => ({
search: '?fwd=/dashboard',
}),
}));

jest.mock('../StripesContext');

describe('OIDCRedirect', () => {
beforeAll(() => {
sessionStorage.setItem(
'unauthorized_path',
'/example'
);
});

afterAll(() => sessionStorage.removeItem('unauthorized_path'));

it('redirects to value from session storage under unauthorized_path key', () => {
useStripes.mockReturnValue({ okapi:{ authnUrl: 'http://example.com/authn' } });
render(<OIDCRedirect />);

expect(screen.getByText(/internalredirect/)).toBeInTheDocument();
});

it('redirects fwd if no authn provided to stripes okapi config', () => {
useStripes.mockReturnValue({ okapi: { } });
render(<OIDCRedirect />);

expect(screen.getByText(/internalredirect/)).toBeInTheDocument();
});
});
12 changes: 12 additions & 0 deletions src/loginServices.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ export const setTokenExpiry = async (te) => {
return localforage.setItem(SESSION_NAME, { ...sess, tokenExpiration: te });
};

/**
* removeUnauthorizedPathFromSession, setUnauthorizedPathToSession, getUnauthorizedPathFromSession
* Add/remove/get unauthorized_path to/from session storage;
* Used to restore path if user is unauthorized.
* @see OIDCRedirect
*/

export const removeUnauthorizedPathFromSession = () => sessionStorage.removeItem('unauthorized_path');
export const setUnauthorizedPathToSession = (pathname) => sessionStorage.setItem('unauthorized_path', pathname);
export const getUnauthorizedPathFromSession = () => sessionStorage.getItem('unauthorized_path');


// export config values for storing user locale
export const userLocaleConfig = {
Expand Down Expand Up @@ -449,6 +460,7 @@ export async function logout(okapiUrl, store) {
.then(localStorage.removeItem('tenant'))
.then(localforage.removeItem(SESSION_NAME))
.then(localforage.removeItem('loginResponse'))
.then(removeUnauthorizedPathFromSession)
.catch((error) => {
// eslint-disable-next-line no-console
console.log(`Error logging out: ${JSON.stringify(error)}`);
Expand Down

0 comments on commit 7b90d22

Please sign in to comment.