Firebase: Securing records by email address when users can use fake addresses - firebase

I'm making a web app where you pay to use it first and then sign in. The sign up workflow is this:
Once a payment is made on my website, the client receives an order ID that is associated with their email address. The client sends this to a Firebase Function endpoint to activate their order.
The Firebase Function checks the order ID is valid and creates a Firebase user with a long random password (to prevent anyone from signing in), then sends a password reset email. A "payment" record is created in Firestore and associated with the user ID.
The user follows the password reset email then logs into my app. The paid features are activated if Firestore contains a payment associated with that user ID.
This all seems fine except:
A malicious user Bob could outside of my control creates a user for email "x#example.com" that he doesn't own and sign in using the Firebase client SDK.
Alice then makes a payment for her email "x#example.com".
Bob will still be signed in and can now use the features Alice paid for.
How do I prevent this?
One idea I had was the above Firebase Function could check if user "x#example.com" exists with an unverified email address and if so it would 1) delete that user and 2) create a user for that email address again (creating a new unique user ID). Bob would then have a different user ID than the one associated with the email address so couldn't access the payment record. However, this breaks if Alice makes two payments without validating her email after the first payment. What's a robust way of solving this?

Consider verifying the email before processing the transaction.
One option is to use sign in with email link. This will sign in the user and verify their email. You can also set a password after if you want. If an existing unverified provider is linked to the account, it will be unlinked and any existing session that a malicious user had previously set up will be revoked.

Related

Set password and verify email in one step

Lots of questions about email verification here on SO, but none seem to cover my scenario.
We would like to add users ourselves after an intake meeting. Our representative has a form to enter some details like company name, VAT number, contact data (which contains an email field), ... This data is saved in Firestore.
After this, an email is sent to the supplied email address which contains a link that takes the user to a form where his/her email address is displayed with a password and a password confirmation input field. When submitting this field, the user is created.
But now the user receives an email asking to confirm their email address. I assume, for security and privacy reasons, there's no way I can set the user's email address as verified.
I've looked at customizing the verification email, but that doesn't seem to solve my problem.
Creating the user with a random password after the intake meeting also doesn't seem to be a solution, as the user still has to verify and then reset the password in 2 steps. Or can I somehow redirect after the email verification to the 'set password' page? That would be an acceptable solution.
Is there any way to achieve the desired flow described above?
As a general workflow, you could achieve this using a Cloud Function along with either database system. You can also make use of App Check to further secure this process.
Representative adds base user information in their portal. Store the data securely in the database of your choice.
Send the user an invite email containing a short-lived verification token linked with the email added by the representative (this could be generated and fired off using an onCreate Cloud Function once the invitee's data is added to the database). This token should follow some standard like JWT so you can deserialize the contained email address or be exchangeable for the underlying email address.
When user clicks/copies the link to their browser, present them with an input form asking for the desired email and password. Note: the email field should be editable! The rep may have used an email the new user doesn't want to use with your platform.
If the token is still valid and not consumed, continue with the next steps.
If the token has expired and not consumed, send another email to reconfirm their email and restart this step.
If the token is already consumed, show an error and don't continue.
Submit the email, password and emailed token to your backend via a Callable Cloud Function.
Sign the user in using the authentication token returned by the function on success. Show an error otherwise.
In the callable function for creating the user:
Confirm the request comes from your app (if using App Check)
Confirm the validity of the emailed token
Pull the data the representative entered from the database linked with the emailed token's original email address.
Using that data, the updated email, the new password, and emailVerified=true, call the createUser API.
Using the User ID from the returned UserRecord, create the user's profile data in the database and also create a Custom Authentication Token.
Once their data has been created and the token generated, return the authentication token as the result of the request.

Firebase linking anon UID to email verification if verified on a different device

Has anyone managed to associate an authorised email to an anon UID that was created earlier on another device?
Here is the workflow:
User comes to the site for the first time, anon UID is created, they enter email as part of signup, which is added to their details in the db
An email is sent to them to verify the email.
However, user misses it for whatever reason.
Instead, they come again with a different device and try to log in using a password-less login by entering their email and receiving a link
they get the link and click on it. The email gets verified, but gets assigned to a NEW UID created on the 2nd visit.
The question is how can the email be linked to the 1st UID instead of the 2nd one on a new device?
Under 2, before the email is sent, I am calling firebase.auth().currentUser.updateEmail(email), but it doesn't seem to associate the email with the UID in the firebase for some reason. I can see that in the Firebase Authentication tab -- the UID is still showing as Identifier (anonymous). This is probably the root cause of the issue as otherwise the email would be there, just unauthenticated.
Any ideas would be welcome!
Setting an email address to an anonymous account doesn't change its provider to suddenly become email+password. It remains an anonymous account, it now just also has an email address associated with it. To associate the user with an email+password account, you'll have to sign them in with email+password credentials and then link those credentials to the existing anonymous account.
To link accounts/providers, you must create credentials for both providers on the same device and call the relevant API to link them.
Since you only have a UID from anonymous sign-in on the first device, that UID can't be recreated on the second device. And that means there is no way to link the email-link UID account on the second device to the anonymous UID on the first device.

Firebase - create a temporary user until the user signs up

I have a use case where User A can say that User B borrowed from User A some amount of money, similar to apps like Splitwise.
I'm using firestore to store the data. In this particular case, I'll store it as a document in the "Transactions" collection which will have the following fields:
amount: 20
fromUser: uid for User A
toUser: uid for User B
The issue here is that the User B doesn't exist yet and so there is no uid for the user B. What I want to do is to create a temporary user for User B with the email address which will generate a uid. And later when the User B signs up on the app, the same user is upgraded to a permanent user with whatever auth provider the User B has used.
While searching, I came across - https://www.freecodecamp.org/news/heres-what-i-wish-i-knew-before-i-started-using-firebase-9110d393e193/
Which mentions that this was possible with the firebase invites which is now depreciated. So, is there any other way to achieve this behavior now?
Firebase supports creating anonymous user accounts for just such scenarios.
Authenticate with Firebase Anonymously
You can use Firebase Authentication to create and use temporary anonymous accounts to authenticate with Firebase. These temporary anonymous accounts can be used to allow users who haven't yet signed up to your app to work with data protected by security rules. If an anonymous user decides to sign up to your app, you can link their sign-in credentials to the anonymous account so that they can continue to work with their protected data in future sessions.
Email Address of 2nd User Known
If you already have the email address for a user (User B) that has not yet signed up, then you can create their account using the Firebase Admin SDK.
See Create a user
The new User B email address could then be configured for Email Link Authentication by calling firebase.auth().sendSignInLinkToEmail(email, actionCodeSettings).
Since the User B account creation was initiated by User A, you would not be able to use Local Storage to save the email of User B to complete sign-in with the email link. However, this is not a problem, since as the documentation example shows, you may prompt the user for their email address.
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.
email = window.prompt('Please provide your email for confirmation');
// The client SDK will parse the code from the link for you.
firebase.auth().signInWithEmailLink(email, window.location.href)
...
Now that User B has successfully signed in using the email link, the standard process may be followed to Link Multiple Auth Providers.
The server side (aka Node.js) Admin SDK allows you to create users programatically. You would need some way to pin/stick the newly created user UID to userB. Email address would seem the easiest way. So UserA would need to specify the email address of userB, then you process that server side Admin SDK.
When user B signs in with their email address, Firebase Authentication will detect an existing account with userB's email address and throw an error which you can use to merge userB's account data.

Firebase authentication: How to deal with users using email addresses they don't own?

Scenario:
Bob creates a Firebase account with email address alice#example.com which he doesn't own (either by accident or maliciously).
Bob creates several private Firestore records (e.g. user comments/messages) that are linked to his user ID that only Bob should be able to read and write to.
Alice tries to sign up and finds her email alice#example.com is already taken. How do we give Alice a user account linked to her email address without her having access to Bob's Firestore records and without Bob having access to her records she will go on to create?
Some ideas:
You can send Alice the password reset email which will log out Bob but then Alice is going to see the records that Bob created when she logs in.
You could use a Cloud function that 1) either deletes Bob's (email unverified) user or changes its email address 2) creates a new user for Alice with the email address verified flag set and send the password reset email.
Is there an easier way?
You could use one (or multiple!) of Firebase's auth mechanisms to help determine ownership.
You could use third-party auth (FB, Google, Twitter, Github) that would help clarify ownership of an e-mail address. (easiest, imo)
You can verify a user's email address via firebase.auth().currentUser.sendEmailVerification() https://firebase.google.com/docs/auth/web/passing-state-in-email-actions
You could use an email link for signups: https://firebase.google.com/docs/auth/web/email-link-auth
From their docs:
There are numerous benefits to signing in by email:
...
The ability to authenticate a user while also verifying that the user is the legitimate owner of an email address.

How to login a new user by default after mail Invite in Meteor?

I have a requirement where I need to log in a new user to a Meteor application by default, and take the user to the reset password route after that.
I looked into this documentation, but I don't think it would apply here, because the password is not known. Here's my use case in detail :-
A user invites another new user by providing the invited user's email ID. The application sends email invite to the user, asking them to be taken to a route which normally requires logging into the application.
The new user who receives the email invite, is not yet signed up with the application. So, they need to pick a password to log in to the invited area of the application.
Currently, I am using the meteor-useraccounts:core package; to trigger the resetPasswd route for FlowRouter. Using the onSubmitHook, one can redirect the user to the proper location after successful reset of the password.
However, I am not able to figure out how to first log in the user automatically. Because if the user is not logged in, the reset password page won't open. Instead, it would show some error, indicating unauthorized action.
How could a link be sent via mail to the invited user, which would automatically log in the user?
I looked into this documentation, but I don't think it would apply
here, because the password is not known.
That is right, and because of that, in order to achieve that you have to create an account for that e-mail at the time the user enters a link sent by e-mail. Or create the account at time of invitation.
So possible solutions:
1) create an account for an e-mail provided in query (http://app/login?email=blabla#google.com) for a route, random password, reset password e-mail sent immediately after the login. (create login automatically login the user). That would also required some security key, so that no-one could create lots of accounts on different e-mail addresses.
2) create account at time of invitation, take random password and send it as a query parameter of the link sent to the invited user. When user enters the site, you take that password (and e-mail) and login him with that, and immediately send reset password for that e-mail

Resources