Firebase: Email verification link always expired even though verification works - javascript

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.

Related

How to track visits or clicks on an Url shortener using ExpressJS and NodeJS?

I'm working on a URL shortener for learning purposes, and I want to add a way to track the clicks or visits for each shortened URL.
An example of a shortened URL from my app is this: http://localhost:3000/384oxt where the code 384oxt is saved in my database and is related to the URL: https://www.google.com/.
So, when the user visits: http://localhost:3000/384oxt, I have this method to do the redirect:
const redirect = async (req, res) => {
const { code } = req.params;
if (!code || !(await ShortenedUrl.existsUrlCode(code))) {
return res.status(404).json({
status: 'err',
errors: [
{
message: "The urlCode doesn't exist",
},
],
});
}
try {
const shortenedUrl = await ShortenedUrl.findOne({ urlCode: code }).exec();
console.log(req);
return res.redirect(301, shortenedUrl.source);
} catch (err) {
console.error(err);
return res.status(500).json({
status: 'err',
errors: [{ message: 'A problem has occurred, please try again' }],
});
}
};
As you can see, I get the code, then I check if the code exists in the database, if not, I return a response with the message error, but if the code exists, I get from the database the URL that is linked to that code and I do the redirect.
The first time, it works OK, and this instruction: console.log(req); prints the request on the console.
But if I use the same shortened URL again: http://localhost:3000/384oxt, it seems like the browser is doing the redirect without entering the redirect method in my NodeJS app. The instruction console.log(req); is not printed anymore. Even if I delete the method, the redirect still works.
I want to store some statistics like the browser, time, etc when someone uses the shortened URL, but with this behavior, I can't.
How can I force that every time the shortener URL is used the method is executed?

Any suggestions on how to implement challenge (2 factor authentication, etc...) in authentication

I am trying to incorporate 2FA in the react admin login flow.
The issue is that the standard way to validate a login is to use useLogin.
const login = useLogin();
try {
await login({username: "joeblack", password: "mybadpassword"}, "/redirectlocation");
} catch (err) {
// display an error notice or whatever
}
Basically, the login function from useLogin will either complete the login process and log the user in or show an error.
Second Authentication Step
For things like 2FA, new password required, etc..., there needs to be an in between step where the user isn't authenticated yet to view resources, but is not in an error state.
So for instance, perhaps login would return a challenge with the type of challenge.
Technically this can be done by returning that info in the authProvider login function and then making decisions based on that.
const loginResult = login({username: "joeblack", password: "mybadpassword"});
// loginResult returns { challenge: "2FA" }
if (challenge) {
// redirect to challenge page
} else {
// redirect to dashboard or wherever
}
The issue is that even if we handle it after the login function, once that login function runs, technically the user is authenticated. So they could just bypass the challenge and directly input the resource they want and they would be able to view it.
The login function of the authProvider only has 2 results, a resolved promise or rejected promise.
Before I go further to figure out how to make this work I wanted to see if anyone else has looked into this issue.
This is basically a workaround, but I think it's the best option for now (although I'd love to be proven wrong).
The idea is that even though Amplify's auth comes back with a resolved promise containing the Cognito user, we check if there is a challenge property. If there is, then we reject the login promise so react-admin doesn't log the user in.
Then we return an error message in the rejected promise that can be read by whatever is calling the login function. If it's a challenge then the login interface can handle it appropriately presenting a 2FA screen to confirm.
Had to also include the Cognito user in the our case because we need that in order to confirm the 2FA.
Here is the authProvider login function
login: async ({ username, password }) => {
try {
const cognitoUser = await Auth.signIn(username, password);
if (cognitoUser.hasOwnProperty("challengeName")) {
return Promise.reject({
message: cognitoUser.challengeName,
cognitoUser: cognitoUser,
});
} else {
return { ...createUser(cognitoUser) };
}
} catch (err) {
return Promise.reject({ message: err.code });
}
}
Hopefully that helps someone. Let me know if you have a better solution.

Can't login with Meteor function loginWithPassword, Unrecognized options for login request [400] error

I'm working on React + Meteor application and can't login using accounts-password package via loginWithPassword function.
The official API says that Unrecognized options for login request [400] error pops up when your user or password is undefined (or, i guess, just do not match the API), but i've checked the arguments and everything seems correct. username and password are strings. Meteor has ability to operate with user object, but this is not working too.
Here's the sample of my code.
const submit = useCallback(
(values) => {
const { email, password } = values;
const callback = (err) => {
if (err) {
console.log(err);
notifyError();
return;
}
login();
history.replace(from);
};
Meteor.loginWithPassword(email, password, callback);
},
[from, history, login, notifyError]
);
Any help appreciated.
It looks like that particular error can occur when there are no login handlers registered.
Have you added the accounts-password package ?
meteor add accounts-password

Firebase Email Link Authentication

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

Meteor's createUser running on client and server

I'm fairly new to Meteor and trying to grasp its concepts. I have a client code below that triggers Meteor method to create new user:
Template["signup-team"].onRendered(function(){
var validator = $('.signup-team-form').validate({
submitHandler: function(event){
var email = $('[name=email]').val();
var password = $('[name=password]').val();
Meteor.call('addNewUser', email, password, "team-captain", function(error, result) {
if (error){
return alert(error.reason);
}
Router.go("complete-signup");
});
}
});
});
The method is defined to run on both client and server. When run on the server I want it to create user and add role to account. On the client side I want to sign user in.
Meteor.methods({
addNewUser: function(email, password, role) {
check(email, String);
check(password, String);
if(Meteor.isClient){
Accounts.createUser({
email: email,
password: password,
profile: {
completed: false
}
}, function(error){
if(error){
console.log(error); // Output error if registration fails
} else {
console.log(Meteor.userId());
}
});
} else {
var id = Accounts.createUser({
email: email,
password: password,
profile: {
completed: false
}
});
console.log(id);
Roles.addUsersToRoles(id, role);
}
}
});
The server part runs fine and new user is created but on client side I get error Error: No result from call to createUser and user isn't signed in automatically.
I assume the problem is I dont need to run createUser on the client and use Meteor.loginWithPassword instead but I would really like to know the theory behind this. Thanks
Don't do this. You are rewriting core code and creating security issues needlessly.
Instead of using your addNewUser method, just call Accounts.createUser on the client. Have a onCreateUser callback handle adding the role.
In your code, you are sending the users password to the server in plaintext. When you call Accounts.createUser, the password is hashed before being sent to the server. It also takes care of logging in the new user for you.
One gotcha with adding the role though, you will not be able to use Roles.addUsersToRoles(id, role) in the onCreateUser callback, as the user object has not yet been added to the database, and does not have an _id. However you can directly add the role to the proposed user object like this:
Accounts.onCreateUser(function(options, user) {
user.roles = ['team-captain']
return user;
})
Then again, maybe you don't want all users to be team captains!

Categories

Resources