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.
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.
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
applyActionCode()
works?
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.
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
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,}
.
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.
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.