Edit: Some friends asked me for a Spanish version, so if you feel more confortable reading it in spanish, here is where you can find it: Autenticación de doble factor con Firebase Cloud Functions
This is my first (of many, I hope) post here, and ever. And I'll try to show all the iterations and learnings from each of them.
The requirement
Our client required an email and password login and then depending on the role, it requires the user to type the missing numbers of a static Pincode. Then if the user/password/Pincode matched, the application takes the user to the dashboard.
The blank spaces are random, so it should request a different set of 3 places each time
First implementation
In the beginning, it was a 2 step mat-stepper where in the first step it asks for the email and password and the second step (required for the roles that have pincodes) where it asks for the Pincode.
Once we get the email and password from the form, we would then log the user in and if the user requires a Pincode we would send them to the Pincode screen. The user types its Pincode and we would compare them with what we have in Firestore. If it matches we would let the user go to the authorized part of the app, if not, the user will be logged out.
The problem with this approach
To get the Pincode from Firestore the user must be logged in, which defeats the purpose of the Pincode.
A contractor made a pentest on the app, and one of the vulnerabilities they've found was that the user was able to change the URL to the authorized section and bypass the Pincode part of the process.
Yeap, that happened
Second implementation
This needed fixing, so we decided to lean on Firebase Cloud Functions to be our gatekeeper.
We take the email and password from the login form, and then send them to the cloud function. There it checks for the credentials, if the user requires a Pincode, it returns the positions needed for the second step along with a token that identifies that attempt (The same user can have multiple attempts with differents positions requested by the cloud function), if not, it returns a custom token that will be used to authenticate the user in the frontend.
When the user requires a Pincode and has the positions requested by the cloud function, it types the numbers and sends the values (not the positions), the token, email, and password to be all checked on the cloud function. In the Cloud Function, we check the credentials, get the positions based on the token provided, and validate the user's values with the stored Pincode. If everything matches, a custom token is returned to authenticate the user on the frontend, else, the cloud function sends a generic error message to let the user know that should try again.
What can be improved
As in every project, we do what we can with the time we have. It means that some improvements that could be made, weren't.
Static Pincode
At the time of writing this post, the Pincode is static, which means that it is stored in the database as a single value that doesn't change. In my opinion, it should be dynamic, a code that changes over time, to avoid someone that finds out a Pincode to use it anytime.
Hash Pincode and Custom Claims
The Pincode should be stored as a hash instead of plain text since it's very sensitive data. If we decide to use a one-way hash, we could also store the hashed Pincode as a Custom Claim property in the user's account. That would avoid a call to the server and speed up the Pincode checking process.
Simplifing the login process
Regarding logging in in two steps, I think they could be merged on a single step that requests email and password, but when the user finishes typing their email (blur the input) triggers a call to a cloud function that checks if that email belongs to a user that requires a Pincode if it does the Pincode input could be shown below the password input.
Get the authentication process from the Pros
Firebase offers a quite robust solution regarding Authentication, however, when there are special needs, it's a better idea to rely on a third-party solution than build it on our own.
Firebase offers the possibility to integrate a third-party solution quite seamlessly ( docs ) Actually, we use this to integrate our Pincode solution but it would be better if we've used it to integrate another solution like the one provided by Auth0, Twilio (Authy) , or Okta
Well, this is it. My take on this problem and how we've solved it. I hope you can take anything useful from our experience and I'd like to know your thoughts about this.