From d7537822640fa052763078124e5cdf5850432ca7 Mon Sep 17 00:00:00 2001 From: nhienlam Date: Fri, 27 Oct 2023 13:54:16 -0700 Subject: [PATCH] Update password reset handler to handle 'PASSWORD_DOES_NOT_MEET_REQUIREMENTS' error --- javascript/widgets/handler/actioncode.js | 33 ++++-- javascript/widgets/handler/actioncode_test.js | 73 ++++++++++++ javascript/widgets/handler/common.js | 63 ++++++++++ javascript/widgets/handler/common_test.js | 13 ++ soy/strings.soy | 112 +++++++++++++++++- 5 files changed, 281 insertions(+), 13 deletions(-) diff --git a/javascript/widgets/handler/actioncode.js b/javascript/widgets/handler/actioncode.js index f86665e7..f85e921a 100644 --- a/javascript/widgets/handler/actioncode.js +++ b/javascript/widgets/handler/actioncode.js @@ -129,27 +129,36 @@ firebaseui.auth.widget.handler.resetPassword_ = function( */ firebaseui.auth.widget.handler.handlePasswordResetFailure_ = function( app, container, opt_component, opt_error) { - var errorCode = opt_error && opt_error['code']; - if (errorCode == 'auth/weak-password') { + const errorCode = opt_error && opt_error['code']; + if (errorCode === 'auth/weak-password') { // Handles this error differently as it just requires to display a message // to the user to use a longer password. - var errorMessage = + const errorMessage = firebaseui.auth.widget.handler.common.getErrorMessage(opt_error); firebaseui.auth.ui.element.setValid( opt_component.getNewPasswordElement(), false); firebaseui.auth.ui.element.show( opt_component.getNewPasswordErrorElement(), errorMessage); opt_component.getNewPasswordElement().focus(); - return; - } - - if (opt_component) { - opt_component.dispose(); + } else if (errorCode === 'auth/password-does-not-meet-requirements') { + // Pass the error message from the backend which contains all the password + // requirements to be met. + const errorMessage = + firebaseui.auth.widget.handler.common.getErrorMessage(opt_error); + firebaseui.auth.ui.element.setValid( + opt_component.getNewPasswordElement(), false); + firebaseui.auth.ui.element.show( + opt_component.getNewPasswordErrorElement(), errorMessage); + opt_component.getNewPasswordElement().focus(); + } else { + if (opt_component) { + opt_component.dispose(); + } + var component = new firebaseui.auth.ui.page.PasswordResetFailure(); + component.render(container); + // Set current UI component. + app.setCurrentComponent(component); } - var component = new firebaseui.auth.ui.page.PasswordResetFailure(); - component.render(container); - // Set current UI component. - app.setCurrentComponent(component); }; diff --git a/javascript/widgets/handler/actioncode_test.js b/javascript/widgets/handler/actioncode_test.js index 5828f69e..d09a2d19 100644 --- a/javascript/widgets/handler/actioncode_test.js +++ b/javascript/widgets/handler/actioncode_test.js @@ -201,6 +201,79 @@ function testHandlePasswordReset_weakPasswordError() { } +function testHandlePasswordReset_nonCompliantPasswordError() { + const errorMessage = 'Missing password requirements: [Password may contain at most 16 characters, Password must contain a lower case character, Password must contain an upper case character, Password must contain a numeric character, Password must contain a non-alphanumeric character]'; + const error = { + 'code': 'auth/password-does-not-meet-requirements', + 'message': errorMessage + }; + asyncTestCase.waitForSignals(1); + firebaseui.auth.widget.handler.handlePasswordReset( + app, container, 'PASSWORD_RESET_ACTION_CODE'); + // Successful action code verification. + app.getAuth().assertVerifyPasswordResetCode( + ['PASSWORD_RESET_ACTION_CODE'], 'user@example.com'); + app.getAuth() + .process() + .then(function() { + // Password reset page should show. + assertPasswordResetPage(); + + goog.dom.forms.setValue(getNewPasswordElement(), '123'); + // Submit password reset form. + submitForm(); + // Simulates password doesn't meet requirements. + app.getAuth().assertConfirmPasswordReset( + ['PASSWORD_RESET_ACTION_CODE', '123'], null, error); + return app.getAuth().process(); + }) + .then(function() { + // Error message should be shown on the same page. + assertPasswordResetPage(); + assertEquals( + firebaseui.auth.widget.handler.common.getErrorMessage(error), + getNewPasswordErrorMessage()); + asyncTestCase.signal(); + }); +} + + +function testHandlePasswordReset_nonCompliantPassword_emptyError() { + const error = { + 'code': 'auth/password-does-not-meet-requirements', + 'message': '' + }; + asyncTestCase.waitForSignals(1); + firebaseui.auth.widget.handler.handlePasswordReset( + app, container, 'PASSWORD_RESET_ACTION_CODE'); + // Successful action code verification. + app.getAuth().assertVerifyPasswordResetCode( + ['PASSWORD_RESET_ACTION_CODE'], 'user@example.com'); + app.getAuth() + .process() + .then(function() { + // Password reset page should show. + assertPasswordResetPage(); + + goog.dom.forms.setValue(getNewPasswordElement(), '123'); + // Submit password reset form. + submitForm(); + // Simulates password doesn't meet requirements. + app.getAuth().assertConfirmPasswordReset( + ['PASSWORD_RESET_ACTION_CODE', '123'], null, error); + return app.getAuth().process(); + }) + .then(function() { + // Error message should be shown on the same page. + assertPasswordResetPage(); + assertEquals( + firebaseui.auth.widget.handler.common.getErrorMessage(error), + getNewPasswordErrorMessage()); + asyncTestCase.signal(); + }); +} + + function testHandlePasswordReset_failToResetPassword() { asyncTestCase.waitForSignals(1); firebaseui.auth.widget.handler.handlePasswordReset( diff --git a/javascript/widgets/handler/common.js b/javascript/widgets/handler/common.js index 1f1b14e6..73bb1bca 100644 --- a/javascript/widgets/handler/common.js +++ b/javascript/widgets/handler/common.js @@ -383,6 +383,69 @@ firebaseui.auth.widget.handler.common.getSignedInRedirectUrl_ = * @package */ firebaseui.auth.widget.handler.common.getErrorMessage = function(error) { + // Password policy error message varies depending on the policy violations. + // Hence, we construct an error message from the strings file based on the + // error message from the backend. + if (error['code'] && + error['code'] == 'auth/password-does-not-meet-requirements') { + const originalError = error['message']; + let minPasswordLength = ''; + let maxPasswordLength = ''; + let passwordNotMeetMinLength = false; + let passwordNotMeetMaxLength = false; + let passwordNotContainLowercaseLetter = false; + let passwordNotContainUppercaseLetter = false; + let passwordNotContainNumericCharacter = false; + let passwordNotContainNonAlphanumericCharacter = false; + + const minLengthErrorMatch = + originalError.match(/Password must contain at least (\d+)/); + if (minLengthErrorMatch) { + passwordNotMeetMinLength = true; + minPasswordLength = minLengthErrorMatch[1]; + } + const maxLengthErrorMatch = + originalError.match(/Password may contain at most (\d+)/); + if (maxLengthErrorMatch) { + passwordNotMeetMaxLength = true; + maxPasswordLength = maxLengthErrorMatch[1]; + } + // This needs to be hardcoded. Checking against + // "firebaseui.auth.soy2.strings.errorPasswordNotContainLowercaseLetter().toString()" + // will return a translated string, while originalError is always in + // English. + if (originalError.includes( + 'Password must contain a lower case character')) { + passwordNotContainLowercaseLetter = true; + } + if (originalError.includes( + 'Password must contain an upper case character')) { + passwordNotContainUppercaseLetter = true; + } + if (originalError.includes('Password must contain a numeric character')) { + passwordNotContainNumericCharacter = true; + } + if (originalError.includes( + 'Password must contain a non-alphanumeric character')) { + passwordNotContainNonAlphanumericCharacter = true; + } + + return firebaseui.auth.soy2.strings + .errorMissingPasswordRequirements({ + passwordNotMeetMinLength: passwordNotMeetMinLength, + minPasswordLength: minPasswordLength, + passwordNotMeetMaxLength: passwordNotMeetMaxLength, + maxPasswordLength: maxPasswordLength, + passwordNotContainLowercaseLetter: passwordNotContainLowercaseLetter, + passwordNotContainUppercaseLetter: passwordNotContainUppercaseLetter, + passwordNotContainNumericCharacter: + passwordNotContainNumericCharacter, + passwordNotContainNonAlphanumericCharacter: + passwordNotContainNonAlphanumericCharacter + }) + .toString(); + } + // Try to get an error message from the strings file, or fall back to the // error message from the Firebase SDK if none is found. var message = diff --git a/javascript/widgets/handler/common_test.js b/javascript/widgets/handler/common_test.js index 9f3c4146..9479e790 100644 --- a/javascript/widgets/handler/common_test.js +++ b/javascript/widgets/handler/common_test.js @@ -1336,6 +1336,19 @@ function testGetErrorMessage_unknownError_jsonMessage() { } +function testGetErrorMessage_passwordPolicyError_message() { + const error = { + code: 'auth/password-does-not-meet-requirements', + message: + 'Missing password requirements: [Password must contain at least 8 characters, Password may contain at most 16 characters, Password must contain a lower case character, Password must contain an upper case character, Password must contain a numeric character, Password must contain a non-alphanumeric character]' + }; + const message = firebaseui.auth.widget.handler.common.getErrorMessage(error); + assertEquals( + 'Missing password requirements: [ Password must contain at least 8 characters. Password may contain at most 16 characters. Password must contain a lower case character. Password must contain an upper case character. Password must contain a numeric character. Password must contain a non-alphanumeric character. ]', + message); +} + + function testIsPasswordProviderOnly_multipleMixedProviders() { // Set a password and federated providers in the FirebaseUI instance // configuration. diff --git a/soy/strings.soy b/soy/strings.soy index 844300a7..c5bc58c3 100644 --- a/soy/strings.soy +++ b/soy/strings.soy @@ -210,6 +210,111 @@ {/template} + +/** Error message for when the user tries to sign in, sign up, or reset password with a + password that is not compliant with the policy.. */ +{template .errorMissingPasswordRequirements kind="text"} + {@param passwordNotMeetMinLength: bool} /** Whether to display the + passwordNotMeetMinLength + error. */ + {@param minPasswordLength: string} /** The minimum password length. */ + {@param passwordNotMeetMaxLength: bool} /** Whether to display the + passwordNotMeetMaxLength + error. */ + {@param maxPasswordLength: string} /** The maximum password length. */ + {@param passwordNotContainLowercaseLetter: bool} /** Whether to display the + passwordNotContainLowercaseLetter + error. */ + {@param passwordNotContainUppercaseLetter: bool} /** Whether to display the + passwordNotContainUppercaseLetter + error. */ + {@param passwordNotContainNumericCharacter: bool} /** Whether to display the + passwordNotContainNumericCharacter + error. */ + {@param passwordNotContainNonAlphanumericCharacter: bool} /** Whether to display the + passwordNotContainNonAlphanumericCharacter + error. */ + + {call .error} + {param code: 'auth/password-does-not-meet-requirements' /} + {/call}{sp} + [{sp} + {if $passwordNotMeetMinLength and $minPasswordLength != null} + {call .errorPasswordNotMeetMinLength} + {param minPasswordLength: $minPasswordLength /} + {/call}.{sp} + {/if} + {if $passwordNotMeetMaxLength and $maxPasswordLength != null} + {call .errorPasswordNotMeetMaxLength} + {param maxPasswordLength: $maxPasswordLength /} + {/call}.{sp} + {/if} + {if $passwordNotContainLowercaseLetter}{call .errorPasswordNotContainLowercaseLetter /}.{sp}{/if} + {if $passwordNotContainUppercaseLetter}{call .errorPasswordNotContainUppercaseLetter /}.{sp}{/if} + {if $passwordNotContainNumericCharacter}{call .errorPasswordNotContainNumericCharacter /}.{sp}{/if} + {if $passwordNotContainNonAlphanumericCharacter} + {call .errorPasswordNotContainNonAlphanumericCharacter /}.{sp} + {/if} + ] +{/template} + + +/** Error message for a password that is shorter than the minimum password length. */ +{template .errorPasswordNotMeetMinLength kind="text"} + {@param minPasswordLength: string} /** The minimum password length. */ + {msg desc="Error message when the user enters a password that is shorter than the minimum password + length."} + Password must contain at least {$minPasswordLength} characters + {/msg} +{/template} + + +/** Error message for a password that is longer than the maximum password length. */ +{template .errorPasswordNotMeetMaxLength kind="text"} + {@param maxPasswordLength: string} /** The maximum password length. */ + {msg desc="Error message when the user enters a password that is longer than the maximum password + length."} + Password may contain at most {$maxPasswordLength} characters + {/msg} +{/template} + + +/** Error message for a password that does not contain a lower case character. */ +{template .errorPasswordNotContainLowercaseLetter kind="text"} + {msg desc="Error message when the user enters a password that does not contain a lower case + character."} + Password must contain a lower case character + {/msg} +{/template} + + +/** Error message for a password that does not contain an upper case character. */ +{template .errorPasswordNotContainUppercaseLetter kind="text"} + {msg desc="Error message when the user enters a password that does not contain an upper case + character."} + Password must contain an upper case character + {/msg} +{/template} + + +/** Error message for a password that does not contain a numeric character. */ +{template .errorPasswordNotContainNumericCharacter kind="text"} + {msg desc="Error message when the user enters a password that does not contain a numeric + character."} + Password must contain a numeric character + {/msg} +{/template} + + +/** Error message for a password that does not contain a non-alphanumeric character. */ +{template .errorPasswordNotContainNonAlphanumericCharacter kind="text"} + {msg desc="Error message when the user enters a password that does not contain a non-alphanumeric + character."} + Password must contain a non-alphanumeric character + {/msg} +{/template} + + /** Translates an error code from Firebase Auth to a user-displayable string. */ {template .error kind="text"} {@param? code: string} /** The error code. */ @@ -241,7 +346,7 @@ {case 'auth/weak-password'} {msg desc="Error message for when the user tries to sign in or sign up with a password that is too short."} - Strong passwords have at least 6 characters and a mix of letters and numbers + The password must be at least 6 characters long {/msg} {case 'auth/wrong-password'} {msg desc="Error message for incorrect password."} @@ -272,6 +377,11 @@ The action code is invalid. This can happen if the code is malformed, expired, or has already been used. {/msg} + {case 'auth/password-does-not-meet-requirements'} + {msg desc="Error message for when the user tries to sign in, sign up, or reset password with a + password that is not compliant with the policy."} + Missing password requirements: + {/msg} {default} {/switch} {/template}