Skip to content

Commit

Permalink
V14: The login page does not respect certain error codes (#16244)
Browse files Browse the repository at this point in the history
* handle 403 and unknown error codes from the server

* resolve 2fa errors in repository

error handling was never being activated because this specific endpoint did not return api errors as it works exactly like the "authorize" endpoint, which is being called directly

* chore: add obsolete message to unused `SetupViewPath`

* chore: remove unused events

* add missing labels

* fix: send only 'error' back if the response is not ok

* chore: remove duplicate error handling for 500 errors

* fix: add hack to allow to submit the form on enter click
  • Loading branch information
iOvergaard committed May 13, 2024
1 parent 2cc5f49 commit 22c0c25
Show file tree
Hide file tree
Showing 11 changed files with 77 additions and 63 deletions.
Expand Up @@ -8,5 +8,6 @@ public class TwoFactorLoginViewOptions
/// <summary>
/// Gets or sets the path of the view to show when setting up this 2fa provider
/// </summary>
[Obsolete("Register the view in the backoffice instead. This will be removed in version 15.")]
public string? SetupViewPath { get; set; }
}
Expand Up @@ -42,6 +42,12 @@ export default class UmbLoginPageElement extends UmbLitElement {

if (!this.#formElement) return;

// We need to listen for the enter key to submit the form, because the uui-button does not support the native input fields submit event
this.#formElement.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.#onSubmitClick();
}
});
this.#formElement.onsubmit = this.#handleSubmit;
}

Expand Down Expand Up @@ -91,7 +97,6 @@ export default class UmbLoginPageElement extends UmbLitElement {
}

if (response.error) {
this.dispatchEvent(new CustomEvent('umb-login-failed', {bubbles: true, composed: true}));
return;
}

Expand All @@ -100,8 +105,6 @@ export default class UmbLoginPageElement extends UmbLitElement {
if (returnPath) {
location.href = returnPath;
}

this.dispatchEvent(new CustomEvent('umb-login-success', {bubbles: true, composed: true, detail: response.data}));
};

get #greetingLocalizationKey() {
Expand Down
48 changes: 18 additions & 30 deletions src/Umbraco.Web.UI.Login/src/components/pages/mfa.page.element.ts
Expand Up @@ -56,6 +56,7 @@ export default class UmbMfaPageElement extends UmbLitElement {
if (codeInput) {
codeInput.error = false;
codeInput.errorMessage = '';
codeInput.setCustomValidity('');
}

if (!form.checkValidity()) return;
Expand Down Expand Up @@ -84,44 +85,30 @@ export default class UmbMfaPageElement extends UmbLitElement {

this.buttonState = 'waiting';

try {
const response = await this.#authContext.validateMfaCode(code, provider);
if (response.error) {
if (codeInput) {
codeInput.error = true;
codeInput.errorMessage = response.error;
} else {
this.error = response.error;
}
this.buttonState = 'failed';
return;
}

this.buttonState = 'success';

const returnPath = this.#authContext.returnPath;
if (returnPath) {
location.href = returnPath;
}

this.dispatchEvent(
new CustomEvent('umb-login-success', {bubbles: true, composed: true})
);
} catch (e) {
if (e instanceof Error) {
this.error = e.message ?? 'Unknown error';
const response = await this.#authContext.validateMfaCode(code, provider);
if (response.error) {
if (codeInput) {
codeInput.error = true;
codeInput.errorMessage = response.error;
} else {
this.error = 'Unknown error';
this.error = response.error;
}
this.buttonState = 'failed';
this.dispatchEvent(new CustomEvent('umb-login-failed', {bubbles: true, composed: true}));
return;
}

this.buttonState = 'success';

const returnPath = this.#authContext.returnPath;
if (returnPath) {
location.href = returnPath;
}
}

protected renderDefaultView() {
return html`
<uui-form>
<form id="LoginForm" @submit=${this.#handleSubmit}>
<form id="LoginForm" @submit=${this.#handleSubmit} novalidate>
<header id="header">
<h1>
<umb-localize key="auth_mfaTitle">One last step</umb-localize>
Expand All @@ -141,7 +128,7 @@ export default class UmbMfaPageElement extends UmbLitElement {
<uui-label id="providerLabel" for="provider" slot="label" required>
<umb-localize key="auth_mfaMultipleText">Please choose a 2-factor provider</umb-localize>
</uui-label>
<uui-select id="provider" name="provider" .options=${this.providers} aria-required="true" required></uui-select>
<uui-select label=${this.localize.term('auth_mfaMultipleText')} id="provider" name="provider" .options=${this.providers} aria-required="true" required></uui-select>
</uui-form-layout-item>
`
: nothing}
Expand All @@ -162,6 +149,7 @@ export default class UmbMfaPageElement extends UmbLitElement {
aria-required="true"
required
required-message=${this.localize.term('auth_mfaCodeInputHelp')}
label=${this.localize.term('auth_mfaCodeInput')}
style="width:100%;">
</uui-input>
</uui-form-layout-item>
Expand Down
68 changes: 38 additions & 30 deletions src/Umbraco.Web.UI.Login/src/contexts/auth.repository.ts
Expand Up @@ -36,55 +36,64 @@ export class UmbAuthRepository extends UmbRepositoryBase {

const response = await fetch(request);

// If the response code is 402, it means that the user has enabled 2-factor authentication
let twoFactorView = '';
let twoFactorProviders: Array<string> = [];
if (response.status === 402) {
const responseData = await response.json();
twoFactorView = responseData.twoFactorLoginView ?? '';
twoFactorProviders = responseData.enabledTwoFactorProviderNames ?? [];
if (!response.ok) {
// If the response code is 402, it means that the user has enabled 2-factor authentication
if (response.status === 402) {
const responseData = await response.json();
return {
status: response.status,
twoFactorView: responseData.twoFactorLoginView ?? '',
twoFactorProviders: responseData.enabledTwoFactorProviderNames ?? [],
};
}

return {
status: response.status,
error: await this.#getErrorText(response),
};
}

return {
status: response.status,
data: {
username: data.username,
},
error: await this.#getErrorText(response),
twoFactorView,
twoFactorProviders,
};
} catch (error) {
return {
status: 500,
error: error instanceof Error ? error.message : 'Unknown error',
error: error instanceof Error ? error.message : this.#localize.term('auth_receivedErrorFromServer'),
};
}
}

public async validateMfaCode(code: string, provider: string): Promise<MfaCodeResponse> {
const requestData = new Request('management/api/v1/security/back-office/verify-2fa', {
method: 'POST',
body: JSON.stringify({
code,
provider,
}),
headers: {
'Content-Type': 'application/json',
},
});
try {
const requestData = new Request('management/api/v1/security/back-office/verify-2fa', {
method: 'POST',
body: JSON.stringify({
code,
provider,
}),
headers: {
'Content-Type': 'application/json',
},
});

const request = fetch(requestData);
const response = await fetch(requestData);

const response = await tryExecute(request);
if (!response.ok) {
return {
error: response.status === 400 ? this.#localize.term('auth_mfaInvalidCode') : await this.#getErrorText(response),
};
}

if (response.error) {
return {};
} catch (error) {
return {
error: this.#getApiErrorDetailText(response.error, 'Could not validate the MFA code'),
error: error instanceof Error ? error.message : this.#localize.term('auth_receivedErrorFromServer'),
};
}

return {};
}

public async resetPassword(email: string): Promise<ResetPasswordResponse> {
Expand Down Expand Up @@ -208,12 +217,11 @@ export class UmbAuthRepository extends UmbRepositoryBase {
case 402:
return this.#localize.term('auth_mfaText');

case 500:
return this.#localize.term('auth_receivedErrorFromServer');
case 403:
return this.#localize.term('auth_userLockedOut');

default:
return (
response.statusText ??
this.#localize.term('auth_receivedErrorFromServer')
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Login/src/localization/lang/da-dk.ts
Expand Up @@ -23,6 +23,7 @@ export default {
passwordMinLength: 'Adgangskoden skal være mindst {0} tegn lang.',
passwordIsBlank: 'Din nye adgangskode kan ikke være tom.',
userFailedLogin: 'Ups! Vi kunne ikke logge dig ind. Tjek at dit brugernavn og adgangskode er korrekt og prøv igen.',
userLockedOut: 'Din konto er blevet låst. Prøv igen senere.',
receivedErrorFromServer: 'Der skete en fejl på serveren',
resetCodeExpired: 'Det link, du har klikket på, er ugyldigt eller udløbet',
userInviteWelcomeMessage: 'Hej og velkommen til Umbraco! På bare 1 minut vil du være klar til at komme i gang, vi skal bare have dig til at oprette en adgangskode.',
Expand All @@ -41,6 +42,7 @@ export default {
mfaText: 'Du har aktiveret multi-faktor godkendelse. Du skal nu bekræfte din identitet.',
mfaMultipleText: 'Vælg venligst en godkendelsesmetode',
mfaCodeInput: 'Kode',
mfaInvalidCode: 'Forkert kode indtastet',
signInWith: 'Log ind med {0}',
returnToLogin: 'Tilbage til log ind',
localLoginDisabled: 'Desværre er det ikke muligt at logge ind direkte. Det er blevet deaktiveret af en login-udbyder.',
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Login/src/localization/lang/de-de.ts
Expand Up @@ -23,6 +23,7 @@ export default {
passwordMinLength: 'Ihr Kennwort muss mindestens {0} Zeichen lang sein.',
passwordIsBlank: 'Ihr neues Kennwort darf nicht leer sein!',
userFailedLogin: 'Hoppla! Wir konnten Sie nicht anmelden. Bitte überprüfen Sie Ihre Anmeldeinformationen und versuchen Sie es erneut.',
userLockedOut: 'Ihr Konto wurde gesperrt. Bitte versuchen Sie es später erneut.',
receivedErrorFromServer: 'Der Server hat einen Fehler gemeldet',
resetCodeExpired: 'Der aufgerufene Link ist ungültig oder abgelaufen',
userInviteWelcomeMessage: 'Hallo und Willkommen bei Umbraco! In nur einer Minute sind Sie bereit loszulegen, Sie müssen nur ein Kennwort festlegen.',
Expand All @@ -41,6 +42,7 @@ export default {
mfaText: 'Sie haben die Multi-Faktor-Authentifizierung aktiviert und müssen Ihre Identität bestätigen.',
mfaMultipleText: 'Bitte wählen Sie einen Multi-Faktor-Anbieter',
mfaCodeInput: 'Bestätigungscode',
mfaInvalidCode: 'Ungültiger Code eingegeben',
signInWith: 'Anmelden mit {0}',
returnToLogin: 'Zurück zur Anmeldung',
localLoginDisabled: 'Leider ist eine direkte Anmeldung nicht möglich. Sie wurde von einem Anbieter deaktiviert.',
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Login/src/localization/lang/en-us.ts
Expand Up @@ -23,6 +23,7 @@ export default {
passwordMinLength: 'The password must be at least {0} characters long.',
passwordIsBlank: 'The password cannot be blank.',
userFailedLogin: 'Oops! We couldn\'t log you in. Please check your credentials and try again.',
userLockedOut: 'Your account has been locked out. Please try again later.',
receivedErrorFromServer: 'Received an error from the server',
resetCodeExpired: 'The link you have clicked on is invalid or has expired',
userInviteWelcomeMessage: 'Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password.',
Expand All @@ -41,6 +42,7 @@ export default {
mfaText: 'You have enabled 2-factor authentication and must verify your identity.',
mfaMultipleText: 'Please choose a 2-factor provider',
mfaCodeInput: 'Verification code',
mfaInvalidCode: 'Invalid code entered',
signInWith: 'Sign in with {0}',
returnToLogin: 'Return to login',
localLoginDisabled: 'Unfortunately, direct login is not possible. It has been disabled by a provider.',
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Login/src/localization/lang/en.ts
Expand Up @@ -23,6 +23,7 @@ export default {
passwordMinLength: 'The password must be at least {0} characters long.',
passwordIsBlank: 'The password cannot be blank.',
userFailedLogin: 'Oops! We couldn\'t log you in. Please check your credentials and try again.',
userLockedOut: 'Your account has been locked out. Please try again later.',
receivedErrorFromServer: 'Received an error from the server',
resetCodeExpired: 'The link you have clicked on is invalid or has expired',
userInviteWelcomeMessage: 'Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password.',
Expand All @@ -41,6 +42,7 @@ export default {
mfaText: 'You have enabled 2-factor authentication and must verify your identity.',
mfaMultipleText: 'Please choose a 2-factor provider',
mfaCodeInput: 'Verification code',
mfaInvalidCode: 'Invalid code entered',
signInWith: 'Sign in with {0}',
returnToLogin: 'Return to login',
localLoginDisabled: 'Unfortunately, direct login is not possible. It has been disabled by a provider.',
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Login/src/localization/lang/nb-no.ts
Expand Up @@ -23,6 +23,7 @@ export default {
passwordMinLength: 'Passordet må være minst {0} tegn langt.',
passwordIsBlank: 'Det nye passordet kan ikke være tomt!',
userFailedLogin: 'Oops! Vi kunne ikke logge deg inn. Vennligst sjekk legitimasjonen din og prøv igjen.',
userLockedOut: 'Kontoen din er låst. Vennligst prøv igjen senere.',
receivedErrorFromServer: 'Mottok en feil fra serveren',
resetCodeExpired: 'Lenken du har klikket på er ugyldig eller har utløpt',
userInviteWelcomeMessage: 'Hei og velkommen til Umbraco! Om bare 1 minutt er du klar til å starte, vi trenger bare at du setter et passord.',
Expand All @@ -41,6 +42,7 @@ export default {
mfaText: 'Du har aktivert tofaktorautentisering og må bekrefte identiteten din.',
mfaMultipleText: 'Velg en tofaktorleverandør',
mfaCodeInput: 'Bekreftelseskode',
mfaInvalidCode: 'Ugyldig kode angitt',
signInWith: 'Logg inn med {0}',
returnToLogin: 'Tilbake til innlogging',
localLoginDisabled: 'Dessverre er direkte innlogging ikke mulig. Den er deaktivert av en leverandør.',
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Login/src/localization/lang/nl-nl.ts
Expand Up @@ -23,6 +23,7 @@ export default {
passwordMinLength: 'Je wachtwoord moet minstens {0} tekens lang zijn.',
passwordIsBlank: 'Je nieuwe wachtwoord mag niet leeg zijn!',
userFailedLogin: 'Oeps! We konden je niet inloggen. Controleer je inloggegevens en probeer het opnieuw.',
userLockedOut: 'Je account is geblokkeerd. Probeer het later opnieuw.',
receivedErrorFromServer: 'Een error ontvangen van de server',
resetCodeExpired: 'De link die je hebt aangeklikt is niet (meer) geldig.',
userInviteWelcomeMessage: 'Hallo en welkom in Umbraco! Binnen ongeveer één minuut kan je aan de slag. Je moet enkel je wachtwoord instellen.',
Expand All @@ -41,6 +42,7 @@ export default {
mfaText: 'Je hebt tweestapsverificatie ingeschakeld en moet je identiteit verifiëren.',
mfaMultipleText: 'Kies een tweestapsverificatie aanbieder',
mfaCodeInput: 'Verificatiecode',
mfaInvalidCode: 'Ongeldige code ingevoerd',
signInWith: 'Inloggen met {0}',
returnToLogin: 'Terug naar loginformulier',
localLoginDisabled: 'Helaas is direct inloggen niet mogelijk. Het is uitgeschakeld door een aanbieder.',
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Login/src/localization/lang/sv-se.ts
Expand Up @@ -23,6 +23,7 @@ export default {
passwordMinLength: 'Lösenordet måste vara minst {0} tecken långt.',
passwordIsBlank: 'Ditt nya lösenord kan inte vara tomt!',
userFailedLogin: 'Hoppsan! Vi kunde inte logga in dig. Vänligen kontrollera dina uppgifter och försök igen.',
userLockedOut: 'Ditt konto har låsts. Vänligen försök igen senare.',
receivedErrorFromServer: 'Ett fel uppstod på servern',
resetCodeExpired: 'Länken du har klickat på är ogiltig eller har gått ut',
userInviteWelcomeMessage: 'Hej och välkommen till Umbraco! Inom bara 1 minut är du redo att börja, vi behöver bara att du ställer in ett lösenord.',
Expand All @@ -41,6 +42,7 @@ export default {
mfaText: 'Du har aktiverat tvåfaktorsautentisering och måste verifiera din identitet.',
mfaMultipleText: 'Vänligen välj en tvåfaktorsleverantör',
mfaCodeInput: 'Verifieringskod',
mfaInvalidCode: 'Ogiltig kod angiven',
signInWith: 'Logga in med {0}',
returnToLogin: 'Återgå till inloggning',
localLoginDisabled: 'Tyvärr är direktinloggning inte möjlig. Det har inaktiverats av en leverantör.',
Expand Down

0 comments on commit 22c0c25

Please sign in to comment.