DEV Community

Cover image for Implementing Passwordless Authentication in Node.JS

Implementing Passwordless Authentication in Node.JS

Milecia on April 20, 2021

Broken authentication is the second-highest security risk for web applications. This usually means that session management and authentication aren'...
Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️ • Edited
  • A user submits their email address or phone number in the web app.
  • They are sent a magic link to log in with.
  • The user clicks the magic link and they are redirected to the app, already logged in.

Sounds good on paper, but gets infuriatingly impractical very quickly:

  • I want to log in at some random computer
  • I enter my email
  • I get an email on my phone
  • I click the link
  • I am now logged in on my phone
  • I sigh

Now I have to enter a password anyway, and deal with whatever added security my email account has. What's worse, I now have to input my email password on some random computer (compromised until proven otherwise), instead of a password for some random application I don't even care about. This isn't an improvement in any way.

A better workflow would be:

  • A user submits their email address or phone number
  • They are sent a magic link
  • When they open said link, on any device, their login clears on whatever device they initiated it
Collapse
 
bugs_bunny profile image
Derek Oware • Edited

I realised this the first time I did this myself. So I send both the magic link and an OTP
So the user can enter the Pincode if he/she is in such a situation
OR
The link can just verify the login attempt. After the attempt has been verified then you log the user in on the device he/she initiated the login attempt. You can use sockets to make this happen

Collapse
 
bbarbour profile image
Brian Barbour

How do you go about implementing that last part? Where clicking the magic link on one device logs you in on another?

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

Not without some degree of back-end persistence, which I assume is the main reason it's not how what the article does. You'd need to create some sort of short-lived state in the back-end that gets cleared by opening a link in the email. The login-window could then just do polling, or use some more sophisticated method for waiting for the server to grant it access.

Thread Thread
 
tysonrm profile image
tyson • Edited

The only state is in the browser. On the server you just need to verify the token, which could be sent by the browser in the authorization header as a bearer token or in the body or in the url as a parameter or search term. (Sending in URL is an unforced error. It exposes the token, so dont do this in practice.) You should also sign with RSA keys vs secret, so you can verify the origin.

Thread Thread
 
tysonrm profile image
tyson

Regarding exposing the token. The magic url, which i hope there's a better term for, can point to a url on the server that returns html where the token and any user data are passed via session storage. It doesnt matter about the nonce. Sincei its now dead.

Collapse
 
idrisrampurawala profile image
Idris Rampurawala

Nice article! Leaving about whether this method is safe or not, as already in discussion by our dev members, I want to highlight one thing here.

In /login API (snippet below), if an email is not found, then we should not tell the user that the email is not in our system. This is a security risk by which a hacker can identify valid emails in a system.

// Login endpoint
app.post("/login", (req, res) => {
  const email = req.body.email;

  if (!email) {
    res.statusCode(403);
    res.send({
      message: "There is no email address that matches this.",
    });
  }

  if (email) {
    res.statusCode(200);
    res.send(email);
  }
});
Enter fullscreen mode Exit fullscreen mode

Hence, /login API can just respond with something generic like, You will receive an email with the link if an account is found associated with this email. 😁

Collapse
 
andreasvirkus profile image
AJ

Excellent point to bring up! Same goes for signups. You should always say "You'll receive an email" and for existing emails, simply state that "Someone tried to sign up with us. If that was you - Log in here instead"

That way a malicious user/attacker can't enumerate the existing emails at a large scale

Collapse
 
karlkras profile image
Karl Krasnowsky

Yeah, I know this is the preferred "safe" response, but can be frustrating when you're "sure" that was the email (I have many) and don't get a response.
I would prefer an email be sent anyway with a message telling me that the email provided wasn't registered so at least I know the process is working.
I don't see a security problem with this approach? Though it may result in an increase of bounced emails from fat fingered entries.

Collapse
 
luis__0c10db7013 profile image
Luis

The line 'const email = req.body.email' is related to what the user types in the input element.
The if statement only checks if the value of email is truthy or not. So, only checks if the user typed something on the input element.

Collapse
 
nanasv profile image
Nana aka y_chris

if I was a hacker, I should know from the message that my details is not, hence no email with some link.
so to me it's still the same.

Collapse
 
wparad profile image
Warren Parad

I was so hoping this would talk about how to easily integrate WebAuthN, but instead unfortunately, it's sharing more about the bad suggestion of legacy passwordless.

SMS is unsafe, email is vulnerable and provides bad UX. There's way more problems than this, but they've been listed many times before

Collapse
 
wonsil profile image
Mark Wonsil

It is no longer a best practice to force a password change after a period of time. This according to NIST, Microsoft, and the man who suggested the idea in the first place.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
wonsil profile image
Mark Wonsil

See Best practices for existing authentication systems, the paragraph before the conclusion.

Collapse
 
aubs profile image
Aubrey

The described solution actually introduces attack vectors; what if someone (else) has access to email / phone? They will be able to login.

Someone said "There is a barrier to access a mailbox.".. there is not! If I already have your mailbox then I can get the app "access" if an email is sent for the access... This is a main reason for 2fa and such. (the same goes for phone number).

The only real solution for passwordless (sic) authentication is either an app or a (usb)key. See for example the auth0 implementation for details. (or the MS Authenticator).

The idea of implementing an own authentication layer is outdated anyway.
Why? You do not want a crappy/outdated implementation, that you need to keep updated and secure, to expose your credential data.

Collapse
 
andreasvirkus profile image
AJ

Whilst the idea of implementing your own authentication may seem outdated to you, it's
1) always good to understand how the services/libraries you use work behind the scenes
2) important to understand that often you'll require custom solutions and services like Auth0 are very rigid in certain regards

Collapse
 
bigbott profile image
Yuriy Novikov

Dangerous.
The attacker steals the mailbox and gets access to all applications/websites.

I think the best way is a custom stateless JWT that contains encrypted userID and timestamp and included in the request as both Cookie and a part of the request body (JSON). The server then compares JWT from Cookie and JSON and if they match -- keeps the user logged in and retrieves needed info from DB with userID.

Collapse
 
genu profile image
Eugen Istoc

Just a fyi, you're not actually creating the JWT with an expiration date. You're simply encoding the expiration in the payload.

You actually shouldn't be putting any sensitive data in the payload as that data can be publicly seen.

Collapse
 
arswaw profile image
Arswaw

A fine article, for a topic that is only becoming more popular.

Collapse
 
cristhos profile image
Br CRISTAL DIBWE

This concept is true for the phone number, but I don't think so with email.

There is a barrier to access a mailbox.

Collapse
 
seanolad profile image
Sean

Nice post, very helpful as a refresher. 😄

Collapse
 
nanasv profile image
Nana aka y_chris

hello Melicia, I have gone through your work and it's awesome.
Just that I could not see where user email was verify from the data base, before login in automatic.

Collapse
 
crazyoptimist profile image
crazyoptimist • Edited

@darkwiiplayer You could check this:
magic.link/
But it's too costly I think :)

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

I mean, if you're going to pay for anything, the extra cost of having a small database isn't really a big deal. It's not like it's hard to build such a login system on its own if you accept that you'll have to persist some data, which is what the internet has mostly been trying to get away from.

Collapse
 
carcruz profile image
Carlos Cruz

Great article 👍

Collapse
 
mardeg profile image
Mardeg

I thought this would be a tutorial on the Node.js implementation of SQRL.
Too bad.

Collapse
 
juancho11gm profile image
Juan Sebastián • Edited

Nice job. I have a question, how are you checking the token is no longer valid after used?

Collapse
 
denizbinay profile image
Deniz Binay

Is there a good passwordless implementation? Gladly open source. Auth0-Passwordless is a pay-service and the passwordless strategy from password js has not been updated for 5 years.