Salted passwordless authentication
In this post, I want to describe the strategy I've used in the last implementation of one of my side projects. The side project is not important at all, it's mostly a TODO/GTD task manager, but I use it as a test bed for anything I'm interested in learning or trying out. The source code is here and it is deployed here if you want to try this out.
I'm not a security expert, I'm just a web developer and probably not even a good one, I want to describe this with the intention that somebody that understands more about this could point out issues and help me make the approach more secure and more easy to use.
The problem with passwords
Personally I don't like passwords, because a) I don't want to use the same password everywhere, 2) Using different passwords everywhere is impossible unless you 2a) use a password manager and/or 2b) use a schema based on the website or something else that lets you generate the passwords quickly and consistently for a any given website. And that's just what I don't like as a user.
As a developer I don't like passwords because its very difficult and risky to implement all the logic for generating them (salts, algorithms, lengths, etc), storing them and implementing registration forms, login forms, password reset forms, etc. So unless you use an existing solution from a CMS or big framework (Django, Rails, Strapi, etc.. ) where you have a "Golden Path" for this, you're in a pretty bad situation.
Existing solutions research
I was looking for a passwordless way to authenticate users without depending on any third party oauth provider. While researching about this, I've found that it seems the most common approach to this is:
- Ask the user for email address.
- Generate from the server a link with a token and send it by email to the user.
- When the user clicks on the link, sign the user in, either in the newly opened tab/browser or in the original one, or both.
This approach is very simple, and I think there are many sites using it. The first time I came across a login system like this was when I signed up to try out Zeit's PaaS platform here, now called Vercel, hopefully will still have this name when you read this.
When I was investigating more about this approach, the first question to came to my mind was wether I should log-in the user in the newly opened tab after the user has clicked in the email link (thus what do I do in his previous tab?) or wether again to follow Zeit's approach and in this new tab tell the user "Awesome, you're now logged in, please close this tab and go back to the other one". The first approach has an important drawback which is that the link might have opened on a completely different browser, even on a completely different device if the user clicked the link on his phone. The second approach I think is even worse because anyone could go to the website, enter somebody else's email and if that person happens to accidentally click this email then somebody somewhere might get logged in as him.
There might be workarounds such as comparing IP addresses, browser fingerprinting, etc, etc... but it gets risky and complex in my opinion. Also, the user might not have even clicked in the link but his antivirus might have made a request to the link, or anything in between him and his email client. Not great.
The salted passwordless approach
So I kept investigating and trying out things and finally came to the following process I want to describe here with the hope that somebody else will help me understand more about its drawbacks and why it might not be a good idea, or if seems to be a good one then help me to improve it.
Also, I'm not sure if this already exists under some well known industry name and I've just missed that out. Thus I'm basically reinventing the square wheel here.
So, the process goes like this:
- The application prompts the user for an email address.
- The application saves in the browser's localStorage (or just in memory) the email address, and randomly generated salt.
- The application contacts the backend, submitting the email address and the generated salt.
- The application shows the user a dialog of the kind "We've just sent you an access code to your email, enter it below".
- The server generates an access code and then saves in the database the tuple consisting of (time, email, salt, code), this is the "authentication request".
- The server sends an email to the user, containing just the code, which would be something like "09124".
- The user enters this code in the screen resulting from step 4.
- The application submits to the server the tuple (code, email, salt).
- If the code, the salt, the email and the expiration (time since request was created) are valid, then create a session, otherwise fail the login.
What problems does this process solve?
- No need for a password, which was the first requirement.
- No need to implement password recovery processes, registration processes, etc.
- No need to permanently store any sensitive information about the user. You might not even need to store the email address (you could use a hash of it as the user ID).
- If the email with the access code is intercepted, there is no risk for the user: whoever intercepted the email would also need access to the user's browser localStorage (or memory) to get the salt, otherwise the server won't validate that access code and thus wont create a session.
- We don't have the problem of the user opening the "magic link" in a different browser, given there is no magic link at all.
What drawbacks does this process have?
- The user will not be able to log in if he is having any kind of problem with his email, or his email provider is slow to delivery new messages, etc.
So far I'm pretty excited about this solution and that's probably bad for me, because I might be missing something out, and that's the main motivation of writing this down here. Unless there is some big security issue I'm not aware of, I think the trade-off/drawbacks of the approach are quite acceptable for the usability and implementation benefits.