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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 0 additions & 2 deletions public/locales/en-US/login.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand All @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions public/locales/es/login.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand All @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions public/locales/fr/login.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand All @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions public/locales/ne-NP/login.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"helpText": "प्रश्नहरू छन्? आफ्नो साइट प्रशासकलाई सम्पर्क गर्नुहोस्।"
},
"errorAlert": {
"accessDeniedNotAuthorized": "पहुँच अस्वीकार गरियो: तपाईंको खाता यस संस्थाको लागि अधिकृत छैन। कृपया आफ्नो प्रशासकलाई सम्पर्क गर्नुहोस्।",
"accessDeniedNoPermission": "पहुँच अस्वीकार गरियो: तपाईंसँग यो अनुप्रयोग पहुँच गर्ने अनुमति छैन।",
"authenticationFailed": "प्रमाणीकरण असफल भयो: कृपया आफ्नो प्रमाणहरू जाँच गर्नुहोस् र पुनः प्रयास गर्नुहोस्।",
"authenticationError": "प्रमाणीकरण त्रुटि: {{errorDescription}}",
Expand All @@ -28,7 +27,6 @@
"eulaLinkLabel": "अर्थरेन्जर EULA (नयाँ ट्याबमा खुल्छ)",
"loginButton": "लग इन",
"loginButtonEmail": "इमेलबाट साइन इन",
"loginButtonIdp": "साइन इन",
"loginButtonLoadingLabel": "लोड हुँदैछ",
"loginButtonManagedUser": "व्यवस्थापित प्रयोगकर्ताको रूपमा साइन इन",
"passwordLabel": "पासवर्ड",
Expand Down
2 changes: 0 additions & 2 deletions public/locales/pt/login.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand All @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions public/locales/sw/login.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand All @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/Auth0TokenManager/accountLinkingGate.integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
Expand Down Expand Up @@ -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),
);
Expand Down
6 changes: 2 additions & 4 deletions src/Auth0TokenManager/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
};
Expand Down
8 changes: 4 additions & 4 deletions src/Auth0TokenManager/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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' } },
Expand All @@ -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);
});
});
});
32 changes: 11 additions & 21 deletions src/Login/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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' };
Expand Down Expand Up @@ -215,11 +206,10 @@ const LoginPage = () => {

<h1 className={styles.srOnly}>{t('title')}</h1>

{/* 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 && (
<section className={styles.infoBox} aria-labelledby="auth0-info-title">
<h2 className={styles.infoBoxTitle} id="auth0-info-title">
{t('auth0Info.title')}
Expand Down Expand Up @@ -253,7 +243,7 @@ const LoginPage = () => {
>
{isAuth0Loading
? <MoonLoader aria-hidden color="white" size={SUBMIT_LOADER_SIZE} />
: t(idpOrgId ? 'loginButtonIdp' : 'loginButtonEmail')}
: t('loginButtonEmail')}
</button>

{canSignInAsManagedUser && (
Expand Down
Loading
Loading