Firebase Email Link Authentication - javascript

I am using passwordless authentication for a project, everything is working as expected, however I have one question about this authentication. I will talk about the scenario.
First step: as we all know, a new user needs an email and then proceeds to click the link to login.
That is the normal case, no problem with it, but what if a user has already done that step and say he/she logs out from the app? it seems like they need to do the first step I described above again.
Here is what I have tried so far:
login() {
const email = this.email;
this.$store
.dispatch("LOGIN", { email })
.then(resp => {
this.$router.replace("/");
})
.catch(err => {
this.autherror = true,
this.errorMessage = err.message;
});
}
LOGIN: ({ commit }, user) => {
return new Promise((resolve, reject) => {
// commit(AUTH_REQUEST)
firebase.auth().signInWithEmailLink(user.email, window.location.href)
.then((result) => {
window.localStorage.removeItem("emailForSignIn");
resolve(result);
})
.catch((err) => {
reject(err);
// Some error occurred, you can inspect the code: error.code
// Common errors could be invalid email and invalid or expired OTPs.
});
});
},
I will get an error "Invalid email link!" trying the above code and even if I put the url as the previous one I logged in with, It will also throw an error "The action code is invalid. This can happen if the code is malformed, expired, or has already been used"
I can understand the point why an email to login is always required but the main point am trying to say is, if a user log's in from the link at first and then log's out, they can sign in the app without needing to do first step again, how? that means if there is a way to store credentials in cookies/localstorage, and the only time they need to do the first step again is if they clear the cookies, storage etc. from all or that particular app/page requiring.
So is it possible? It is something that will definitely improve user experience.

You should read and understand how users work in Firebase (and basically the same in any oAuth type verification system) - https://firebase.google.com/docs/auth/users
and more particularly, how email is used - https://firebase.google.com/docs/auth/web/email-link-auth
In your code you should use the email confirmation steps as shown in the reference above (so, something like the code below - you may need some minor changes to fit your local scenario):
LOGIN: ({ commit }, user) => {
return new Promise((resolve, reject) => {
// Confirm the link is a sign-in with email link.
if (firebase.auth().isSignInWithEmailLink(window.location.href)) {
// Additional state parameters can also be passed via URL.
// This can be used to continue the user's intended action before triggering
// the sign-in operation.
// Get the email if available. This should be available if the user completes
// the flow on the same device where they started it.
var email = window.localStorage.getItem('emailForSignIn');
if (!email) {
// User opened the link on a different device. To prevent session fixation
// attacks, ask the user to provide the associated email again. For example:
email = window.prompt('Please provide your email for confirmation');
}
// commit(AUTH_REQUEST)
firebase.auth().signInWithEmailLink(email, window.location.href)
.then((result) => {
window.localStorage.removeItem("emailForSignIn");
resolve(result);
})
.catch((err) => {
reject(err);
// Some error occurred, you can inspect the code: error.code
// Common errors could be invalid email and invalid or expired OTPs.
});
});
}
},

Just indicate in your db that the person has been verified in case you don't want the local storage to store the data

Related

Firebase: Email verification link always expired even though verification works

I'm trying to set up an email verification flow in my project, but I can't seem to get it right.
How my flow works now is the user enters their credentials (email and password), which are used to create a new firebase user. Then, once that promise is resolved, it sends an email verification link to the new user that was created. The code looks like this:
async createUser(email: string, password: string) {
try {
console.log("Creating user...");
const userCredentials = await createUserWithEmailAndPassword(
auth,
email,
password
);
console.log("Successfully created user");
const { user } = userCredentials;
console.log("Sending email verification link...");
await this.verifyEmail(user);
console.log("EMAIL VERIFICATION LINK SUCCESSFULLY SENT");
return user;
} catch (err) {
throw err;
}
}
async verifyEmail(user: User) {
try {
sendEmailVerification(user);
} catch (err) {
throw err;
}
}
The link is sent through fine, but once I press on it, I'm redirected to a page that says this:
Strangely, the user's email is verified after this, in spite of the error message displayed. Any idea why this is happening?
Update:
I managed to figure it out. The email provider I'm using is my university's, and it seems to be preventing the verification link from working properly. I did try with my personal email to see if that was the case, but I wasn't seeing the verification link appearing there. I eventually realized that it was because it was being stored in the spam folder. It's working on other email providers, though, ideally, I'd want it to work on my university's email provider (the emails that users sign up with are supposed to be exclusively student emails). Any ideas how I could resolve this?
I eventually figured out that the issue was with my email provider. I was using my student email, which the university provides, and I imagine they've placed rigorous measures in place to secure them as much as possible. I have no idea what was preventing it from working, but I managed to figure out a workaround.
In brief, I changed the action URL in the template (which can be found in the console for your Firebase project in the Authentication section, under the Templates tab) to a route on my website titled /authenticate. I created a module to handle email verification. Included in it is a function that parses the URL, extracting the mode (email verification, password reset, etc.), actionCode (this is the important one. It stores the id that Firebase decodes to determine if it's valid), continueURL (optional), and lang (optional).
export const parseUrl = (queryString: string) => {
const urlParams = new URLSearchParams(window.location.search);
const mode = urlParams.get("mode");
const actionCode = urlParams.get("oobCode");
const continueUrl = urlParams.get("continueUrl");
const lang = urlParams.get("lang") ?? "en";
return { mode, actionCode, continueUrl, lang };
};
I created another method that handles the email verification by applying the actionCode from the URL using Firebase's applyActionCode.
export const handleVerifyEmail = async (
actionCode: string,
continueUrl?: string,
lang?: string
) => {
try {
await applyActionCode(auth, actionCode);
return { alreadyVerified: false };
} catch (err) {
if (err instanceof FirebaseError) {
switch (err.code) {
case "auth/invalid-action-code": {
return { alreadyVerified: true };
}
}
}
throw err;
}
};
The auth/invalid-action-code error seems to be thrown when the user is already verified. I don't throw an error for it, because I handle this differently to other errors.
Once the user presses the verification link, they're redirected to the /authenticate page on my website. This page then handles the email verification by parsing the query appended to the route. The URL looks something like this http://localhost:3000/authenticate?mode=verifyEmail&oobCode=FLVl85S-ZI13_am0uwWeb4Jy8DUWC3E6kIiwN2LLFpUAAAGDUJHSwA&apiKey=AIzaSyA_V9nKEZeoTOECWaD7UXuzqCzcptmmHQI&lang=en
Of course, in production, the root path would be the name of the website instead of localhost. I have my development environment running on port 3000.
Once the user lands on the authentication page, I handle the email verification in a useEffect() hook (Note: I'm using Next.js, so if you're using a different framework you might have to handle changing the URL differently):
useEffect(() => {
verifyEmail();
async function verifyEmail() {
const { actionCode } = parseUrl(window.location.search);
if (!actionCode) return;
router.replace("/authenticate", undefined, { shallow: true });
setLoadingState(LoadingState.LOADING);
try {
const response = await handleVerifyEmail(actionCode!);
if (response.alreadyVerified) {
setEmailAlreadyVerified(true);
onEmailAlreadyVerified();
return;
}
setLoadingState(LoadingState.SUCCESS);
onSuccess();
} catch (err) {
console.error(err);
onFailure();
setLoadingState(LoadingState.ERROR);
}
}
}, []);
It first checks if there is an action code in the URL, in case a user tries to access the page manually.
The onSuccess, onFailure, and onEmailAlreadyVerified callbacks just display toasts. loadingState and emailAlreadyVerified are used to conditionally render different responses to the user.

How do I catch specific error returned by Firebase Auth, when user updates their password?

I am writing an app, where I want to give the user possibility to change their password. So I have a simple UpdatePassword.js page, where I invoke Firebase Authentication .updatePassword(password) method. As explained in the docs, this is a sensitive operation, and as such, the user needs to authenticate (if they haven't authenticated recently), in order to change their password to a new one.
This is my method:
const update = async () => {
const user = await firebase.auth().currentUser;
await user
.updatePassword(password)
.then(() => {
setUpdated(true);
})
.catch((error) => {
//I want to handle this specific error but I don't know how
if (
error.message ===
"This operation is sensitive and requires recent authentication. Log in again before retrying this request."
) {
console.log("should display a modal for user to authenticate again");
}
console.log("error while updating pass: ", error);
setSaving(false);
});
};
As you can see from my console.logs, in the case where the user needs to authenticate again, I want to display a modal, where they will sign in with their credentials again. This is not a problem and is easy to do. However, my question is, how do I catch this specific type of error where the user needs to authenticate? As per my console.logs, the way I have implemented it right now, I am just comparing the error message which I receive from Firebase Authentication, which is really not the right way to do. What if Firebase Auth change the error message to something else? Is there something like an error code which I can compare to the error thrown, and handle the exception by error code or something more safe than just a string message?
As you will see in the doc, the error that is thrown in this case (i.e. "if the user's last sign-in time does not meet the security threshold") has an auth/requires-recent-login error code.
So:
//...
.catch((error) => {
if (error.code === 'auth/requires-recent-login') {
// Display the modal
} else {
// ...

firebase auth applyActionCode method does not turn emailVerified to true

I am developing firebase authentication system where a user is sent email to verify email adr. I got everything working eventually. The user signs up and the email (with the link) is sent to the signed up edmail adr. I use custom email action handler (https://firebase.google.com/docs/auth/custom-email-handler) to respond to a click on the link. On my node.js express route, I get oobCode (which firebase documentation say is "A one-time code, used to identify and verify a request") and pass it as an argument to firebase.auth().applyActionCode(oobCode) which returns a void promise when resolved. see code below.
firebase.auth().applyActionCode(oobCode)
.then( () => {
return admin.auth().updateUser(currentUser.uid, {
emailVerified: true,
})
})
.then( () => {
return res.status(301).redirect(`../unps/${currentUser.uid}`)
})
.catch( (error) => {
return res.status(500).json({ unp_error: `error message: ${error.message} - error code:
${error.code}` })
});
My undestanding of the documentation is that, applyActionCode method, if resolved, will set the emailVerified to true but this does not happen even though there is no error. I had to call updateUser to change emailVerified to true. Shouldnt this be done automatically by the method applyActionCode if a valid oobCode is presented as argument? What am I missinmg? Pleaase help?
All information about the Firebase Authentication user in your application code is taken from the ID token. This ID token is valid for an hour, and automatically refreshed by the SDK about 5 minutes before it expires. Until the token is refreshed, it may not reflect the latest value of emailVerified or other information about that user profile on the server.
It is indeed normal that you need to force a refresh of the token, to get the updated status before it auto-refreshes. When you do that, you shouldn't have to call admin.auth().updateUser(...) though.

firebase - Firebase Current User not storing the correct user

I have a project using Firebase for the authentication. When I log into my project, sometimes the firebase.auth().currentUser method returns info from some other user. Sometimes I get the info from the actual current user and sometimes the previously signed in user - when this happens, the only way to correct the problem and get data for the correct user is to logout and sign back in again.
My login method:
onLoginClick = () => {
firebase.auth().signInWithEmailAndPassword(this.state.email.trim(), this.state.password.trim())
.then((user) => {
//Some redirects depending on my users role
})
.catch((error) => {
console.log(error, error.message);
if (error.code == "auth/user-not-found" || error.code == "auth/wrong-password") {
//Handle error
}
});
}
And my Logout method:
onLogoutClick = () => {
firebase.auth().signOut().then(function () {
this.props.history.replace("/")
}).catch(function (error) {
console.log(error);
});
}
They're pretty standard but seems like I'm doing something wrong. I've tried clearing the sessionStorage and the localStorage on the logout method but that didn't help.
UPDATE:
I've tried with some suggestions like doing:
firebase.auth().signOut().then(() => {
firebase.auth().signInWithEmailAndPassword(email, password).then((user) => {
//My redirects and everything
})
})
But this doesn't work.
Also, I've noticed that when I log in and use wrong credentials, the sessionStorage gets an entry with the previously signed in user, even though I signed him out.
I'm pretty sure that during the initialization of a new login session, until it is complete, currentUser hasn't been updated yet; use the user parameter when one is passed in on those methods. Because the login is still in progress, there is not yet a currentUser, so it passes the user in progress via the user parameter. (I think this means it is likely that it will be null the first time someone logs in, and it will be the previous one thereafter, unless it's cleared on logout by some code.)
Use the user parameter when it is provided, such as in the onAuthStateChanged handler. See the first example here, and the starred note in the blue box farther down that same page.

Firebase 4.1.3: onAuthStateChange not called after first sign in with email and password

I upgraded the version of Firebase for my app from 3.5.0 to 4.1.3 and noticed that the onAuthStateChange callback function is no longer called after a user successfully signs in for the first time after verifying their email address.
The app is written in JavaScript.
These are the relevant sections of my code:
Callback setup
firebase.auth().onAuthStateChanged(onAuthStateChange);
Callback
function onAuthStateChange (user) {
console.log(user); // Not appearing in console
}
Sign in
firebase.auth().signInWithEmailAndPassword(email, password)
.then(function (user) {
console.log("signInWithEmailAndPassword success"); // This appears in the console
})
.catch(function(error) {
console.log("signInWithEmailAndPassword", error);
self.signInError = error.message;
$timeout(function () {
$scope.$apply();
});
});
Edit - these are the steps to reproduce the problem (the typical action of a user of my app):
User downloads and launches app
User registers with email and password
App sends email verification email
User receives verification email and clicks on link
User goes to sign in page of app
User signs in triggering the console.log("signInWithEmailAndPassword success");
onAuthStateChanged callback is not called
For development and testing purposes (not what a user would do but I have done)
User reloads the app
User is now in the app
User signs out of the app
User signs in to the app triggering the console.log("signInWithEmailAndPassword success");
onAuthStateChanged callback is called
The problem is that auth().createUserWithEmailAndPassword() does in fact log in a type of user in.
You will find that if you type
firebase.auth().onAuthStateChanged(user => {
console.log("onAuthStateChanged()");
that createUserWithEmailAndPassword() does trigger the console log. However, there seems to be no valid "user" object, which would explain why nothing appears for you since you are only logging the user.
I ran into the exact same problems. At the sendEmailverification() step notice how it does require you to use auth().currentUser, signalling there must be some sort of user signed in (I am not sure how firebase handles the difference between email verified users and non-verified users behind the scenes)
You can simply called the signOut() function after sending the email verification and it should allow the onAuthStateChanged() function to call when logging in for the first time (without reloading the app)
firebase.auth().currentUser.sendEmailVerification()
.then(() => {
console.log("Email verification sent");
firebase.auth().signOut().then(() => {
console.log("Signed out!");
}).catch((err) => {
console.log("Error signing out!", err);
});
It is rather confusing that you can actually "Log in" successfully without causing a change in AuthStateChanged or returning any errors.
TLDR: Remember to use the auth().signOut() function after sending the email verification.
Try this way, i hope it'll work
firebase.auth().onAuthStateChanged(function(user){
console.log(user);
})

Categories

Resources