diff --git a/AGENTS.md b/AGENTS.md index bec2dcfec..675b662bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -300,7 +300,7 @@ Positions are stored and sent as **WGS84** longitude and latitude, but users rea **Signing in.** The site's status, read before anything renders, sets one way to sign in: - **Username and password** (`require_idp` off): the DAS OAuth password grant. -- **Auth0** (`require_idp` on): through the organization's identity provider, or EarthRanger Identity where it has none. These sites are mid-migration, so an account not linked yet goes to the server's account linker. A site may also offer a **managed user** button, for accounts in its own Auth0 connection. +- **Auth0** (`require_idp` on): through EarthRanger Identity. These sites are mid-migration, so an account not linked yet goes to the server's account linker. A site may also offer a **managed user** button, for accounts in its own Auth0 connection. Either way the client gets an access token, Auth0's used as is, kept in a cookie and in Redux and sent as a `Bearer` header. diff --git a/public/locales/en-US/login.json b/public/locales/en-US/login.json index fa24a1c67..e76a47598 100644 --- a/public/locales/en-US/login.json +++ b/public/locales/en-US/login.json @@ -9,7 +9,6 @@ "helpText": "Questions? Reach out to your site admin." }, "errorAlert": { - "accessDeniedNotAuthorized": "Access denied: Your account is not authorized for this organization. Please contact your administrator.", "accessDeniedNoPermission": "Access denied: You do not have permission to access this application.", "authenticationFailed": "Authentication failed: Please check your credentials and try again.", "authenticationError": "Authentication error: {{errorDescription}}", @@ -28,7 +27,6 @@ "eulaLinkLabel": "EarthRanger EULA (opens in a new tab)", "loginButton": "Log in", "loginButtonEmail": "Sign in with email", - "loginButtonIdp": "Sign in", "loginButtonLoadingLabel": "Loading", "loginButtonManagedUser": "Sign in as a managed user", "passwordLabel": "Password", diff --git a/public/locales/es/login.json b/public/locales/es/login.json index b7889253e..2faf32920 100644 --- a/public/locales/es/login.json +++ b/public/locales/es/login.json @@ -9,7 +9,6 @@ "helpText": "¿Tiene preguntas? Comuníquese con el administrador de su sitio." }, "errorAlert": { - "accessDeniedNotAuthorized": "Acceso denegado: Su cuenta no está autorizada para esta organización. Por favor contacte a su administrador.", "accessDeniedNoPermission": "Acceso denegado: No tiene permiso para acceder a esta aplicación.", "authenticationFailed": "Autenticación fallida: Por favor verifique sus credenciales e intente de nuevo.", "authenticationError": "Error de autenticación: {{errorDescription}}", @@ -28,7 +27,6 @@ "eulaLinkLabel": "EULA de EarthRanger (se abre en una nueva pestaña)", "loginButton": "Iniciar sesión", "loginButtonEmail": "Iniciar sesión con correo electrónico", - "loginButtonIdp": "Iniciar sesión", "loginButtonLoadingLabel": "Cargando", "loginButtonManagedUser": "Iniciar sesión como usuario gestionado", "passwordLabel": "Contraseña", diff --git a/public/locales/fr/login.json b/public/locales/fr/login.json index 2f421f40b..5772ccb8a 100644 --- a/public/locales/fr/login.json +++ b/public/locales/fr/login.json @@ -9,7 +9,6 @@ "helpText": "Des questions ? Contactez l'administrateur de votre site." }, "errorAlert": { - "accessDeniedNotAuthorized": "Accès refusé : Votre compte n'est pas autorisé pour cette organisation. Veuillez contacter votre administrateur.", "accessDeniedNoPermission": "Accès refusé : Vous n'avez pas la permission d'accéder à cette application.", "authenticationFailed": "Authentification échouée : Veuillez vérifier vos identifiants et réessayer.", "authenticationError": "Erreur d'authentification : {{errorDescription}}", @@ -28,7 +27,6 @@ "eulaLinkLabel": "EULA EarthRanger (s'ouvre dans un nouvel onglet)", "loginButton": "Se Connecter", "loginButtonEmail": "Se connecter avec un e-mail", - "loginButtonIdp": "Se Connecter", "loginButtonLoadingLabel": "Chargement", "loginButtonManagedUser": "Se connecter en tant qu'utilisateur géré", "passwordLabel": "Mot de passe", diff --git a/public/locales/ne-NP/login.json b/public/locales/ne-NP/login.json index cb7773542..b27afd2d4 100644 --- a/public/locales/ne-NP/login.json +++ b/public/locales/ne-NP/login.json @@ -9,7 +9,6 @@ "helpText": "प्रश्नहरू छन्? आफ्नो साइट प्रशासकलाई सम्पर्क गर्नुहोस्।" }, "errorAlert": { - "accessDeniedNotAuthorized": "पहुँच अस्वीकार गरियो: तपाईंको खाता यस संस्थाको लागि अधिकृत छैन। कृपया आफ्नो प्रशासकलाई सम्पर्क गर्नुहोस्।", "accessDeniedNoPermission": "पहुँच अस्वीकार गरियो: तपाईंसँग यो अनुप्रयोग पहुँच गर्ने अनुमति छैन।", "authenticationFailed": "प्रमाणीकरण असफल भयो: कृपया आफ्नो प्रमाणहरू जाँच गर्नुहोस् र पुनः प्रयास गर्नुहोस्।", "authenticationError": "प्रमाणीकरण त्रुटि: {{errorDescription}}", @@ -28,7 +27,6 @@ "eulaLinkLabel": "अर्थरेन्जर EULA (नयाँ ट्याबमा खुल्छ)", "loginButton": "लग इन", "loginButtonEmail": "इमेलबाट साइन इन", - "loginButtonIdp": "साइन इन", "loginButtonLoadingLabel": "लोड हुँदैछ", "loginButtonManagedUser": "व्यवस्थापित प्रयोगकर्ताको रूपमा साइन इन", "passwordLabel": "पासवर्ड", diff --git a/public/locales/pt/login.json b/public/locales/pt/login.json index 96ba87603..591cd6c0b 100644 --- a/public/locales/pt/login.json +++ b/public/locales/pt/login.json @@ -9,7 +9,6 @@ "helpText": "Dúvidas? Entre em contato com o administrador do seu site." }, "errorAlert": { - "accessDeniedNotAuthorized": "Acesso negado: Sua conta não está autorizada para esta organização. Entre em contato com o administrador.", "accessDeniedNoPermission": "Acesso negado: Você não tem permissão para acessar esta aplicação.", "authenticationFailed": "Falha na autenticação: Verifique suas credenciais e tente novamente.", "authenticationError": "Erro de autenticação: {{errorDescription}}", @@ -28,7 +27,6 @@ "eulaLinkLabel": "EULA EarthRanger (abre em uma nova guia)", "loginButton": "Conecte-se", "loginButtonEmail": "Conecte-se com e-mail", - "loginButtonIdp": "Conecte-se", "loginButtonLoadingLabel": "Carregando", "loginButtonManagedUser": "Conecte-se como usuário gerenciado", "passwordLabel": "Senha", diff --git a/public/locales/sw/login.json b/public/locales/sw/login.json index 1ed590c60..32886fe74 100644 --- a/public/locales/sw/login.json +++ b/public/locales/sw/login.json @@ -9,7 +9,6 @@ "helpText": "Una maswali? Wasiliana na msimamizi wa tovuti yako." }, "errorAlert": { - "accessDeniedNotAuthorized": "Ufikiaji umekataliwa: Akaunti yako haijaruhusiwa kwa shirika hili. Tafadhali wasiliana na msimamizi wako.", "accessDeniedNoPermission": "Ufikiaji umekataliwa: Huna ruhusa ya kufikia programu hii.", "authenticationFailed": "Uthibitishaji umeshindwa: Tafadhali hakikisha maelezo yako na jaribu tena.", "authenticationError": "Kosa la uthibitishaji: {{errorDescription}}", @@ -28,7 +27,6 @@ "eulaLinkLabel": "EarthRanger EULA (hufungua kwenye kichupo kipya)", "loginButton": "Ingia", "loginButtonEmail": "Ingia kwa barua pepe", - "loginButtonIdp": "Ingia", "loginButtonLoadingLabel": "Inapakia", "loginButtonManagedUser": "Ingia kama mtumiaji anayesimamiwa", "passwordLabel": "Nenosiri", diff --git a/src/Auth0TokenManager/accountLinkingGate.integration.test.js b/src/Auth0TokenManager/accountLinkingGate.integration.test.js index e3da2226f..29ca0572f 100644 --- a/src/Auth0TokenManager/accountLinkingGate.integration.test.js +++ b/src/Auth0TokenManager/accountLinkingGate.integration.test.js @@ -74,7 +74,7 @@ describe('Auth0 error redirect reaches the login page', () => { }), { data: { token: { access_token: null } }, - view: { systemConfig: { require_idp: true, idp_org_id: null } }, + view: { systemConfig: { require_idp: true } }, }, applyMiddleware(thunk, promiseMiddleware), ); @@ -158,7 +158,7 @@ describe('post-callback account-linking gate', () => { }), { data: { token: { access_token: null } }, - view: { systemConfig: { require_idp: true, idp_org_id: null } }, // common-DB site + view: { systemConfig: { require_idp: true } }, }, applyMiddleware(thunk, promiseMiddleware), ); diff --git a/src/Auth0TokenManager/index.js b/src/Auth0TokenManager/index.js index d6d26f8e5..4164f323a 100644 --- a/src/Auth0TokenManager/index.js +++ b/src/Auth0TokenManager/index.js @@ -26,7 +26,6 @@ const Auth0TokenManager = () => { const navigate = useNavigate(); const existingToken = useSelector((state) => state.data.token?.access_token); - const idpOrgId = useSelector((state) => state.view.systemConfig?.idp_org_id); const requireIdp = useSelector((state) => !!state.view.systemConfig?.require_idp); const { isAuthenticated, getAccessTokenSilently, logout } = useAuth0(); @@ -64,8 +63,7 @@ const Auth0TokenManager = () => { return; } - // Account-linking gate — common-DB path only; org-scoped (rcuksa) sites skip it. - if (requireIdp && !idpOrgId?.trim()) { + if (requireIdp) { const { result, linkUrl } = await checkAccountLinked(safe); if (result === GATE_RESULT.UNLINKED) { @@ -134,7 +132,7 @@ const Auth0TokenManager = () => { } }; ensureIdpToken(); - }, [dispatch, existingToken, getAccessTokenSilently, idpOrgId, isAuthenticated, logout, requireIdp, navigate, location.search]); + }, [dispatch, existingToken, getAccessTokenSilently, isAuthenticated, logout, requireIdp, navigate, location.search]); return null; }; diff --git a/src/Auth0TokenManager/index.test.js b/src/Auth0TokenManager/index.test.js index 137e8d183..32831c976 100644 --- a/src/Auth0TokenManager/index.test.js +++ b/src/Auth0TokenManager/index.test.js @@ -55,7 +55,7 @@ describe('Auth0TokenManager', () => { useSelector.mockImplementation((selector) => { const state = { data: { token: { access_token: null } }, - view: { systemConfig: { require_idp: true, idp_org_id: null } } + view: { systemConfig: { require_idp: true } } }; return selector(state); }); @@ -350,7 +350,7 @@ describe('Auth0TokenManager', () => { }); }); - test('org-scoped (idp_org_id set): skips the gate and authenticates', async () => { + test('runs the gate on a site whose status response still reports an organization ID', async () => { useSelector.mockImplementation((selector) => selector({ data: { token: { access_token: null } }, view: { systemConfig: { require_idp: true, idp_org_id: 'org_abc' } }, @@ -359,9 +359,9 @@ describe('Auth0TokenManager', () => { renderAfterCallback(); await waitFor(() => { - expect(applyAccessToken).toHaveBeenCalledWith(VALID_TOKEN); + expect(checkAccountLinked).toHaveBeenCalledWith(VALID_TOKEN); }); - expect(checkAccountLinked).not.toHaveBeenCalled(); + expect(applyAccessToken).toHaveBeenCalledWith(VALID_TOKEN); }); }); }); diff --git a/src/Login/index.js b/src/Login/index.js index cc833d7de..b0c1b048b 100644 --- a/src/Login/index.js +++ b/src/Login/index.js @@ -55,44 +55,37 @@ const LoginPage = () => { const [formErrors, setFormErrors] = useState({ username: null, password: null }); const [isLoading, setIsLoading] = useState(false); - const idpOrgId = systemConfig?.idp_org_id?.trim() || null; const isEULAEnabled = !!systemConfig?.[SYSTEM_CONFIG_FLAGS.EULA]; const requireIdp = !!systemConfig?.require_idp; const siteSlug = systemConfig?.site_slug?.trim() || null; // No slug means no connection on the redirect, which would sign the user into the - // common database. Org-scoped sites skip the gate that catches an unmapped one. - const canSignInAsManagedUser = !!systemConfig?.support_managed_users - && !!siteSlug - && !idpOrgId; + // common database. + const canSignInAsManagedUser = !!systemConfig?.support_managed_users && !!siteSlug; const onAuth0Login = useCallback(async () => { try { await auth0LoginWithRedirect({ - authorizationParams: buildAuth0AuthorizationParams(appConfig.auth0.audience, idpOrgId), + authorizationParams: { audience: appConfig.auth0.audience }, }); } catch (_error) { setAlert({ key: 'errorAlert.signInFailed' }); } - }, [auth0LoginWithRedirect, idpOrgId]); + }, [auth0LoginWithRedirect]); const onManagedUserLogin = useCallback(async () => { markManagedUserLoginAttempt(); try { await auth0LoginWithRedirect({ - authorizationParams: buildAuth0AuthorizationParams( - appConfig.auth0.audience, - idpOrgId, - siteSlug, - ), + authorizationParams: buildAuth0AuthorizationParams(appConfig.auth0.audience, siteSlug), }); } catch (_error) { // No redirect happened, so there is no attempt left to attribute. takeManagedUserLoginAttempt(); setAlert({ key: 'errorAlert.signInFailed' }); } - }, [auth0LoginWithRedirect, idpOrgId, siteSlug]); + }, [auth0LoginWithRedirect, siteSlug]); const onFormSubmit = useCallback(async (event) => { event.preventDefault(); @@ -181,9 +174,7 @@ const LoginPage = () => { return { key: 'errorAlert.managedUserSignInFailed' }; } if (auth0Error === 'access_denied') { - return auth0ErrorDescription?.includes('not part of the') - ? { key: 'errorAlert.accessDeniedNotAuthorized' } - : { key: 'errorAlert.accessDeniedNoPermission' }; + return { key: 'errorAlert.accessDeniedNoPermission' }; } if (auth0Error === 'unauthorized') { return { key: 'errorAlert.authenticationFailed' }; @@ -215,11 +206,10 @@ const LoginPage = () => {

{t('title')}

- {/* Auth0 migration guidance: shown only on common-DB sites (require_idp with - no idp_org_id). "Sign in with email" below auto-drives EarthRanger + {/* Auth0 migration guidance: "Sign in with email" below drives EarthRanger Identity; users who have not converted their account yet are linked to - the server account linker. Org-scoped sites show no box. */} - {requireIdp && !idpOrgId && ( + the server account linker. */} + {requireIdp && (

{t('auth0Info.title')} @@ -253,7 +243,7 @@ const LoginPage = () => { > {isAuth0Loading ? - : t(idpOrgId ? 'loginButtonIdp' : 'loginButtonEmail')} + : t('loginButtonEmail')} {canSignInAsManagedUser && ( diff --git a/src/Login/index.test.js b/src/Login/index.test.js index 3b1965e5d..7a05870a2 100644 --- a/src/Login/index.test.js +++ b/src/Login/index.test.js @@ -94,12 +94,12 @@ describe('Login', () => { test('shows the Auth0 sign-in button and hides local credentials when IDP login is required', () => { store = mockStore({ data: { eula: { eula_url: '' } }, - view: { systemConfig: { require_idp: true, idp_org_id: 'org_abc' } }, + view: { systemConfig: { require_idp: true } }, }); renderLogin(); - const signIn = screen.getByRole('button', { name: 'Sign in' }); + const signIn = screen.getByRole('button', { name: 'Sign in with email' }); expect(signIn).toBeVisible(); expect(signIn).toHaveAttribute('type', 'button'); expect(signIn).toBeEnabled(); @@ -108,71 +108,52 @@ describe('Login', () => { expect(screen.queryByLabelText('Username')).not.toBeInTheDocument(); }); - test('calls loginWithRedirect with audience and organization when the user clicks Sign in', async () => { + test('calls loginWithRedirect with the audience when the user clicks Sign in with email', async () => { store = mockStore({ data: { eula: { eula_url: '' } }, - view: { systemConfig: { require_idp: true, idp_org_id: 'org_abc' } }, + view: { systemConfig: { require_idp: true } }, }); loginWithRedirect.mockResolvedValue(undefined); renderLogin(); - await userEvent.click(screen.getByRole('button', { name: 'Sign in' })); + await userEvent.click(screen.getByRole('button', { name: 'Sign in with email' })); expect(loginWithRedirect).toHaveBeenCalledWith({ - authorizationParams: { - audience: appConfig.auth0.audience, - organization: 'org_abc', - }, + authorizationParams: { audience: appConfig.auth0.audience }, }); }); - test('disables the Auth0 sign-in button and shows a loading state while Auth0 reports loading', () => { + test('sends no organization param on a site whose status response still reports an organization ID', async () => { store = mockStore({ data: { eula: { eula_url: '' } }, view: { systemConfig: { require_idp: true, idp_org_id: 'org_abc' } }, }); - useAuth0.mockReturnValue({ loginWithRedirect, isLoading: true }); + loginWithRedirect.mockResolvedValue(undefined); renderLogin(); - const button = screen.getByRole('button', { name: 'Loading' }); - expect(button).toHaveAttribute('type', 'button'); - expect(button).toHaveAttribute('aria-busy', 'true'); - expect(button).toHaveAttribute('aria-label', 'Loading'); - expect(button).toBeDisabled(); - }); + await userEvent.click(screen.getByRole('button', { name: 'Sign in with email' })); - test('shows the common-DB "Sign in with email" button and no configuration error when IDP is required without an organization ID', () => { - store = mockStore({ - data: { eula: { eula_url: '' } }, - view: { systemConfig: { require_idp: true } }, + expect(loginWithRedirect).toHaveBeenCalledWith({ + authorizationParams: { audience: appConfig.auth0.audience }, }); - - renderLogin(); - - const signIn = screen.getByRole('button', { name: 'Sign in with email' }); - expect(signIn).toBeVisible(); - expect(signIn).toHaveAttribute('type', 'button'); - expect(signIn).toBeEnabled(); - expect(screen.queryByText('Identity provider organization is not configured.')).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Username')).not.toBeInTheDocument(); }); - test('calls loginWithRedirect with audience and no organization when the user clicks Sign in with email', async () => { + test('disables the Auth0 sign-in button and shows a loading state while Auth0 reports loading', () => { store = mockStore({ data: { eula: { eula_url: '' } }, view: { systemConfig: { require_idp: true } }, }); - loginWithRedirect.mockResolvedValue(undefined); + useAuth0.mockReturnValue({ loginWithRedirect, isLoading: true }); renderLogin(); - await userEvent.click(screen.getByRole('button', { name: 'Sign in with email' })); - - expect(loginWithRedirect).toHaveBeenCalledWith({ - authorizationParams: { audience: appConfig.auth0.audience }, - }); + const button = screen.getByRole('button', { name: 'Loading' }); + expect(button).toHaveAttribute('type', 'button'); + expect(button).toHaveAttribute('aria-busy', 'true'); + expect(button).toHaveAttribute('aria-label', 'Loading'); + expect(button).toBeDisabled(); }); describe('managed-user sign-in', () => { @@ -247,15 +228,23 @@ describe('Login', () => { expect(screen.queryByRole('button', { name: 'Sign in as a managed user' })).not.toBeInTheDocument(); }); - test('hides the managed-user path on an org-scoped site, where the unmapped-user guard does not run', () => { + test('sends no organization param on a site whose status response still reports an organization ID', async () => { store = mockStore({ data: { eula: { eula_url: '' } }, view: { systemConfig: { ...managedUserSystemConfig, idp_org_id: 'org_abc' } }, }); + loginWithRedirect.mockResolvedValue(undefined); renderLogin(); - expect(screen.queryByRole('button', { name: 'Sign in as a managed user' })).not.toBeInTheDocument(); + await userEvent.click(screen.getByRole('button', { name: 'Sign in as a managed user' })); + + expect(loginWithRedirect).toHaveBeenCalledWith({ + authorizationParams: { + audience: appConfig.auth0.audience, + connection: 'gdl-zoo', + }, + }); }); test('points at the managed-user option from the migration guidance when it is offered', () => { @@ -581,7 +570,7 @@ describe('Login', () => { }); describe('Auth0 sign-in info box', () => { - test('renders the info box on a common-DB Auth0 site (require_idp without an organization ID)', () => { + test('renders the info box on an Auth0 site', () => { store = mockStore({ data: { eula: { eula_url: '' } }, view: { systemConfig: { require_idp: true } }, @@ -607,33 +596,19 @@ describe('Login', () => { expect(screen.getByText('Questions? Reach out to your site admin.')).toBeVisible(); }); - test('treats a whitespace-only organization ID as common-DB and still renders the info box', () => { + test('renders the info box on a site whose status response still reports an organization ID', () => { store = mockStore({ data: { eula: { eula_url: '' } }, - view: { systemConfig: { require_idp: true, idp_org_id: ' ' } }, + view: { systemConfig: { require_idp: true, idp_org_id: 'org_abc' } }, }); renderLogin(); expect(screen.getByRole('heading', { level: 2 })).toBeVisible(); expect(screen.getByRole('link', { name: 'Convert your account' })).toBeVisible(); - // Whitespace-only org id is treated as no org -> common-DB "Sign in with email". expect(screen.getByRole('button', { name: 'Sign in with email' })).toBeVisible(); }); - test('does not render the info box on an org-scoped Auth0 site (require_idp with an organization ID)', () => { - store = mockStore({ - data: { eula: { eula_url: '' } }, - view: { systemConfig: { require_idp: true, idp_org_id: 'org_abc' } }, - }); - - renderLogin(); - - expect(screen.queryByRole('heading', { level: 2 })).not.toBeInTheDocument(); - expect(screen.queryByRole('link', { name: 'Convert your account' })).not.toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Sign in' })).toBeVisible(); - }); - test('does not render the info box on a site without Auth0 configured', () => { store = mockStore({ data: { eula: { eula_url: '' } }, @@ -652,13 +627,13 @@ describe('Login', () => { test('shows a sign-in failure alert when loginWithRedirect rejects', async () => { store = mockStore({ data: { eula: { eula_url: '' } }, - view: { systemConfig: { require_idp: true, idp_org_id: 'org_abc' } }, + view: { systemConfig: { require_idp: true } }, }); loginWithRedirect.mockRejectedValue(new Error('Auth0 failed')); renderLogin(); - await userEvent.click(screen.getByRole('button', { name: 'Sign in' })); + await userEvent.click(screen.getByRole('button', { name: 'Sign in with email' })); await waitFor(() => { const alert = screen.getByText('Sign-in failed. Please try again.'); @@ -974,20 +949,7 @@ describe('Login', () => { expect(fetchEula).toHaveBeenCalled(); }); - test('shows the organization-access alert when the callback URL includes access_denied with a membership-related description', () => { - renderLogin({ - initialEntries: ['/login?error=access_denied&error_description=user+is+not+part+of+the+org'], - }); - - const alert = screen.getByText( - 'Access denied: Your account is not authorized for this organization. Please contact your administrator.', - ); - expect(alert).toBeVisible(); - expect(alert).toHaveAttribute('role', 'alert'); - expect(alert).toHaveClass(loginStyles.alertMessage); - }); - - test('shows the generic access-denied alert when access_denied has no membership-related description', () => { + test('shows the access-denied alert when the callback URL includes an access_denied error', () => { renderLogin({ initialEntries: ['/login?error=access_denied&error_description=User+cancelled+login'], }); diff --git a/src/ducks/system-config/index.js b/src/ducks/system-config/index.js index f6f13865e..1a14413d0 100644 --- a/src/ducks/system-config/index.js +++ b/src/ducks/system-config/index.js @@ -30,7 +30,6 @@ export const setSystemConfigFromSystemStatus = (systemStatus) => (dispatch) => { [SYSTEM_CONFIG_FLAGS.SUBJECTS]: systemStatus[SYSTEM_CONFIG_FLAGS.SUBJECTS] ?? true, [SYSTEM_CONFIG_FLAGS.TABLEAU]: systemStatus[SYSTEM_CONFIG_FLAGS.TABLEAU] ?? true, [SYSTEM_CONFIG_FLAGS.GEO_SPAN]: systemStatus[SYSTEM_CONFIG_FLAGS.GEO_SPAN] ?? null, - idp_org_id: systemStatus.idp_org_id || null, previewFeatures: systemStatus.preview_features || {}, require_idp: !!systemStatus.require_idp, site_slug: systemStatus.site_slug || null, @@ -77,7 +76,6 @@ export const INITIAL_STATE = { [SYSTEM_CONFIG_FLAGS.SUBJECTS]: false, [SYSTEM_CONFIG_FLAGS.TABLEAU]: false, [SYSTEM_CONFIG_FLAGS.GEO_SPAN]: null, - idp_org_id: null, previewFeatures: {}, require_idp: null, site_slug: null, diff --git a/src/ducks/system-config/index.test.js b/src/ducks/system-config/index.test.js index e2e1314cb..be3e2013e 100644 --- a/src/ducks/system-config/index.test.js +++ b/src/ducks/system-config/index.test.js @@ -51,6 +51,7 @@ describe('Ducks - System config', () => { [SYSTEM_CONFIG_FLAGS.SUBJECTS]: true, [SYSTEM_CONFIG_FLAGS.TABLEAU]: true, geoPermissionsEnabled: true, + idp_org_id: 'org_abc', preview_features: { community_input_admin_enabled: true }, show_track_days: true, site_name: 'Site name', @@ -76,7 +77,6 @@ describe('Ducks - System config', () => { [SYSTEM_CONFIG_FLAGS.SPATIAL_FEATURES]: true, [SYSTEM_CONFIG_FLAGS.SUBJECTS]: true, [SYSTEM_CONFIG_FLAGS.TABLEAU]: true, - idp_org_id: null, previewFeatures: { community_input_admin_enabled: true, }, @@ -248,7 +248,6 @@ describe('Ducks - System config', () => { [SYSTEM_CONFIG_FLAGS.SPATIAL_FEATURES]: true, [SYSTEM_CONFIG_FLAGS.SUBJECTS]: true, [SYSTEM_CONFIG_FLAGS.TABLEAU]: true, - idp_org_id: null, previewFeatures: { community_input_admin_enabled: true }, require_idp: null, showTrackDays: true, diff --git a/src/hooks/useAuthRecovery.js b/src/hooks/useAuthRecovery.js index 79f151269..d92637b12 100644 --- a/src/hooks/useAuthRecovery.js +++ b/src/hooks/useAuthRecovery.js @@ -1,11 +1,9 @@ import { useEffect } from 'react'; import { useAuth0 } from '@auth0/auth0-react'; -import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import appConfig from '../config'; import { registerAuthRecovery } from '../utils/auth-recovery'; -import { buildAuth0AuthorizationParams } from '../utils/auth0'; import { setIntendedPostAuth0SuccessRoute } from '../utils/auth'; /** @@ -15,7 +13,6 @@ import { setIntendedPostAuth0SuccessRoute } from '../utils/auth'; const useAuthRecovery = () => { const { getAccessTokenSilently, loginWithRedirect } = useAuth0(); const { pathname, search } = useLocation(); - const idpOrgId = useSelector((state) => state.view.systemConfig?.idp_org_id); useEffect(() => { registerAuthRecovery({ @@ -26,7 +23,7 @@ const useAuthRecovery = () => { setIntendedPostAuth0SuccessRoute(`${pathname}${search}`); await loginWithRedirect({ authorizationParams: { - ...buildAuth0AuthorizationParams(appConfig.auth0.audience, idpOrgId), + audience: appConfig.auth0.audience, ...(acrValues ? { acr_values: acrValues } : {}), ...(maxAge ? { max_age: maxAge } : {}), }, @@ -34,7 +31,7 @@ const useAuthRecovery = () => { return new Promise(() => {}); }, }); - }, [getAccessTokenSilently, loginWithRedirect, idpOrgId, pathname, search]); + }, [getAccessTokenSilently, loginWithRedirect, pathname, search]); }; export default useAuthRecovery; diff --git a/src/hooks/useAuthRecovery.test.js b/src/hooks/useAuthRecovery.test.js index 0a21825cf..f4738b9eb 100644 --- a/src/hooks/useAuthRecovery.test.js +++ b/src/hooks/useAuthRecovery.test.js @@ -1,6 +1,5 @@ import { renderHook } from '@testing-library/react'; import { useAuth0 } from '@auth0/auth0-react'; -import { useSelector } from 'react-redux'; import { useLocation } from 'react-router'; import useAuthRecovery from './useAuthRecovery'; @@ -8,7 +7,6 @@ import { registerAuthRecovery } from '../utils/auth-recovery'; import { setIntendedPostAuth0SuccessRoute } from '../utils/auth'; jest.mock('@auth0/auth0-react'); -jest.mock('react-redux', () => ({ useSelector: jest.fn() })); jest.mock('react-router', () => ({ __esModule: true, useLocation: jest.fn() })); jest.mock('../utils/auth-recovery', () => ({ registerAuthRecovery: jest.fn() })); jest.mock('../utils/auth', () => ({ setIntendedPostAuth0SuccessRoute: jest.fn() })); @@ -24,7 +22,6 @@ describe('useAuthRecovery', () => { getAccessTokenSilently = jest.fn(); loginWithRedirect = jest.fn().mockResolvedValue(undefined); useAuth0.mockReturnValue({ getAccessTokenSilently, loginWithRedirect }); - useSelector.mockReturnValue(null); // idp_org_id (common-DB site) useLocation.mockReturnValue({ pathname: '/events/123', search: '?foo=bar' }); }); @@ -43,11 +40,11 @@ describe('useAuthRecovery', () => { expect(setIntendedPostAuth0SuccessRoute).toHaveBeenCalledWith('/events/123?foo=bar'); expect(loginWithRedirect).toHaveBeenCalledWith({ - authorizationParams: expect.objectContaining({ + authorizationParams: { audience: 'https://api.example', acr_values: 'urn:mfa', max_age: '3600', - }), + }, }); }); diff --git a/src/i18n.js b/src/i18n.js index 053ac3e8a..8c89c9b7d 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -7,7 +7,7 @@ import LocalStorageBackend from 'i18next-localstorage-backend'; import { SUPPORTED_LANGUAGES } from './constants'; -const I18N_FILES_VERSION = '1.76'; +const I18N_FILES_VERSION = '1.77'; const preloadNamespaces = [ 'components', diff --git a/src/utils/auth.test.js b/src/utils/auth.test.js index abfd5b6b8..db1933383 100644 --- a/src/utils/auth.test.js +++ b/src/utils/auth.test.js @@ -29,7 +29,7 @@ describe('auth utils', () => { }); test('returns true when require_idp is true (loaded)', () => { - const systemConfig = { require_idp: true, sitename: 'Test Site', idp_org_id: 'org_123' }; + const systemConfig = { require_idp: true, sitename: 'Test Site' }; expect(isSystemConfigLoaded(systemConfig)).toBe(true); }); }); diff --git a/src/utils/auth0.js b/src/utils/auth0.js index 16069c0a9..c0be8f7f6 100644 --- a/src/utils/auth0.js +++ b/src/utils/auth0.js @@ -3,16 +3,12 @@ export const hasAuth0CallbackParams = (searchParams) => { return urlParams.has('code') && (urlParams.has('state') || urlParams.has('error')); }; -// Both params are omitted when blank so Auth0 falls back to the tenant's Default -// Directory; a connection opts out of it. Carrying both at once is an artifact of -// the unfinished migration away from organizations, left unguarded on purpose — -// the combination cannot arise once that finishes. -export const buildAuth0AuthorizationParams = (audience, idpOrgId, connection) => { - const org = idpOrgId?.trim(); +// The connection is omitted when blank so Auth0 falls back to the tenant's Default +// Directory; naming one opts out of it. +export const buildAuth0AuthorizationParams = (audience, connection) => { const namedConnection = connection?.trim(); return { audience, - ...(org ? { organization: org } : {}), ...(namedConnection ? { connection: namedConnection } : {}), }; }; diff --git a/src/utils/auth0.test.js b/src/utils/auth0.test.js index 2bc7ea49e..66bd42d42 100644 --- a/src/utils/auth0.test.js +++ b/src/utils/auth0.test.js @@ -51,72 +51,32 @@ describe('auth0 utils', () => { describe('buildAuth0AuthorizationParams', () => { const AUDIENCE = 'https://example.org/api'; - test('forwards the organization when an idp org id is provided', () => { - expect(buildAuth0AuthorizationParams(AUDIENCE, 'org_abc')).toEqual({ - audience: AUDIENCE, - organization: 'org_abc', - }); - }); - - test('omits the organization entirely when the idp org id is null', () => { - const params = buildAuth0AuthorizationParams(AUDIENCE, null); - expect(params).toEqual({ audience: AUDIENCE }); - expect(params).not.toHaveProperty('organization'); - }); - - test('omits the organization entirely when the idp org id is an empty string', () => { - const params = buildAuth0AuthorizationParams(AUDIENCE, ''); - expect(params).toEqual({ audience: AUDIENCE }); - expect(params).not.toHaveProperty('organization'); - }); - - test('omits the organization entirely when the idp org id is only whitespace', () => { - const params = buildAuth0AuthorizationParams(AUDIENCE, ' '); - expect(params).toEqual({ audience: AUDIENCE }); - expect(params).not.toHaveProperty('organization'); - }); - - test('forwards a trimmed organization when the idp org id has surrounding whitespace', () => { - expect(buildAuth0AuthorizationParams(AUDIENCE, ' org_abc ')).toEqual({ - audience: AUDIENCE, - organization: 'org_abc', - }); - }); - test('forwards the connection when one is provided', () => { - expect(buildAuth0AuthorizationParams(AUDIENCE, null, 'gdl-zoo')).toEqual({ + expect(buildAuth0AuthorizationParams(AUDIENCE, 'gdl-zoo')).toEqual({ audience: AUDIENCE, connection: 'gdl-zoo', }); }); test('forwards a trimmed connection when it has surrounding whitespace', () => { - expect(buildAuth0AuthorizationParams(AUDIENCE, null, ' gdl-zoo ')).toEqual({ + expect(buildAuth0AuthorizationParams(AUDIENCE, ' gdl-zoo ')).toEqual({ audience: AUDIENCE, connection: 'gdl-zoo', }); }); test('omits the connection entirely when it is not provided', () => { - const params = buildAuth0AuthorizationParams(AUDIENCE, null); + const params = buildAuth0AuthorizationParams(AUDIENCE); expect(params).toEqual({ audience: AUDIENCE }); expect(params).not.toHaveProperty('connection'); }); test('omits the connection entirely when it is null, empty, or only whitespace', () => { [null, '', ' '].forEach((connection) => { - const params = buildAuth0AuthorizationParams(AUDIENCE, null, connection); + const params = buildAuth0AuthorizationParams(AUDIENCE, connection); expect(params).toEqual({ audience: AUDIENCE }); expect(params).not.toHaveProperty('connection'); }); }); - - test('forwards both the organization and the connection instead of special-casing their overlap', () => { - expect(buildAuth0AuthorizationParams(AUDIENCE, 'org_abc', 'gdl-zoo')).toEqual({ - audience: AUDIENCE, - organization: 'org_abc', - connection: 'gdl-zoo', - }); - }); }); });