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 interface. In 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.
Learn more about how OAuth 2.0 and Access Token works.
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.
<main>
<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>
<div className="card">
<h3>Private Endpoint</h3>
<p>You need to log in to access this endpoint</p>
</div>
</div>
</main>
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 react
import 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
cotter
.signInWithOTP()
.showEmailForm()
.then(payload => {
console.log(payload);
alert("Success");
})
.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": "team@cotter.app",
"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
cotter
.signInWithOTP()
.showEmailForm()
.then(payload => {
console.log(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 form
useEffect(() => {...}, []);
// Add the lines below here
// 2️⃣(b) Check if the ACCESS_TOKEN exists every time the page loads
useEffect(() => {
if (localStorage.getItem("ACCESS_TOKEN") != null) {
setIsLoggedIn(true);
}
}, []);
Now let's show if the user is logged in below our form:
{/* 2️⃣(c) Show if the user is logged in. */}
<p>
{isLoggedIn ? "✅ You are logged in" : "❌ You are not logged in"}
</p>
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 users
const logOut = () => {
localStorage.removeItem("ACCESS_TOKEN");
setIsLoggedIn(false);
};
Home
before return
And show the logout button:
{/* 3️⃣ Show the logout button */}
{isLoggedIn ? (
<div
className="card"
style={{ padding: 10, margin: 5 }}
onClick={logOut}
>
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;
res.end(
"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];
console.log(token);
// 2) TODO: Validate the access_token
handler(req, res);
}
const handler = (req, res) => {
res.statusCode = 200;
res.end(
`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
import { 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) {
console.log(e);
valid = false;
}
if (!valid) {
res.statusCode = 403;
res.end("Authorization header is invalid");
}
checkJWT
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");
return;
}
var resp = await fetch("/api/private", {
headers: {
Authorization: `Bearer ${token}`,
},
});
setPrivateResource(await resp.text());
};
// Until here
return(...);
}
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>
<p>{publicResource}</p>
</div>
<div className="card" onClick={getPrivateResource}>
<h3>Private Endpoint</h3>
<p>{privateResource}</p>
</div>
</div>
grid
classWe 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 https://jwt.io/ 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.
Top comments (0)