Passwordless Login with Email and JSON Web Token (JWT) Authentication using Next.js

How do you log your users in and how do you give them access? We'll go over how to authenticate and authorize users without passwords in Next.js.

When you start adding users to your website, the main question that you need to answer is: how do you log your users in and how do you give them access to the appropriate resources?

In this tutorial we'll go over how to address both questions and build a Next.js app that only allows logged-in users to access private resources within the app.

So you want to have users.

Let's go over some concepts: authentication vs authorization.

Authentication: How do I log my users in?

Authentication is a way for your server to verify the user's identity. The most common way to authenticate users is by using the email/password combo. Unfortunately, passwords have serious disadvantages on both security and user interfaceIn this tutorial, we'll use a verification code sent to the user's email to authenticate the user.

Authorization: How do I keep my users logged in?

Authorization is a way for your server to authorize a request. In simpler terms, this is where you pass in a token or session to your backend server when calling an API to view or update some data. The 2 common strategies are cookie-based sessions and JWT tokens.

The main advantage for JWT tokens is that it's not stored in your database so you don't need to do a DB check to validate every request. That's why we're going to use JWT Tokens in this tutorial.

How would the overall registration/login look like?

Authentication: We'll ask for the user's email and send them an email containing a code. If the user enters the code correctly, we'll get a JWT Token in the frontend and store it in localStorage.

Authorization: Every time we want to access a private API endpoint we need to include a header Authorization: Bearer ${token}.

Storing the token in the browser local storage is susceptible to cross-site scripting (XSS) attack. If an attacker can successfully run JavaScript code in your site, they can retrieve the tokens stored in local storage. XSS vulnerability arises when your website takes data from users without proper validation or from a third-party JavaScript code (like Google Analytics, jQuery, etc) included in the website.

Let's Start Building

Create your Next.js app. We'll call the app next-passwordless-login and use the default starter app.

yarn create next-app
cd next-passwordless-login && yarn dev

Update our website

Update your pages/index.js . Delete everything except the styling and the container div, then add this inside the container div.

    <h1 className="title">Passwordless App.</h1>

    {/* 1️⃣ TODO: Setup a div to contain the form */}
    <div className="grid">
        <div className="card">
            <h3>Public Endpoint</h3>
            <p>You should be able to access this when not logged in</p>

        <div className="card">
            <h3>Private Endpoint</h3>
            <p>You need to log in to access this endpoint</p>

Step 1: Show the Register/Login form

Install the dependencies:

yarn add cotter cotter-node

Add a div to contain the form below our title in pages/index.js

<h1 className="title">Passwordless App.</h1>

{/* 1️⃣ TODO: Setup a div to contain the form */}
<div id="cotter-form-container" style={{ width: 300, height: 300 }} />

Then import and initialize Cotter to embed the email form.

// 1️⃣ import Cotter verification form and useEffect from reactimport Cotter from "cotter";import { useEffect } from "react";
export default function Home() {
  // 1️⃣ Initialize and show the form
  // Add the lines here
  useEffect(() => {
    var cotter = new Cotter(API_KEY_ID); // 👈 Specify your API KEY ID here
      .then(payload => {
      .catch(err => console.log(err));
  }, []);
  // until here

  return (...);}

You need to add your API_KEY_ID here. Create a free account at Cotter, then create a Project and take notes of the API Keys.

Now you should be able to see the login form like below.

The form will automatically send an email as necessary and show an input to enter the code. It won't send another email if you've already verified your email in this browser.

Step 2: Keep users logged in with access_token

Read the console.log

Try entering your email and logging-in. You should see that the payload we receive in the OnSuccess function contains the following object:

  "token": {...},
  "email": "[email protected]",
  "oauth_token": {
    "access_token": "eyJhbGciOiJFUzI1NiIsIn...",
    "id_token": "eyJhbGciOiJFUzI1NiIsInR5cC...",
    "refresh_token": "199:doZor3GtgsrYo4R7L...",
    "expires_in": 3600,
    "token_type": "Bearer",
    "auth_method": "OTP"
  "user": {
    "ID": "ecadbd2c-56f8-4078-b45d-f17786ed499e", // Cotter User ID

We want to use the access_token in this tutorial, so let's grab that and store it in localStorage.

  useEffect(() => {
    var cotter = new Cotter(API_KEY_ID); // 👈 Specify your API KEY ID here
      .then(payload => {
-        alert("Success");
+        // 2️⃣(a) Store the access token and set logged in
+        localStorage.setItem("ACCESS_TOKEN", payload.oauth_token.access_token);
+        setIsLoggedIn(true);
      .catch(err => console.log(err));
  }, []);

Now let's define setIsLoggedIn(), this will help us show whether the user is logged in or not.

     import Cotter from "cotter";
     import { useEffect } from "react";
+    import { useState } from "react";
    export default function Home() {
+      // 2️⃣(a) Show if the user is logged in.
+      var [isLoggedIn, setIsLoggedIn] = useState(false);

We also want to check if the localStorage contains ACCESS_TOKEN every time the page loads and update our isLoggedIn variable. Add this below the first useEffect() .

// 1️⃣ Initialize and show the formuseEffect(() => {...}, []);

// Add the lines below here// 2️⃣(b) Check if the ACCESS_TOKEN exists every time the page loadsuseEffect(() => {
    if (localStorage.getItem("ACCESS_TOKEN") != null) {
    }}, []);

Now let's show if the user is logged in below our form:

{/* 2️⃣(c) Show if the user is logged in. */}
    {isLoggedIn ? "✅ You are logged in" : "❌ You are not logged in"}

Step 3: Logging-out

Logging-out is achieved by removing the access_token from our localStorage. Let's add the logout function inside Home before return() in pages/index.js

// 3️⃣ Log out usersconst logOut = () => {

And show the logout button:

{/* 3️⃣ Show the logout button */}{isLoggedIn ? (
        style={{ padding: 10, margin: 5 }}
    	Log Out
    </div>) : null}

You can now see the if you're logged in and the logout button:

Step 4: Allowing the user from accessing public/private endpoints.

Let's add 2 routes in our pages/api

touch pages/api/public.js pages/api/private.js

Defining the routes

Let's define our /api/public endpoint in pages/api/public.js. We're just going to return that the request is successful.

export default (req, res) => {
  res.statusCode = 200;
    "Success! This is a public resource, you can see it without logging in."

Let's define our /api/private endpoint in pages/api/private.js. First we'll check if the authorization header exists.

// 2) TODO: Import Cotter

const checkJWT = (handler) => async (req, res) => {
  // 1) Check that the access_token exists
  if (!("authorization" in req.headers)) {
    res.statusCode = 401;
    res.end("Authorization header missing");
  const auth = await req.headers.authorization;
  const bearer = auth.split(" ");
  const token = bearer[1];
  // 2) TODO: Validate the access_token
  handler(req, res);}

const handler = (req, res) => {
  res.statusCode = 200;
    `Success! This is a private resource and you have the access_token to view it.`

export default checkJWT(handler);

Now let's validate the access token.

First, import Cotter's jwt validator function at the top of pages/api/private.js

// 2) TODO: Import Cotterimport { CotterValidateJWT } from "cotter-node";

Then call CotterValidateJWT(token) under step (2) inside checkJWT.

  // 2) TODO: Validate the access_token
  var valid = false;
  try {
    valid = await CotterValidateJWT(token);
  } catch (e) {
    valid = false;
  if (!valid) {
    res.statusCode = 403;
    res.end("Authorization header is invalid");

Calling the /public and /private API endpoints

Let's go back to pages/index.js and add 2 functions: getPublicResource and getPrivateResource that will call the endpoint /api/public and /api/private.

export default function Home() {
  // 4️⃣ Get Public and Private Resources
  // Add the lines here
  var [publicResource, setPublicResource] = useState(null);
  var [privateResource, setPrivateResource] = useState(null);
  // Get Public Resource
  const getPublicResource = async () => {
    var resp = await fetch("/api/public");
    setPublicResource(await resp.text());
  // Get Private Resource
  const getPrivateResource = async () => {
    var token = localStorage.getItem("ACCESS_TOKEN");
    if (token == null) {
      setPrivateResource("Token doesn't exist, you're logged-out");
    var resp = await fetch("/api/private", {
      headers: {
        Authorization: `Bearer ${token}`,
    setPrivateResource(await resp.text());
  // Until here

Now let's call the 2 functions from our buttons and show the response from the endpoints. Update the div with className="grid" to the following code:

{/* 4️⃣ Call Get Public and Private Resources */}
<div className="grid">
    <div className="card" onClick={getPublicResource}>
        <h3>Public Endpoint</h3>

    <div className="card" onClick={getPrivateResource}>
        <h3>Private Endpoint</h3>

We display the response from the endpoints in the publicResource and privateResource variables.

That's it

Now you can authenticate users by sending a code to their emails and allow them to access private endpoints that require an access_token to access.

If you're curious, print out the access_token and copy it to to see what information is decoded. The id_token contains more information about the user and the refresh_token is used to get a new access_token if it's expired.

What's Next?

Learn more about the OAuth tokens returned from Cotter and use them in your API endpoints.

If you want to authenticate users using their phone number, follow this guide on Verifying User's Phone Number via SMS and WhatsApp.

Questions & Feedback

If you have any questions or feedback, feel free to join Cotter's Slack Channel and chat us there.

Ready to use Cotter?

If you enjoyed this tutorial and want to integrate Cotter into your website or app, you can create a free account and check out our documentation.

Made in Typedream