56

Can I customize the landing page of password reset in Firebase. I want to localize that page since my app is not in english. Is there any way to do so?

Thanks in advance.

1
  • 3
    Seems clear to me -- he is asking how to customize the landing page for password reset since his app is not in English. Jul 2, 2016 at 15:19

3 Answers 3

122

You can customize the Password Reset email under Firebase Console -> Auth -> Email Templates -> Password Reset, and change the link in the email to point to your own page. Note that the <code> placeholder will be replaced by the password reset code in the URL.

Then, in your custom page, you can read the password reset code from the URL and do

firebase.auth().confirmPasswordReset(code, newPassword)
    .then(function() {
      // Success
    })
    .catch(function() {
      // Invalid code
    })

Optionally, you can first check if the code is valid before displaying the password reset form with

firebase.auth().verifyPasswordResetCode(code)
    .then(function(email) {
      // Display a "new password" form with the user's email address
    })
    .catch(function() {
      // Invalid code
    })

Check the docs: https://firebase.google.com/docs/reference/js/v8/firebase.auth.Auth#verifypasswordresetcode

4
  • 5
    When changing the custom action, the form says: "Your custom action URL applies to all email templates"... doesn't it mean, that I need to implement also the email reset (revert) and email verification pages? How does the applyActionCode() works?
    – Tom
    Aug 24, 2016 at 21:51
  • 1
    can you dynamically change the language in the template?
    – Wild Goat
    Jun 3, 2019 at 21:54
  • No, you will need to setup the language translation yourself i18n
    – Sigex
    Nov 18, 2020 at 21:09
  • 1
    Here is the documentation link anyone can follow firebase.google.com/docs/auth/custom-email-handler since the above links in answers are not working any more. Follow this documentation if you are struggling to reset and verify email on web app
    – mzparacha
    Jan 25, 2023 at 8:03
9

Dec 2022 Answer

Replace const firebaseConfig = {}; in code below with your firebaseConfig and you have a working custom email auth handler page.

There are plenty of reasons to want to use a custom email handler page for firebase auth actions.

  1. Use your own custom domain, rather than a firebase unbranded domain, and your custom email handler page can have your own styling and logo, etc. I went with the subdomain https://auth.mydomain.com and the code below is in index.html at the root. So firebase email handler parameters are appended and the links in emails look like https://auth.mydomain.com/?mode=resetPassword&oobCode=longStringCode&apiKey=apiCodeString&lang=en
  2. It is very easy to use the boilerplate code below and host it on firebase hosting (not covered here).
  3. The firebase default reset password page handler only sets a password requirement of >= 6 characters. You cannot yet configure password complexity. For this reason alone, creating a custom email action handler page was enough.

Note: When you set a custom action handler url, the page your url points to has to handle all email action modes. E.g. you can't just set a custom url for password reset and use the default for email verification url in your templates console. You have to handle all modes on your custom email handler page url. The code below handles all modes.

In the code, you'll find the validatePasswordComplexity function. It is currently set to display minimum password complexity requirements, as you can see in the screenshot below. As the user enters the required minimum, the red alert will be removed. E.g. when the user enters a special character, the red alert for missing a special character will disappear, and so on, until the password meets your complexity requirements and alerts have disappeared. The user cannot reset their password until the complexity requirements have been met. If you want the user to be required to enter 2 special characters, e.g., then alter the hasMinSpecialChar regex changing {1,} to {2,}.

enter image description here

Custom Auth Email Mode Handler

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Firebase Auth Handlers</title>
  <meta name="robots" content="noindex">
  <link rel="icon" type="image/png" href="favicon.png"/>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" />
  <style>
    body {
      font-family: Roboto,sans-serif;
    }
    i {
      margin-left: -30px;
      cursor: pointer;
    }
    .button {
      background-color: #141645;
      border: none;
      color: white;
      padding: 11px 20px;
      text-align: center;
      text-decoration: none;
      display: inline-block;
      font-size: 16px;
      margin: 4px 2px;
      cursor: pointer;
      border-radius: 4px;
    }
    input {
      width: 200px;
      padding: 12px 20px;
      margin: 8px 0;
      display: inline-block;
      border: 1px solid #ccc;
      border-radius: 4px;
      box-sizing: border-box;
    }
    .red-alert {
      color: #B71C1C;
    }
    .center {
      position: absolute;
      left: 50%;
      top: 50%;
      -webkit-transform: translate(-50%, -50%);
      transform: translate(-50%, -50%);
      text-align: center;
    }
    #cover-spin {
      position:fixed;
      width:100%;
      left:0;right:0;top:0;bottom:0;
      background-color: rgba(255,255,255,0.7);
      z-index:9999;
    }

    @-webkit-keyframes spin {
      from {-webkit-transform:rotate(0deg);}
      to {-webkit-transform:rotate(360deg);}
    }

    @keyframes spin {
      from {transform:rotate(0deg);}
      to {transform:rotate(360deg);}
    }

    #cover-spin::after {
      content:'';
      display:block;
      position:absolute;
      left:48%;top:40%;
      width:40px;height:40px;
      border-style:solid;
      border-color:black;
      border-top-color:transparent;
      border-width: 4px;
      border-radius:50%;
      -webkit-animation: spin .8s linear infinite;
      animation: spin .8s linear infinite;
    }
  </style>
  <script>
    const AuthHandler = {
      init: props => {
        AuthHandler.conf = props
        AuthHandler.bindMode()
      },
      bindMode: () => {
        switch (AuthHandler.conf.mode) {
          case 'resetPassword':
            AuthHandler.setModeTitle('Password Reset')

            if (!AuthHandler.validateRequiredAuthParams()) {
              AuthHandler.displayErrorMessage(AuthHandler.conf.defaultErrorMessage)
              return
            }

            AuthHandler.handleResetPassword()
            break;
          case 'recoverEmail':
            AuthHandler.setModeTitle('Email Recovery')

            if (!AuthHandler.validateRequiredAuthParams()) {
              AuthHandler.displayErrorMessage(AuthHandler.conf.defaultErrorMessage)
              return
            }

            AuthHandler.handleRecoverEmail()
            break;
          case 'verifyEmail':
            AuthHandler.setModeTitle('Email Verification')

            if (!AuthHandler.validateRequiredAuthParams()) {
              AuthHandler.displayErrorMessage(AuthHandler.conf.defaultErrorMessage)
              return
            }

            AuthHandler.handleVerifyEmail()
            break;
          default:
            AuthHandler.displayErrorMessage(AuthHandler.conf.defaultErrorMessage)
            break;
        }
      },
      handleResetPassword: () => {
        AuthHandler.showLoadingSpinner()

        // Verify the code is valid before displaying the reset password form.
        AuthHandler.conf.verifyPasswordResetCode(AuthHandler.conf.auth, AuthHandler.conf.oobCode).then(() => {
          AuthHandler.hideLoadingSpinner()

          // Display the form if we have a valid reset code.
          AuthHandler.showPasswordResetForm()
          AuthHandler.conf.passwordField.addEventListener('input', AuthHandler.validatePasswordComplexity);

          AuthHandler.conf.passwordToggleButton.addEventListener('click', e => {
            AuthHandler.conf.passwordField.setAttribute(
                    'type',
                    AuthHandler.conf.passwordField.getAttribute('type') === 'password'
                            ? 'text' : 'password');
            e.target.classList.toggle('bi-eye');
          });

          AuthHandler.conf.passwordResetButton.addEventListener('click', () => {
            AuthHandler.hideMessages()

            // Test the password again. If it does not pass, errors will display.
            if (AuthHandler.validatePasswordComplexity(AuthHandler.conf.passwordField)) {
              AuthHandler.showLoadingSpinner()

              // Attempt to reset the password.
              AuthHandler.conf.confirmPasswordReset(
                      AuthHandler.conf.auth,
                      AuthHandler.conf.oobCode,
                      AuthHandler.conf.passwordField.value.trim()
              ).then(() => {
                AuthHandler.hidePasswordResetForm()
                AuthHandler.hideLoadingSpinner()
                AuthHandler.displaySuccessMessage('Password has been reset!')
              }).catch(() => {
                AuthHandler.hideLoadingSpinner()
                AuthHandler.displayErrorMessage('Password reset failed. Please try again.')
              })
            }
          });
        }).catch(() => {
          AuthHandler.hideLoadingSpinner()
          AuthHandler.hidePasswordResetForm()
          AuthHandler.displayErrorMessage('Invalid password reset code. Please try again.')
        });
      },
      handleRecoverEmail: () => {
        AuthHandler.showLoadingSpinner()

        let restoredEmail = null;

        AuthHandler.conf.checkActionCode(AuthHandler.conf.auth, AuthHandler.conf.oobCode).then(info => {
          restoredEmail = info['data']['email'];
          AuthHandler.conf.applyActionCode(AuthHandler.conf.auth, AuthHandler.conf.oobCode).then(() => {
            AuthHandler.conf.sendPasswordResetEmail(AuthHandler.conf.auth, restoredEmail).then(() => {
              AuthHandler.hideLoadingSpinner()
              AuthHandler.displaySuccessMessage(`Your email has been restored and a reset password email has been sent to ${restoredEmail}. For security, please reset your password immediately.`)
            }).catch(() => {
              AuthHandler.hideLoadingSpinner()
              AuthHandler.displaySuccessMessage(`Your email ${restoredEmail} has been restored. For security, please reset your password immediately.`)
            })
          }).catch(() => {
            AuthHandler.hideLoadingSpinner()
            AuthHandler.displayErrorMessage('Sorry, something went wrong recovering your email. Please try again or contact support.')
          })
        }).catch(() => {
          AuthHandler.hideLoadingSpinner()
          AuthHandler.displayErrorMessage('Invalid action code. Please try again.')
        })
      },
      handleVerifyEmail: () => {
        AuthHandler.showLoadingSpinner()
        AuthHandler.conf.applyActionCode(AuthHandler.conf.auth, AuthHandler.conf.oobCode).then(() => {
          AuthHandler.hideLoadingSpinner()
          AuthHandler.displaySuccessMessage('Email verified! Your account is now active. Time to send some messages!')
        }).catch(() => {
          AuthHandler.hideLoadingSpinner()
          AuthHandler.displayErrorMessage('Your code is invalid or has expired. Please try to verify your email address again by tapping the resend email button in app.')
        })
      },
      validateRequiredAuthParams: () => {
        // Mode is evaluated in the bindMode switch. If no mode will display default error message. So, we're just
        // checking for a valid oobCode here.
        return !!AuthHandler.conf.oobCode
      },
      setModeTitle: title => {
        AuthHandler.conf.modeTitle.innerText = title
      },
      validatePasswordComplexity: e => {
        const password = !!e.target ? e.target.value.trim() : e.value.trim()
        const isValidString = typeof password === 'string'

        /// Checks if password has minLength
        const hasMinLength = isValidString && password.length >= 8
        AuthHandler.conf.passwordHasMinLength.style.display = hasMinLength ? 'none' : ''

        /// Checks if password has at least 1 normal char letter matches
        const hasMinNormalChar = isValidString && password.toUpperCase().match(RegExp('^(.*?[A-Z]){1,}')) !== null
        AuthHandler.conf.passwordHasMinNormalChar.style.display = hasMinNormalChar ? 'none' : ''

        /// Checks if password has at least 1 uppercase letter matches
        const hasMinUppercase =
                isValidString && password.match(RegExp('^(.*?[A-Z]){1,}')) !== null
        AuthHandler.conf.passwordHasMinUppercase.style.display = hasMinUppercase ? 'none' : ''

        /// Checks if password has at least 1 numeric character matches
        const hasMinNumericChar =
                isValidString && password.match(RegExp('^(.*?[0-9]){1,}')) !== null
        AuthHandler.conf.passwordHasMinNumericChar.style.display = hasMinNumericChar ? 'none' : ''

        /// Checks if password has at least 1 special character matches
        const hasMinSpecialChar = isValidString && password.match(RegExp("^(.*?[\$&+,:;/=?@#|'<>.^*()_%!-]){1,}")) !== null
        AuthHandler.conf.passwordHasMinSpecialChar.style.display = hasMinSpecialChar ? 'none' : ''

        const passing = hasMinLength &&
                hasMinNormalChar &&
                hasMinUppercase &&
                hasMinNumericChar &&
                hasMinSpecialChar
        AuthHandler.conf.passwordIncreaseComplexity.style.display = passing ? 'none' : ''

        return passing
      },
      showLoadingSpinner: () => {
        AuthHandler.conf.loading.style.display = ''
      },
      hideLoadingSpinner: () => {
        AuthHandler.conf.loading.style.display = 'none'
      },
      showPasswordResetForm: () => {
        AuthHandler.conf.passwordForm.style.display = '';
      },
      hidePasswordResetForm: () => {
        AuthHandler.conf.passwordForm.style.display = 'none';
      },
      displaySuccessMessage: message => {
        AuthHandler.hideErrorMessage()
        AuthHandler.conf.success.innerText = message
        AuthHandler.conf.success.style.display = ''
      },
      hideSuccessMessage: () => {
        AuthHandler.conf.success.innerText = ''
        AuthHandler.conf.success.style.display = 'none'
      },
      displayErrorMessage: message => {
        AuthHandler.hideSuccessMessage()
        AuthHandler.conf.error.innerText = message
        AuthHandler.conf.error.style.display = ''
      },
      hideErrorMessage: () => {
        AuthHandler.conf.error.innerText = ''
        AuthHandler.conf.error.style.display = 'none'
      },
      hideMessages: () => {
        AuthHandler.hideErrorMessage()
        AuthHandler.hideSuccessMessage()
      },
    }
  </script>
</head>
<body>
<div class="center">
  <div id="cover-spin" style="display: none;"></div>
  <p>
    <image src="https://via.placeholder.com/400x70/000000/FFFFFF?text=Your+Logo"/>
  </p>
  <p id="mode-title" style="font-size: 20px; font-weight: bold;"></p>
  <p id="error" class="red-alert" style="display: none;"></p>
  <p id="success" style="display: none;"></p>
  <div id="password-form" style="min-width: 700px; min-height: 300px; display: none;">
    <label for="password">New Password</label>
    <input id="password" type="password" minlength="8" maxlength="35" autocomplete="off" placeholder="Enter new password" style="margin-left: 10px;" required>
    <i class="bi bi-eye-slash" id="toggle-password"></i>
    <button id="reset-button" type="button" class="button" style="margin-left: 20px;">Reset</button>
    <p class="red-alert" id="increase-complexity" style="display: none;"><strong>Increase Complexity</strong></p>
    <p class="red-alert" id="has-min-length" style="display: none;">Minimum 8 characters</p>
    <p class="red-alert" id="has-min-normal-char" style="display: none;">Minimum 1 normal characters</p>
    <p class="red-alert" id="has-min-uppercase" style="display: none;">Minimum 1 uppercase characters</p>
    <p class="red-alert" id="has-min-numeric-char" style="display: none;">Minimum 1 numeric characters</p>
    <p class="red-alert" id="has-min-special-char" style="display: none;">Minimum 1 special characters</p>
  </div>
</div>

<script type="module">
  // https://firebase.google.com/docs/web/setup#available-libraries
  // https://firebase.google.com/docs/web/alt-setup
  import { initializeApp } from 'https://www.gstatic.com/firebasejs/9.15.0/firebase-app.js';
  import {
    applyActionCode,
    checkActionCode,
    confirmPasswordReset,
    getAuth,
    sendPasswordResetEmail,
    verifyPasswordResetCode,
  } from 'https://www.gstatic.com/firebasejs/9.15.0/firebase-auth.js';

  // Replace {} with your firebaseConfig
  // https://firebase.google.com/docs/web/learn-more#config-object
  const firebaseConfig = {};

  const app = initializeApp(firebaseConfig);
  const auth = getAuth(app);

  document.addEventListener('DOMContentLoaded', () => {
    // Get the mode and oobCode from url params
    const params = new Proxy(new URLSearchParams(window.location.search), {
      get: (searchParams, prop) => searchParams.get(prop),
    });

    AuthHandler.init({
      app,
      auth,
      applyActionCode,
      checkActionCode,
      confirmPasswordReset,
      getAuth,
      sendPasswordResetEmail,
      verifyPasswordResetCode,
      // Used by all modes to display error or success messages
      error: document.getElementById('error'),
      success: document.getElementById('success'),
      // https://firebase.google.com/docs/auth/custom-email-handler#create_the_email_action_handler_page
      mode: params.mode,
      oobCode: params.oobCode,
      modeTitle: document.getElementById('mode-title'),
      loading: document.getElementById('cover-spin'),
      // Password reset elements
      passwordForm: document.getElementById('password-form'),
      passwordField: document.getElementById('password'),
      passwordResetButton: document.getElementById('reset-button'),
      passwordToggleButton: document.getElementById('toggle-password'),
      passwordHasMinLength: document.getElementById('has-min-length'),
      passwordHasMinNormalChar: document.getElementById('has-min-normal-char'),
      passwordHasMinUppercase: document.getElementById('has-min-uppercase'),
      passwordHasMinNumericChar: document.getElementById('has-min-numeric-char'),
      passwordHasMinSpecialChar: document.getElementById('has-min-special-char'),
      passwordIncreaseComplexity: document.getElementById('increase-complexity'),
      defaultErrorMessage: 'Invalid auth parameters. Please try again.',
    });
  });
</script>
</body>
</html>

Note: I opted out of using the new modular tree shakable way of doing things, because I didn't want to setup webpack, and opted for the alt setup style so I could use the latest firebasejs version, which, as of this writing, is v9.15.0. If you're worried about bloat on your single page custom email handler page, then look into tree shaking and webpack. I went for the super fast option with less configuring.

Note: I am not handling the lang or apiKey param included in the url from firebase. My use case doesn't need to do any language changes.

After tapping reset, if all goes well with firebase auth, the user will see this. For each action mode the success and error messages will display accordingly.

enter image description here

1
  • 1
    Bruv you built a whole web framework just to do a reset password flow. Hats off to you!
    – dwu39
    Jul 6, 2023 at 19:09
2

The answer provided by @Channing Huang is the correct answer but you will also need to keep in mind that the error that it will return will not always be invalid-code.

Checking to see if the error is perhaps expired where the user didn't open the URL till later or possibly other cases. You can then send another email if expired.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.