Forget password

Apr 29, 2017   #Authentication  #Signin  #Passwordless 

Can we have a signin/signup flow that is email-based and passwordless similar to a “forgot password” flow but where the URL will work only for the initiator, and only once per signin? This is the scheme I’ve implemented on Patantara and I describe its innards here.

Context

Last December, Mozilla retired their Persona authentication service which provided both developer convenience as well as a familiar email-based signin for users. I’d implemented Persona on Patantara for users who want to create their own documents and share them. The regular Google/Facebook/Twitter/whatever OAuth-based authentication was a replacement option. However I don’t like to use any such service as an intermediary, at least not directly and in a way that advantages one of them with my users’ signin metadata. I also happen to dislike passwords and use a password manager to enter random passwords for all services I use. It was an opportunity to try something different.

I wanted to keep the same kind of signin mode as Persona, where a user keys in their email address to identify themselves. No special “sign up” flow where they’re forced to choose a “user name” and “password”, with all the craziness that goes with it. I wanted it to be simpler both from development perspective as well as from a user’s perspective.

So I implemented an email-based passwordless scheme, which I describe here. I’ve described the flow on the Patantara blog and here I present the inside technical details for devs. Please checkout the flow there before diving in here. It’s pretty straightforward.

Passwordless

Most services which provide a “username/password” scheme also provide a “Forgot password” flow. You can give your email address and you’ll receive a unique one-time token URL in your email box which you can use to reset your password. They also give you a “Remember me” option that is usually left unchecked by default when you signin.

What if we only had the “forgot password” flow, but without the “password” bit?

This concept is nothing new. Medium provides this option for their users and we also have Passwordless, an open source library for NodeJS which provides token based authentication via email validation. In essence, this piggybacks on the security provided by the user’s email provider, whoever that may be.

With such services, the token received via email needs to be a one-time token. This forces Passwordless to use a DB backend to enforce the token’s usage contract. So every signin attempt, successful or not or malicious, must touch the DB backend even if it is an ephemeral one.

I use Redis as the backing DB for the whole of Patantara since the amount of data I need to deal with is low enough to fit in RAM (< 200MB) and Redis’ aof file and 1 sec persistence guarantee suffices for my site’s low traffic due to its niche domain. Due to this unique situation, I had an incentive to try not to touch the DB backend for the one-time use contract of tokens, as I didn’t want to pollute the saved AOF file with useless data about signin tokens that weren’t used, especially by folks up to some mischief.1

So here’s the scheme I used, in enough detail for replication. Comments about security aspects are most welcome.

Mechanism

Note: “Signin” and “Signup” are the same flow since email validation happens in both cases anyway.

  1. When a user provides their email address and clicks on “Signin”, the email address is sent over to the server side.

  2. The server generates a crypto-random token string, puts together signin context info into an auth structure – email, expiry, redirect URL – though you can add anything else here too.

  3. The server then signs this structure using a key generated by a HMAC of $token.$secret.

       // pseudo-code
       var key = hmac(secret, token + '.' + secret);
       var cookie = JWT.sign({email: email, exp: expiry_date, redirectTo: url}, key);
    
  4. The signed structure is sent over to the browser in the response screen to be stored as a secure cookie.

  5. The server then sends a signin URL to the user’s email address. The URL simply mentions the random token.

       var signinURL = 'https://stage.patantara.com/auth/enter?token=SOME_RANDOM_HEX_DIGITS';
    
  6. When the user clicks on the URL and is taken to the browser session in which they initiated the signin, the auth cookie is sent securely, with the token accompanying it as a GET parameter.

  7. The server then checks the validity of the auth cookie by constructing the verification key using the supplied token and checking the JWT token’s signature.

       // pseudo-code
       var key = hmac(secret, token + '.' + secret);
       var success = JWT.verify(auth_cookie, key);
    
  8. If the signature didn’t validate, the authentication is rejected. Note that in the case of rejection, the DB has not been touched.

  9. If the signature validated, then a new server signed assertion token with full information about the authentication is generated and stored as a cookie, replacing the previous auth cookie. A new user is created on the DB if necessary and successful login info is stored.

       var assertion = JWT.sign({email: email, exp: expiry, redirectTo: url}, server_secret)
    
  10. Permissions for the user are granted for various parts of the site based on the successful verification of the assertion token sent with every request.

  11. A signout just trashes the assertion cookie.

Highlights

  1. Replacing the auth cookie with the assertion (step 9) essentially destroys all knowledge of the token in the auth cookie. This makes the token a single-use token. There is a tiny window in which the signin URL can be “reused”, but that ends up being an idempotent operation.

  2. The signin must be completed on the same client so that the auth cookie and the token can be sent together to the server for validation (step 6). This prevents the possibility of someone initiating a signin for a random user and the user accidentally signing into patantara by clicking the URL, when they didn’t intend to. While this is not a major issue for Patantara, it could be important for other sites.

  3. If the email happens to be intercepted by a hacker, the signin URL would be useless to the hacker because it needs to be combined with the auth cookie in order to validate .. and the auth cookie only resides in the initiator’s browser. So unlike current “forgot password” schemes, you can forward the signin URL to anyone you want and be assured that even if you hadn’t used the link, they will not be able to use it to signin as you!

  4. An invalid signin doesn’t touch the DB at all. This means the DB cannot be hogged by lots of signins/signouts happening. For valid signins and authenticated accesses, the DB (in my case) needs to be accessed for permissions. This is, of course, fine.

  5. As long as you (the user) have a default browser you use on your system, the “same client” constraint will be invisible to you. Only when the initiating client is not your default browser do you need to copy the URL manually into the client for signin.

Feedback welcome

I’ve not seen this kind of a scheme described anywhere else, but I didn’t search much beyond passwordless.net. So, dear readers, if you know of this scheme already implemented elsewhere, please do let me know in the comments. I also welcome any questions and criticisms about the usability and security of this scheme.


  1. I was surprised by how many hack attacks my small niche site gets from China, Russia, etc.

    [return]