DEV Community

loading...
Cover image for Create a Passwordless πŸ”‘ auth login flow with Next.js API Routes

Create a Passwordless πŸ”‘ auth login flow with Next.js API Routes

Richard
I'm building a better web 🧠
・4 min read

Many of us struggling with remembering passwords for every sites we signed up so creating a passwordless authentication could serve a pretty nice user experience in some use cases.

There are perfect solutions to achieve this with third party services but in this post I would like to cover a self-made example.

The technologies and main packages I will use are:

  • Next.js
  • mongodb
  • jsonwebtoken
  • cookie
  • nodemailer

Notice: As I want to keep it short I will not cover the signup scenario here but it's similar. Also won't cover the frontend scenario (sending the requests, waiting for the answers and handle them) that could be covered by another post.

The main idea is in order:

  1. the user wants to sign in and sends his/her email address (without password)
  2. an API route catches the email address and checks if that exists in the Database.
  3. the server creates a JWT token for the user and sends an email with it.
  4. the user clicks the link in the email which hits another api route: This one checks if the token is valid. If it is it sets an HttpOnly Cookie to the browser and redirects the user to the desired page.

The result: The user is safely logged in until he/she logges out or the token expires.

Let's see the flow in details

First we need set up a Next.js project and connect it to a database. I won't cover the project set up here please check the Next.js docs how to do that. Once we have a project up and running connect that to mongodb. To do that I found this article from mongodb very useful:

How to Integrate MongoDB Into Your Next.js App

Don't forget to install the remaining packages:

npm install jsonwebtoken cookie nodemailer
Enter fullscreen mode Exit fullscreen mode

After we have that - consider we have at least one user in the database who wants to sign in πŸ™ƒ

imagine this is mongodb:

{
  "_id": "606101c0af993c79f24a52d2",
  "email": "user@example.com"
}
Enter fullscreen mode Exit fullscreen mode

Look, there is no password or an enormous hash! πŸ€“

We need an API endpoint to catch the email address and send an email.

pages/api/auth/login.js

import jwt from 'jsonwebtoken';
import { connectToDatabase } from '..../mongodb';
import { sendEmail } from '..../server/sendEmail';

// We need a secret on there server side for the tokens
const { JWT_SIGNATURE } = process.env;

export default async function login(req, res) {
  // pls check the mongodb article above for details
  const { db } = await connectToDatabase(); 
  const { email } = req.body;

  try {
    const user = await db.collection('users').findOne({ email });
    if (user._id){
      // We found the user registered, let's create a token!
      const payload = { userId: user._id, email: user.email };
      const token = jwt.sign(payload, JWT_SIGNATURE);

      // We have the token let's email it!
      const messageId = await sendEmail(email, token);
      res
        .status(200)
        .json({ message: `Email was sent! id: ${messageId}` });

  } else {
      res
        .status(401)
        .json({ message: 'This email was not registered yet.' })
    }
  } catch(err) {
    console.error(err);
  }
}
Enter fullscreen mode Exit fullscreen mode

Set up nodemailer for sending emails

This is just a basic setup you can of course implement more styling and separate the config in different files.

server/sendEmail.js

import nodemailer from "nodemailer";

export async function sendEmail(email, token) {
  const href = `https://frontend.com/api/auth/verify?token=${token}`;

  try {
    let transporter = nodemailer.createTransport({
      host: process.env.EMAIL_SERVER_HOST,
      port: process.env.EMAIL_SERVER_PORT,
      auth: {
        user: process.env.EMAIL_SERVER_USER,
        pass: process.env.EMAIL_SERVER_PASSWORD,
      },
    });

    let info = await transporter.sendMail({
      from: process.env.EMAIL_FROM,
      to: email,
      subject: 'Here is your login link! πŸ₯³',
      html: `
        <h1>Hello/</h1>
        <p>Please click <a href={href}>here</a> to sign in!</p>
      `;
    });

    console.log("Message sent: %s", info.messageId);
    return info.messageId;
  } catch (e) {
    console.error;
  }
}
Enter fullscreen mode Exit fullscreen mode

We need an API endpoint to wait for this token and sign in the user

pages/api/auth/verify.js

import cookie from 'cookie';
import jwt from 'jsonwebtoken';

const { JWT_SIGNATURE } = process.env;

export default async function verify(req, res) {
  const { token } = req.query;

  jwt.verify(token, JWT_SIGNATURE, (err, decoded) => {
    if (err) {
      res.status(401).json({ message: 'Token expired / invalid' });
    } else {
      res.setHeader(
        'Set-Cookie',
        cookie.serialize('anyCookieName', decoded.userId, {
          path: '/',
          httpOnly: true,
          maxAge: 60 * 60 * 24 * 7, // 1 week
          secure: process.env.NODE_ENV !== 'development',
        })
      );
      res.status(200).redirect('https://frontend.com');
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

The user is signed in! ✨

After this any time a request hits an endpoint from this browser the HTTP Cookies travels with them so you can capture it and check the user against the database.

export default async function anyEndpoint(req, res) {
  const userId = req.cookies.anyCookieName;
  // ...
}
Enter fullscreen mode Exit fullscreen mode

You got the idea.

Conclusion

βœ… Β  Implementing a passwordless authentication is fun and hopefully also very convenient for the end users. You cannot lose the passwords they cannot leak either. You don't need to handle password resets.

βœ… Β  This tokens (~sessions) could live in the users' browser really long as they are HttpOnly Cookies so cannot accessed by browser extensions or any client side javascript. Pretty safe.

πŸ‘Ž You should always go to the email client to sign in.

If you want to logout remotely you can implement an accessToken / refreshToken session based version which could be covered by another post πŸ™‚

Hope you enjoyed! ✌️

More thoughts:

Other techs:

Discussion (2)

Collapse
kosztolanyi profile image
Zsolt Kosztolanyi-Baji

NiceπŸ‘πŸ»πŸ‘πŸ»

Collapse
nemethricsi profile image
Richard Author

Thank you @kosztolanyi !