Skip to content

Commit

Permalink
STCOR-902 show error message on OIDC fetch failure
Browse files Browse the repository at this point in the history
When returning from a SAML/OIDC request, stripes makes an API request to
exchange the token for cookies. If that API request fails, show an error
and a button prompting the user to try authenticating again.

Previously, instead of showing a button, stripes would automatically
redirect to `/`, which (because there were no cookies) would redirect to
the authentication URL, which (because there _were_ still valid
authentication cookies) would redirect to stripes, starting an endless
circle.

Refs STCOR-902
  • Loading branch information
zburke committed Nov 4, 2024
1 parent 031a523 commit 4f248ae
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 37 deletions.
88 changes: 55 additions & 33 deletions src/components/OIDCLanding.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
import React, { useEffect, useState } from 'react';
import { useLocation, Redirect } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import queryString from 'query-string';
import { useStore } from 'react-redux';
import { FormattedMessage } from 'react-intl';

import { Loading } from '@folio/stripes-components';
import {
Button,
Col,
Headline,
Loading,
Row,
} from '@folio/stripes-components';

import OrganizationLogo from './OrganizationLogo';
import { requestUserWithPerms, setTokenExpiry } from '../loginServices';

import css from './Front.css';
import { useStripes } from '../StripesContext';

/**
* OIDCLanding: un-authenticated route handler for /sso-landing.
* OIDCLanding: un-authenticated route handler for /oidc-landing.
*
* Reads one-time-code from URL params, exchanging it for an access_token
* and then leveraging that to retrieve a user via requestUserWithPerms,
* eventually dispatching session and Okapi-ready, resulting in a
* re-render of RoothWithIntl with prop isAuthenticated: true.
* * Read one-time-code from URL params
* * make an API call to /authn/token to exchange the OTP for cookies
* * call requestUserWithPerms to make an API call to .../_self,
* eventually dispatching session and Okapi-ready, resulting in a
* re-render of RoothWithIntl with prop isAuthenticated: true
*
* @see RootWithIntl
*/
const OIDCLanding = () => {
const location = useLocation();
const store = useStore();
// const samlError = useRef();
const { okapi } = useStripes();
const [potp, setPotp] = useState();
const [samlError, setSamlError] = useState();

const [oidcError, setOIDCError] = useState();

/**
* Exchange the otp for AT/RT cookies, then retrieve the user.
Expand Down Expand Up @@ -56,7 +61,6 @@ const OIDCLanding = () => {
const otp = getOtp();

if (otp) {
setPotp(otp);
fetch(`${okapi.url}/authn/token?code=${otp}&redirect-uri=${window.location.protocol}//${window.location.host}/oidc-landing`, {
credentials: 'include',
headers: { 'X-Okapi-tenant': okapi.tenant, 'Content-Type': 'application/json' },
Expand All @@ -83,7 +87,7 @@ const OIDCLanding = () => {
.catch(e => {
// eslint-disable-next-line no-console
console.error('@@ Oh, snap, OTP exchange failed!', e);
setSamlError(e);
setOIDCError(e);
});
}
// we only want to run this effect once, on load.
Expand All @@ -93,22 +97,45 @@ const OIDCLanding = () => {
// store: the redux store
}, []); // eslint-disable-line react-hooks/exhaustive-deps

if (samlError) {
/**
* formatOIDCError
* Return formatted OIDC error message, or null
* @returns
*/
const formatOIDCError = () => {
if (Array.isArray(oidcError?.errors)) {
return (
<Row center="xs">
<Col xs={12}>
<Headline>{oidcError.errors[0]?.message}</Headline>
</Col>
</Row>
);
}

return null;
};

if (oidcError) {
return (
<div data-test-saml-error>
<div>
<FormattedMessage id="errors.saml.missingToken" />
</div>
<div>
<h3>code</h3>
{potp}
<h3>error</h3>
<code>
{JSON.stringify(samlError, null, 2)}
</code>
</div>
<Redirect to="/" />
</div>
<main data-test-saml-error>
<Row center="xs">
<Col xs={12}>
<OrganizationLogo />
</Col>
</Row>
<Row center="xs">
<Col xs={12}>
<Headline size="large"><FormattedMessage id="stripes-core.errors.oidc" /></Headline>
</Col>
</Row>
{formatOIDCError()}
<Row center="xs">
<Col xs={12}>
<Button to="/"><FormattedMessage id="stripes-core.rtr.idleSession.logInAgain" /></Button>
</Col>
</Row>
</main>
);
}

Expand All @@ -117,11 +144,6 @@ const OIDCLanding = () => {
<div className={css.frontWrap}>
<Loading size="xlarge" />
</div>
<div>
<pre>
{JSON.stringify(samlError, null, 2)}
</pre>
</div>
</div>
);
};
Expand Down
5 changes: 2 additions & 3 deletions src/components/OIDCLanding.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ jest.mock('../StripesContext', () => ({
}),
}));

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

jest.mock('./OrganizationLogo', () => (() => <div>OrganizationLogo</div>));

const mockSetTokenExpiry = jest.fn();
const mockRequestUserWithPerms = jest.fn();
Expand Down Expand Up @@ -79,7 +78,7 @@ describe('OIDCLanding', () => {
mockFetchError('barf');

await render(<OIDCLanding />);
await screen.findByText('errors.saml.missingToken');
await screen.findByText('stripes-core.errors.oidc');
mockFetchCleanUp();
});
});
1 change: 1 addition & 0 deletions translations/stripes-core/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
"errors.password.consecutiveWhitespaces.invalid": "The password must not contain consecutive white space characters.",
"errors.password.compromised.invalid": "The password must not be commonly-used, expected or compromised",
"errors.saml.missingToken": "No <code>code</code> query parameter.",
"errors.oidc": "Error: server is forbidden, unreachable, or unavailable.",

"createResetPassword.header": "Choose a password",
"createResetPassword.newPassword": "New Password",
Expand Down
4 changes: 3 additions & 1 deletion translations/stripes-core/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,7 @@
"tenantLibrary": "Tenant/Library",
"errors.saml.missingToken": "No <code>code</code> query parameter.",
"rtr.fixedLengthSession.timeRemaining": "Your session will end soon! Time remaining:",
"logoutComplete": "You have logged out."
"logoutComplete": "You have logged out.",
"errors.oidc": "Error: server is forbidden, unreachable, or unavailable."

}

0 comments on commit 4f248ae

Please sign in to comment.