Skip to content

Commit

Permalink
[STCOR-787] Always retrieve clientId and tenant values from config.te…
Browse files Browse the repository at this point in the history
…nantOptions in stripes.config.js (#1487)

* Retrieve clientId and tenant values from config.tenantOptions before login

* Fix tenant gathering

* Remove isSingleTenant param which is redundant

* If user object not returned from local storage, then default user from /_self response

* Update CHANGELOG.md

* Revert PreLoginLanding which uses okapi values

* Remove space

* Rework flow to immediately set config to okapi for compatibility.

* Lint fix

* Fix unit test

(cherry picked from commit e738a2f)
  • Loading branch information
ryandberger authored and zburke committed Jul 25, 2024
1 parent b17e9ad commit 2d712ea
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 14 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Change history for stripes-core

<<<<<<< HEAD
## [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.

## [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
30 changes: 27 additions & 3 deletions src/RootWithIntl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,21 @@ jest.mock('./components/Redirect', () => () => '<redirect>');
jest.mock('./components/Login', () => () => '<login>');
jest.mock('./components/PreLoginLanding', () => () => '<preloginlanding>');

const store = {
getState: () => ({
okapi: {
token: '123',
},
}),
dispatch: () => {},
subscribe: () => {},
replaceReducer: () => {},
};

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

expect(screen.getByText(/<login>/)).toBeInTheDocument();
Expand All @@ -35,7 +46,13 @@ describe('RootWithIntl', () => {
it('handles single-tenant', () => {
const stripes = {
okapi: { authnUrl: 'https://barbie.com' },
config: { isSingleTenant: true }
config: {
isSingleTenant: true,
tenantOptions: {
diku: { name: 'diku', clientId: 'diku-application' }
}
},
store
};
render(<AuthnLogin stripes={stripes} />);

Expand All @@ -45,7 +62,14 @@ describe('RootWithIntl', () => {
it('handles multi-tenant', () => {
const stripes = {
okapi: { authnUrl: 'https://oppie.com' },
config: { },
config: {
isSingleTenant: false,
tenantOptions: {
diku: { name: 'diku', clientId: 'diku-application' },
diku2: { name: 'diku2', clientId: 'diku2-application' }
}
},
store
};
render(<AuthnLogin stripes={stripes} />);

Expand Down
1 change: 1 addition & 0 deletions src/Stripes.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const stripesShape = PropTypes.shape({
logTimestamp: PropTypes.bool,
showHomeLink: PropTypes.bool,
showPerms: PropTypes.bool,
tenantOptions: PropTypes.object,
}).isRequired,
connect: PropTypes.func.isRequired,
currency: PropTypes.string,
Expand Down
27 changes: 19 additions & 8 deletions src/components/AuthnLogin/AuthnLogin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import { setUnauthorizedPathToSession } from '../../loginServices';

const AuthnLogin = ({ stripes }) => {
const { config, okapi } = stripes;
// If config.tenantOptions is not defined, default to classic okapi.tenant and okapi.clientId
const { tenantOptions = [{ name: okapi.tenant, clientId: okapi.clientId }] } = config;
const tenants = Object.values(tenantOptions);

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

useEffect(() => {
if (okapi.authnUrl) {
Expand All @@ -17,23 +25,26 @@ const AuthnLogin = ({ stripes }) => {
*/
setUnauthorizedPathToSession(window.location.pathname);
}

// If only 1 tenant is defined in config (in either okapi or config.tenantOptions) set to okapi to be accessed there
// in the rest of the application for compatibity across existing modules.
if (tenants.length === 1) {
const loginTenant = tenants[0];
setTenant(loginTenant.name, loginTenant.clientId);
}
// we only want to run this effect once, on load.
// okapi.authnUrl are defined in stripes.config.js
// okapi.authnUrl tenant values are defined in stripes.config.js
}, []); // eslint-disable-line react-hooks/exhaustive-deps

if (okapi.authnUrl) {
if (config.isSingleTenant) {
// If only 1 tenant is defined in config, skip the tenant selection screen.
if (tenants.length === 1) {
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 <PreLoginLanding onSelectTenant={setTenant} />;
}

return <Login
Expand Down
1 change: 0 additions & 1 deletion src/components/OIDCLanding.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const OIDCLanding = () => {
const store = useStore();
// const samlError = useRef();
const { okapi } = useStripes();

const [potp, setPotp] = useState();
const [samlError, setSamlError] = useState();

Expand Down
85 changes: 85 additions & 0 deletions src/components/OIDCLanding.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { render, screen, waitFor } from '@folio/jest-config-stripes/testing-library/react';

import OIDCLanding from './OIDCLanding';

jest.mock('react-router-dom', () => ({
useLocation: () => ({
search: 'session_state=dead-beef&code=c0ffee'
}),
Redirect: () => <>Redirect</>,
}));

jest.mock('react-redux', () => ({
useStore: () => { },
}));

jest.mock('../StripesContext', () => ({
useStripes: () => ({
okapi: { url: 'https://whaterver' },
config: { tenantOptions: { diku: { name: 'diku', clientId: 'diku-application' } } },
}),
}));

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


const mockSetTokenExpiry = jest.fn();
const mockRequestUserWithPerms = jest.fn();
const mockFoo = jest.fn();
jest.mock('../loginServices', () => ({
setTokenExpiry: () => mockSetTokenExpiry(),
requestUserWithPerms: () => mockRequestUserWithPerms(),
foo: () => mockFoo(),
}));


// fetch success: resolve promise with ok == true and $data in json()
const mockFetchSuccess = (data) => {
global.fetch = jest.fn().mockImplementation(() => (
Promise.resolve({
ok: true,
json: () => Promise.resolve(data),
headers: new Map(),
})
));
};

// fetch failure: resolve promise with ok == false and $error in json()
const mockFetchError = (error) => {
global.fetch = jest.fn().mockImplementation(() => (
Promise.resolve({
ok: false,
json: () => Promise.resolve(error),
headers: new Map(),
})
));
};

// restore default fetch impl
const mockFetchCleanUp = () => {
global.fetch.mockClear();
delete global.fetch;
};

describe('OIDCLanding', () => {
it('calls requestUserWithPerms, setTokenExpiry on success', async () => {
mockFetchSuccess({
accessTokenExpiration: '2024-05-23T09:47:17.000-04:00',
refreshTokenExpiration: '2024-05-23T10:07:17.000-04:00',
});

await render(<OIDCLanding />);
screen.getByText('Loading');
await waitFor(() => expect(mockSetTokenExpiry).toHaveBeenCalledTimes(1));
await waitFor(() => expect(mockRequestUserWithPerms).toHaveBeenCalledTimes(1));
mockFetchCleanUp();
});

it('displays an error on failure', async () => {
mockFetchError('barf');

await render(<OIDCLanding />);
await screen.findByText('errors.saml.missingToken');
mockFetchCleanUp();
});
});
2 changes: 1 addition & 1 deletion src/components/OIDCRedirect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('OIDCRedirect', () => {
afterAll(() => sessionStorage.removeItem('unauthorized_path'));

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

expect(screen.getByText(/internalredirect/)).toBeInTheDocument();
Expand Down

0 comments on commit 2d712ea

Please sign in to comment.