diff --git a/src/common/utils.js b/src/common/utils.js
index 0537329bf..3d971328a 100644
--- a/src/common/utils.js
+++ b/src/common/utils.js
@@ -836,6 +836,7 @@ export const isDeprecatedBrowser = () => isIE() || isOpera();
const PKCE_CODE_VERIFIER_KEY = 'pkce_code_verifier'
const OAUTH_STATE_KEY = 'oauth_state'
+const OAUTH_RETURN_TO_KEY = 'oauth_return_to'
const base64UrlEncode = buffer => {
const bytes = new Uint8Array(buffer)
@@ -892,15 +893,42 @@ export const consumeAndValidateOAuthState = returnedState => {
return !returnedState || returnedState === storedState
}
+// A route's path as the router sees it: decoded once, as history does, so /%73ignup is /signup. A malformed
+// encoding makes the router throw, so it has no path.
+const routePath = route => {
+ try {
+ return decodeURI(route.split(/[?#]/)[0])
+ } catch {
+ return null
+ }
+}
+
+// Keycloak only redeems a code when the token request repeats the sign-in's redirect_uri exactly, and the
+// callback can't rebuild a page's query string (e.g. ?referrer= on links from openconceptlab.org). So sign-in
+// always goes through LOGIN_REDIRECT_URL, and the page to come back to (its hash route) waits here, in this tab.
+const prepareOAuthReturnTo = returnTo => {
+ const route = returnTo?.includes('#') ? returnTo.slice(returnTo.indexOf('#') + 1) : null
+ const path = route && routePath(route)
+ // The router matches paths case-insensitively, so /SIGNUP would start a sign-up too.
+ if(path?.startsWith('/') && !/^\/(oidc\/login|signin|signup)(\/|$)/i.test(path))
+ sessionStorage.setItem(OAUTH_RETURN_TO_KEY, route)
+ else
+ sessionStorage.removeItem(OAUTH_RETURN_TO_KEY)
+}
+
+export const consumeOAuthReturnTo = () => {
+ const route = sessionStorage.getItem(OAUTH_RETURN_TO_KEY)
+ sessionStorage.removeItem(OAUTH_RETURN_TO_KEY)
+ return route
+}
+
export const getLoginURL = async returnTo => {
const oidClientID = window.OIDC_RP_CLIENT_ID || process.env.OIDC_RP_CLIENT_ID
let redirectURL = window.LOGIN_REDIRECT_URL || process.env.LOGIN_REDIRECT_URL
redirectURL = redirectURL.replace(/([^:]\/)\/+/g, "$1");
- if(returnTo && returnTo.includes('/#/') && returnTo.split('/#/')[1])
- redirectURL = returnTo.replace('/#/', '/')
-
+ prepareOAuthReturnTo(returnTo)
const codeChallenge = await preparePKCECodeChallenge()
const state = prepareOAuthState()
const nonce = generateSecureRandomString(32)
@@ -916,6 +944,7 @@ export const getResetPasswordURL = async returnTo => {
redirectURL = redirectURL.replace(/([^:]\/)\/+/g, "$1");
+ prepareOAuthReturnTo()
const codeChallenge = await preparePKCECodeChallenge()
return `${getAPIURL()}/users/password/reset/?client_id=${oidClientID}&redirect_uri=${redirectURL}&code_challenge=${codeChallenge}&code_challenge_method=S256`
@@ -927,6 +956,7 @@ export const getRegisterURL = async returnTo => {
redirectURL = redirectURL.replace(/([^:]\/)\/+/g, "$1");
+ prepareOAuthReturnTo()
const codeChallenge = await preparePKCECodeChallenge()
const state = prepareOAuthState(SIGNUP_STATE_PREFIX)
const nonce = generateSecureRandomString(32)
diff --git a/src/components/users/OIDLoginCallback.jsx b/src/components/users/OIDLoginCallback.jsx
index 9f58ed7f3..36b98cf56 100644
--- a/src/components/users/OIDLoginCallback.jsx
+++ b/src/components/users/OIDLoginCallback.jsx
@@ -3,7 +3,7 @@ import React from 'react';
import { withTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import {
- refreshCurrentUserCache, consumeStoredPKCECodeVerifier, consumeAndValidateOAuthState,
+ refreshCurrentUserCache, consumeStoredPKCECodeVerifier, consumeAndValidateOAuthState, consumeOAuthReturnTo,
isSignupOAuthState, isLoggedIn, getLoginURL
} from '../../common/utils';
import APIService from '../../services/APIService'
@@ -16,6 +16,7 @@ class OIDLoginCallback extends React.Component {
super(props)
this.state = {
next: null,
+ returnTo: null,
}
}
componentDidMount() {
@@ -32,12 +33,14 @@ class OIDLoginCallback extends React.Component {
const { setAlert } = this.context
const isStateValid = consumeAndValidateOAuthState(state)
const codeVerifier = consumeStoredPKCECodeVerifier()
+ const returnTo = consumeOAuthReturnTo()
if(!isStateValid || !codeVerifier) {
this.onSignInStartedElsewhere(state, next)
return
}
setAlert({message: this.props.t('auth.signing_in'), severity: 'info'})
- this.setState({next: next && next !== '/' ? next : null }, () => {
+ // next still decides the redirect_uri sent for sign-ins that started before redirect_uri was fixed.
+ this.setState({next: next && next !== '/' ? next : null, returnTo: returnTo }, () => {
const redirectURL = this.state.next ? window.location.origin + this.state.next : (window.LOGIN_REDIRECT_URL || process.env.LOGIN_REDIRECT_URL)
const clientId = window.OIDC_RP_CLIENT_ID || process.env.OIDC_RP_CLIENT_ID
@@ -86,7 +89,9 @@ class OIDLoginCallback extends React.Component {
cacheUserData() {
refreshCurrentUserCache(() => {
- if(this.state.next)
+ if(this.state.returnTo)
+ window.location.hash = '#' + this.state.returnTo
+ else if(this.state.next)
window.location.hash = '#' + this.state.next
else {
let returnToURL = '/'
diff --git a/src/components/users/UserIcon.jsx b/src/components/users/UserIcon.jsx
index 65b5d3e97..9cb9b6787 100644
--- a/src/components/users/UserIcon.jsx
+++ b/src/components/users/UserIcon.jsx
@@ -1,21 +1,24 @@
import React from 'react';
+import isPlainObject from 'lodash/isPlainObject'
import PersonIcon from '@mui/icons-material/Face2';
import StrangerIcon from '@mui/icons-material/Person';
import { isLoggedIn } from '../../common/utils';
import UserTooltip from './UserTooltip'
const UserIcon = ({ user, color, logoClassName, sx, authenticated, noTooltip }) => {
- const iconStyle = {...(sx || {})}
+ // sx may be an array (LeftMenu passes one for followed items), and spreading that into an
style
+ // blanks the app. Only a plain object doubles as the image's style; the MUI icons take sx as is.
+ const imgStyle = isPlainObject(sx) ? sx : undefined
return noTooltip ? (
user?.logo_url ?
:
(authenticated || isLoggedIn()) ?
- :
-
+ :
+
) : (
{
@@ -23,11 +26,11 @@ const UserIcon = ({ user, color, logoClassName, sx, authenticated, noTooltip })
:
(authenticated || isLoggedIn()) ?
- :
-
+ :
+
}
)