Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions src/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,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)
Expand Down Expand Up @@ -864,12 +865,40 @@ 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)
Expand All @@ -885,6 +914,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`
}
Expand All @@ -895,6 +925,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)
Expand Down
11 changes: 8 additions & 3 deletions src/components/users/OIDLoginCallback.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -16,6 +16,7 @@ class OIDLoginCallback extends React.Component {
super(props)
this.state = {
next: null,
returnTo: null,
}
}
componentDidMount() {
Expand All @@ -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

Expand Down Expand Up @@ -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 = '/'
Expand Down
Loading