diff --git a/CHANGELOG.md b/CHANGELOG.md index 742c84dab..321e6258c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ * Add `idName` and `limit` as passable props to `useChunkedCQLFetch`. Refs STCOR-821. * Check for valid token before rotating during XHR send. Refs STCOR-817. * Remove `autoComplete` from ``, `` fields. Refs STCOR-742. +* Use keycloak URLs in place of users-bl for tenant-switch. Refs US1153537. ## [10.0.3](https://github.com/folio-org/stripes-core/tree/v10.0.3) (2023-11-10) [Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.0.2...v10.0.3) @@ -55,6 +56,8 @@ [Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.0.0...v10.0.1) * Export `validateUser`. Refs STCOR-749. +* Opt-in: handle access-control via cookies. Refs STCOR-671. +* Opt-in: disable login when cookies are disabled. Refs STCOR-762. ## [10.0.0](https://github.com/folio-org/stripes-core/tree/v10.0.0) (2023-10-11) [Full Changelog](https://github.com/folio-org/stripes-core/compare/v9.0.0...v10.0.0) diff --git a/src/components/Login/Login.js b/src/components/Login/Login.js index 4d491cfba..b2f6e3960 100644 --- a/src/components/Login/Login.js +++ b/src/components/Login/Login.js @@ -1,72 +1,45 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { connect as reduxConnect } from 'react-redux'; -import { - withRouter, - matchPath, -} from 'react-router-dom'; - -import { ConnectContext } from '@folio/stripes-connect'; -import { - requestLogin, - requestSSOLogin, -} from '../../loginServices'; -import { setAuthError } from '../../okapiActions'; - -class LoginCtrl extends Component { - static propTypes = { - authFailure: PropTypes.arrayOf(PropTypes.object), - ssoEnabled: PropTypes.bool, - autoLogin: PropTypes.shape({ - username: PropTypes.string.isRequired, - password: PropTypes.string.isRequired, - }), - clearAuthErrors: PropTypes.func.isRequired, - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - }).isRequired, - location: PropTypes.shape({ - pathname: PropTypes.string.isRequired, - }).isRequired, - }; +import { FormattedMessage } from 'react-intl'; +import { Field, Form } from 'react-final-form'; - static contextType = ConnectContext; +import { branding } from 'stripes-config'; - constructor(props) { - super(props); - this.sys = require('stripes-config'); // eslint-disable-line global-require - this.authnUrl = this.sys.okapi.authnUrl; - this.okapiUrl = this.sys.okapi.url; - this.tenant = this.sys.okapi.tenant; - if (props.autoLogin && props.autoLogin.username) { - this.handleSubmit(props.autoLogin); - } - } +import { + TextField, + Button, + Row, + Col, + Headline, +} from '@folio/stripes-components'; - componentWillUnmount() { - this.props.clearAuthErrors(); - } +import SSOLogin from '../SSOLogin'; +import OrganizationLogo from '../OrganizationLogo'; +import AuthErrorsContainer from '../AuthErrorsContainer'; +import FieldLabel from '../CreateResetPassword/components/FieldLabel'; - handleSuccessfulLogin = () => { - if (matchPath(this.props.location.pathname, '/login')) { - this.props.history.push('/'); - } - } +import styles from './Login.css'; - handleSubmit = (data) => { - return requestLogin({ okapi: this.sys.okapi }, this.context.store, this.tenant, data) - .then(this.handleSuccessfulLogin) - .catch(e => { - console.error(e); // eslint-disable-line no-console - }); - } +class Login extends Component { + static propTypes = { + ssoActive: PropTypes.bool, + authErrors: PropTypes.arrayOf(PropTypes.object), + onSubmit: PropTypes.func.isRequired, + handleSSOLogin: PropTypes.func.isRequired, + }; - handleSSOLogin = () => { - requestSSOLogin(this.okapiUrl, this.tenant); - } + static defaultProps = { + authErrors: [], + ssoActive: false, + }; render() { - const { authFailure, ssoEnabled } = this.props; + const { + authErrors, + handleSSOLogin, + ssoActive, + onSubmit, + } = this.props; const cookieMessage = navigator.cookieEnabled ? '' : @@ -267,12 +240,4 @@ class LoginCtrl extends Component { } } -const mapStateToProps = state => ({ - authFailure: state.okapi.authFailure, - ssoEnabled: state.okapi.ssoEnabled, -}); -const mapDispatchToProps = dispatch => ({ - clearAuthErrors: () => dispatch(setAuthError([])), -}); - -export default reduxConnect(mapStateToProps, mapDispatchToProps)(withRouter(LoginCtrl)); +export default Login; diff --git a/src/components/Login/index.js b/src/components/Login/index.js index 2a741cdbd..44e798405 100644 --- a/src/components/Login/index.js +++ b/src/components/Login/index.js @@ -1 +1 @@ -export { default } from './Login'; +export { default } from './LoginCtrl'; diff --git a/src/components/MainNav/MainNav.js b/src/components/MainNav/MainNav.js index 1ced15b62..2a26383d4 100644 --- a/src/components/MainNav/MainNav.js +++ b/src/components/MainNav/MainNav.js @@ -116,6 +116,14 @@ class MainNav extends Component { }); } + // Return the user to the login screen, but after logging in they will return to their previous activity. + returnToLogin() { + const { okapi } = this.store.getState(); + + return getLocale(okapi.url, this.store, okapi.tenant) + .then(sessionLogout(okapi.url, this.store)); + } + // return the user to the login screen, but after logging in they will be brought to the default screen. logout() { const { okapi } = this.store.getState(); diff --git a/src/components/OIDCLanding.js b/src/components/OIDCLanding.js index 82ae09c1c..18960f8f9 100644 --- a/src/components/OIDCLanding.js +++ b/src/components/OIDCLanding.js @@ -47,8 +47,7 @@ const OIDCLanding = () => { const otp = getOtp(); /** - * Exchange the otp for an access token, then use it to retrieve - * the user + * Exchange the otp for AT/RT cookies, then retrieve the user. * * See https://ebscoinddev.atlassian.net/wiki/spaces/TEUR/pages/12419306/mod-login-keycloak#mod-login-keycloak-APIs * for additional details. May not be necessary for SAML-specific pages @@ -58,12 +57,14 @@ const OIDCLanding = () => { useEffect(() => { if (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' }, + mode: 'cors', }) .then((resp) => { if (resp.ok) { return resp.json().then((json) => { - return requestUserWithPermsDeb(okapi.url, store, okapi.tenant, json.okapiToken); + return requestUserWithPermsDeb(okapi.url, store, okapi.tenant); }); } else { return resp.json().then((error) => { diff --git a/src/loginServices.js b/src/loginServices.js index e5dd0988f..715beda6c 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -298,8 +298,8 @@ export function getUserLocale(okapiUrl, store, tenant, userId) { */ export function getPlugins(okapiUrl, store, tenant) { return fetch(`${okapiUrl}/configurations/entries?query=(module==PLUGINS)`, { - headers: getHeaders(tenant, store.getState().okapi.token), credentials: 'include', + headers: getHeaders(tenant, store.getState().okapi.token), mode: 'cors', }) .then((response) => { @@ -391,7 +391,7 @@ function loadResources(store, tenant, userId) { /** * spreadUserWithPerms - * return an object { user, perms } based on response from bl-users/self. + * return an object { user, perms } based on response from .../_self. * * @param {object} userWithPerms * @@ -515,9 +515,9 @@ export async function logout(okapiUrl, store) { * Dispatch the session object, then return a Promise that fetches * and dispatches tenant resources. * - * @param {object} store - * @param {string} tenant - * @param {string} token + * @param {object} store redux store + * @param {string} tenant tenant name + * @param {string} token access token [deprecated; prefer folioAccessToken cookie] * @param {*} data * * @returns {Promise} @@ -536,7 +536,7 @@ export function createOkapiSession(store, tenant, token, data) { store.dispatch(setCurrentPerms(perms)); - // if we can't parse tokenExpiration data, e.g. because data comes from `/bl-users/_self` + // if we can't parse tokenExpiration data, e.g. because data comes from `.../_self` // which doesn't provide it, then set an invalid AT value and a near-future (+10 minutes) RT value. // the invalid AT will prompt an RTR cycle which will either give us new AT/RT values // (if the RT was valid) or throw an RTR_ERROR (if the RT was not valid). @@ -655,7 +655,7 @@ export function handleLoginError(dispatch, resp) { /** * processOkapiSession * create a new okapi session with the response from either a username/password - * authentication request or a bl-users/_self request. + * authentication request or a .../_self request. * response body is shaped like * { 'access_token': 'SOME_STRING', @@ -695,7 +695,7 @@ export function processOkapiSession(store, tenant, resp, ssoToken) { /** * validateUser - * return a promise that fetches from bl-users/_self. + * return a promise that fetches from .../_self. * if successful, dispatch the result to create a session * if not, clear the session and token. * @@ -708,7 +708,9 @@ export function processOkapiSession(store, tenant, resp, ssoToken) { */ export function validateUser(okapiUrl, store, tenant, session) { const { token, user, perms, tenant: sessionTenant = tenant } = session; - return fetch(`${okapiUrl}/bl-users/_self`, { + const usersPath = okapi.authnUrl ? 'users-keycloak' : 'bl-users'; + + return fetch(`${okapiUrl}/${usersPath}/_self`, { headers: getHeaders(sessionTenant, token), credentials: 'include', mode: 'cors', @@ -737,8 +739,7 @@ export function validateUser(okapiUrl, store, tenant, session) { token, tokenExpiration, })); - - return loadResources(okapiUrl, store, sessionTenant, user.id); + return loadResources(store, sessionTenant, user.id); }); } else { store.dispatch(clearCurrentUser()); diff --git a/src/loginServices.test.js b/src/loginServices.test.js index 5cde05064..fec4f9694 100644 --- a/src/loginServices.test.js +++ b/src/loginServices.test.js @@ -226,7 +226,7 @@ describe('processOkapiSession', () => { mockFetchSuccess(); - await processOkapiSession(store, 'tenant', resp, 'token'); + await processOkapiSession(store, 'tenant', resp); expect(store.dispatch).toHaveBeenCalledWith(setAuthError(null)); expect(store.dispatch).toHaveBeenCalledWith(setOkapiReady()); @@ -243,7 +243,7 @@ describe('processOkapiSession', () => { } }; - await processOkapiSession(store, 'tenant', resp, 'token'); + await processOkapiSession(store, 'tenant', resp); expect(store.dispatch).toHaveBeenCalledWith(setOkapiReady()); expect(store.dispatch).toHaveBeenCalledWith(setAuthError([defaultErrors.DEFAULT_LOGIN_CLIENT_ERROR])); diff --git a/translations/stripes-core/en.json b/translations/stripes-core/en.json index b86b68a25..b08b58fc5 100644 --- a/translations/stripes-core/en.json +++ b/translations/stripes-core/en.json @@ -13,6 +13,7 @@ "title.noPermission": "No permission", "title.cookieEnabled": "Cookies are required to login. Please enable cookies and try again.", "title.logout": "Log out", + "title.cookieEnabled": "Cookies are required to login. Please enable cookies and try again.", "front.welcome": "Welcome, the Future Of Libraries Is OPEN!", "front.home": "Home", "front.about": "Software versions", diff --git a/translations/stripes-core/en_GB.json b/translations/stripes-core/en_GB.json index 63d766417..7641546bc 100644 --- a/translations/stripes-core/en_GB.json +++ b/translations/stripes-core/en_GB.json @@ -64,6 +64,7 @@ "title.forgotUsername": "Forgot username?", "title.checkEmail": "Check your email", "title.changePassword": "Change password", + "title.cookieEnabled": "Cookies are required to login. Please enable cookies and try again.", "button.hidePassword": "Hide password", "button.showPassword": "Show password", "button.forgotPassword": "Forgot password?", diff --git a/translations/stripes-core/en_SE.json b/translations/stripes-core/en_SE.json index b034a5bc5..9be4c881a 100644 --- a/translations/stripes-core/en_SE.json +++ b/translations/stripes-core/en_SE.json @@ -64,6 +64,7 @@ "title.forgotUsername": "Forgot username?", "title.checkEmail": "Check your email", "title.changePassword": "Change password", + "title.cookieEnabled": "Cookies are required to login. Please enable cookies and try again.", "button.hidePassword": "Hide password", "button.showPassword": "Show password", "button.forgotPassword": "Forgot password?", diff --git a/translations/stripes-core/en_US.json b/translations/stripes-core/en_US.json index 1eeb20adc..663986727 100644 --- a/translations/stripes-core/en_US.json +++ b/translations/stripes-core/en_US.json @@ -66,6 +66,7 @@ "title.forgotUsername": "Forgot username?", "title.checkEmail": "Check your email", "title.changePassword": "Change password", + "title.cookieEnabled": "Cookies are required to login. Please enable cookies and try again.", "button.hidePassword": "Hide password", "button.showPassword": "Show password", "button.forgotPassword": "Forgot password?", diff --git a/yarn.lock b/yarn.lock index c04af2d8f..5ed10041d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9558,6 +9558,22 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== +postcss-calc@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-9.0.1.tgz#a744fd592438a93d6de0f1434c572670361eb6c6" + integrity sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ== + dependencies: + postcss-selector-parser "^6.0.11" + postcss-value-parser "^4.2.0" + +postcss-color-function@folio-org/postcss-color-function: + version "4.1.0" + resolved "https://codeload.github.com/folio-org/postcss-color-function/tar.gz/c128aad740ae740fb571c4b6493f467dd51efe85" + dependencies: + css-color-function "~1.3.3" + postcss-message-helpers "^2.0.0" + postcss-value-parser "^4.1.0" + postcss-custom-media@^9.0.1: version "9.1.5" resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-9.1.5.tgz#20c5822dd15155d768f8dd84e07a6ffd5d01b054"