Jan 25, 2023
Aniket Bhattacharyea
Learn how to create protected routes using React Context as well as how using Clerk makes this process easier.
In part one of this series, you learned about Next.js API routes and how to protect API routes with JWT authentication. In this part, you'll learn how to create protected routes using React Context as well as how using Clerk makes this process easier.
To get started, a starter app already has been made that you can clone from GitHub:
1git clone https://github.com/heraldofsolace/NextJS-protected-routes-demo.git2cd NextJS-protected-routes-demo3yarn install4yarn dev
The app will start running at localhost:3000
.
The posts page can be found at http://localhost:3000/posts
, which shows a list of all the posts.
This page has a companion API route that returns all the posts in JSON:
1$ curl "http://localhost:3000/api/posts"2[{"id":1,"userId":1,"text":"Hello, World!"},{"id":2,"userId":2,"text":"Hello, NextJS"},{"id":3,"userId":1,"text":"Lorem ipsum dolor sit amet. "},{"id":4,"userId":3,"text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ut rhoncus neque. Sed lacinia magna a mi tincidunt, ac interdum."}]
The data for posts is stored in data.js.
The /api/auth/login
route implements JWT authentication and returns a JWT when the correct username and password combination is supplied:
1$ curl -X POST "http://localhost:3000/api/auth/login" -d '{"username": "john", "password": "password"}' -H 'Content-Type: application/json'2{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjMsImxvY2F0aW9uIjoiRnJhbmNlIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE2Njk0NDUzMDQsImV4cCI6MTY2OTUzMTcwNH0.mlShbJr6xG8TIOyY6B2gD0c-leoyw1T3Sr5EncTgl00"}
Finally, the /api/users/me
route returns the currently authenticated user, provided a valid JWT is passed in the header:
1$ curl http://localhost:3000/api/users/me -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjMsImxvY2F0aW9uIjoiRnJhbmNlIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE2Njk0NDUzMDQsImV4cCI6MTY2OTUzMTcwNH0.mlShbJr6xG8TIOyY6B2gD0c-leoyw1T3Sr5EncTgl00"2{"id":3,"username":"john","name":"John","location":"France"}
The user profiles can also be found in data.js.
The goal of the article is to protect the /api/posts
API route and the /posts
page using the JWT authentication strategy. In part one, you saw how JWT authentication can be added to API routes using the jwt.verify
method. However, it's resource intensive to manually verify and decode the JWT in every single API route. It's also tedious to manually include the authentication header in every request that you make from a page.
In this article, you'll learn how to "share" an authenticated session across all pages by using an AuthContext
. You'll also refactor the JWT verification to a withAuth
wrapper that will make it easy to protect API routes. Finally, you'll see how using Clerk makes this process smooth and seamless.
AuthContext
is simply a React Context that will make it easy to pass required authentication parameters throughout the app. To implement this, first install the required libraries:
1yarn add js-cookie axios
js-cookie
will be used to store the JWT in the browser's cookie. axios
makes it easy to preconfigure a default API service with headers. You'll use this to include the token in the headers once the user logs in.
Create a file named api.js in the project root:
1import Axios from "axios";23const api = Axios.create({4baseURL: "http://localhost:3000",5headers: {6'Accept': 'application/json',7'Content-Type': 'application/json'8}9});1011export default api;
Here, an instance of axios
is created that will be used to make API calls later.
Create the file auth_context.js:
1import React, { createContext, useState, useContext, useEffect } from 'react'2import Cookies from 'js-cookie'3import api from './api';4import jwt from "jsonwebtoken";56const JWT_KEY = process.env.JWT_SECRET_KEY;7const AuthContext = createContext({});89export const AuthProvider = ({ children }) => {1011const [user, setUser] = useState(null)12const [isLoading, setIsLoading] = useState(true)1314useEffect(() => {15async function fetchUserFromCookie() {16const token = Cookies.get('token')17if (token) {18api.defaults.headers.Authorization = `Bearer ${token}`19const { data: user } = await api.get('api/users/me')20if (user) setUser(user);21}22setIsLoading(false)23}24fetchUserFromCookie()25}, [])2627const login = async (username, password) => {28const { data: {token } } = await api.post('api/auth/login', { username, password })29console.log("TOKEN ", token)30if (token) {31Cookies.set('token', token, { expires: 60 })32api.defaults.headers.Authorization = `Bearer ${token}`33const { data: user } = await api.get('api/users/me')34setUser(user)35}36}3738const logout = () => {39Cookies.remove('token')40setUser(null)41delete api.defaults.headers.Authorization42window.location.pathname = '/login'43}444546return (47<AuthContext.Provider value={{ isAuthenticated: !!user, user, login, loading: isLoading, logout }}>48{children}49</AuthContext.Provider>50)51}52535455export const useAuth = () => useContext(AuthContext)
The most important bits in the above Context are the fetchUserFromCookie
, login
, and logout
functions. The fetchUserFromCookie
function fetches the token from the cookie and sets the authorization
header in the default api
instance.
It then makes a call to /api/users/me
to retrieve and store the authenticated user. The login
function logs in the user through the /api/auth/login
route and stores the token in the cookie. The logout
function deletes the user, the cookie, and the authorization
header.
Finally, create the withAuth
function that'll protect the API routes by wrapping the handlers:
1export const withAuth = (handler) => {2return (req, res) => {3const { authorization } = req.headers;4if(!authorization) return res.status(401).json({ error: "The authorization header is required" });5const token = authorization.split(' ')[1];67jwt.verify(token, JWT_KEY, (err, payload) => {8if(err) return res.status(401).json({ error: "Unauthorized" });9req.auth = { user: payload };10handler(req, res);11});12}13}
You can now modify pages/api/posts.js
to include withAuth
:
1import { withAuth } from "../../auth_context";2import { users, posts } from "../../data";34export default withAuth((req, res) => {5const { userId } = req.auth.user;67const userPosts = posts.filter(post => {8return post.userId == userId9})1011res.status(200).json(userPosts)12})
Note that only the posts by the logged in user are returned. The logged in user is found through req.auth.user
.
Let's now create the login page. First, add Formik and Yup. These are not strictly required but will help in creating the login form.
1yarn add formik yup
Create pages/login.js
:
1import React from "react";2import { Formik, ErrorMessage, Form, Field } from "formik";3import * as Yup from "yup";4import { useAuth } from "../auth_context";5import { useRouter } from "next/router";6import api from "../api";78const LoginForm = () => {9const { login } = useAuth();10const router = useRouter();11return (12<Formik13initialValues={{ username: "", password: "" }}14validationSchema={Yup.object({15username: Yup.string().required("Required"),16password: Yup.string().required("Required"),17})}18onSubmit={async ({ username, password }, { setSubmitting, setErrors }) => {19try {20await login(username, password)21setSubmitting(false);22router.push("/posts");23} catch (error) {24const formikErrors = { password: error.response.data.error };25setErrors(formikErrors);26setSubmitting(false);27}28}}29>30{({ isSubmitting }) => (31<Form>32<div>33<label>Username</label><br />34<Field name="username" type="text"></Field>35<ErrorMessage name="username" component="p"></ErrorMessage>36</div>37<div>38<label>Password</label><br />39<Field name="password" type="password"></Field>40<ErrorMessage name="password" component="p"></ErrorMessage>41</div>42<button type="submit" disabled={isSubmitting}>Login</button>43</Form>4445)}46</Formik>47);48};4950export default LoginForm;
This page uses the login
function described above to log in the user.
Now update pages/posts.js
to make use of AuthContext
:
1import { useEffect, useState } from "react";2import api from "../api";3import { useAuth } from "../auth_context";45export default function Posts() {6const [posts, setPosts] = useState(null);7const { user, logout } = useAuth()8console.log(user)9useEffect(() => {10async function fetchPosts() {11const { data: postsList } = await api.get("api/posts");12setPosts(postsList);13console.log(postsList);14}15if(user) fetchPosts()16}, [user]);1718return (19<>20<ul>21{posts?.map(post => {22return (<li key={post.id}>{post.text}</li>)23})}24</ul>25<button onClick={logout}>Logout</button>26</>27)28}
Finally, update pages/_app.js
to wrap everything in AuthProvider
:
1import '../styles/globals.css'2import { AuthProvider } from "../auth_context"34function MyApp({ Component, pageProps }) {5return (6<AuthProvider>7<Component {...pageProps} />8</AuthProvider>9)10}1112export default MyApp
Start the server with yarn dev
and visit http://localhost:3000/login
. Log in with the credentials (you can find the username in data.js and the password is "password") and you'll be redirected to the posts page. You can verify that you're only seeing posts from the logged in user.
The final app for this section can be found in the manual
branch of the GitHub repo.
In this section, you'll replace the manual authentication with Clerk. Before starting with Clerk, you should revert the changes you've made so far. You can simply run git stash && git clean -fdx
to stash your changes.
First, create an application in Clerk. You can keep all the default options.
Once the application is created, go to the API keys page and copy the frontend API key, the backend API key, and the JWT verification key. Paste these into .env
:
1NEXT_PUBLIC_CLERK_FRONTEND_API=your_frontend_api_key2CLERK_API_KEY=your_backend_api_key3CLERK_JWT_KEY=your_jwt_key
Install the required library:
1yarn add @clerk/nextjs
Wrap pages/_app.js
with Clerkprovider
:
1import { ClerkProvider, SignedIn, SignedOut, RedirectToSignIn } from '@clerk/nextjs';2import { useRouter } from 'next/router';34const publicPages = [ "/" ];56function MyApp({ Component, pageProps }) {78const { pathname } = useRouter()9const isPublicPage = publicPages.includes(pathname)1011return (12<ClerkProvider {...pageProps}>13{isPublicPage ? (14<Component {...pageProps} />15) : (16<>17<SignedIn>18<Component {...pageProps} />19</SignedIn>20<SignedOut>21<RedirectToSignIn />22</SignedOut>23</>24)}25</ClerkProvider>26)27}2829export default MyApp;
The publicPages
array decides which pages will not be protected under authentication. For a protected page, if the user is not signed in, they will be redirected to the login page.
Create the file middleware.js in the project route:
1import { withClerkMiddleware } from "@clerk/nextjs/server";2import { NextResponse } from "next/server";34export default withClerkMiddleware((req) => {5return NextResponse.next();6});78// Stop Middleware running on static files9export const config = { matcher: '/((?!.*\\.).*)' }
Modify pages/api/posts.js
to include the getAuth
function that authenticates the user with Clerk:
1import { posts } from "../../data";2import { getAuth } from "@clerk/nextjs/server";34export default function handler(req, res) {5const { userId } = getAuth(req);6const userPosts = posts.filter(post => {7return post.userId == userId8})910res.status(200).json(userPosts)11}
In the Clerk dashboard, go to the Users page and create a test user.
After the user is created, open the record and copy the ID as shown below.
Open data.js and replace the numeric id
field of any one user and the userId
field in the posts
array:
1export const users = [2{3id: "user_2IUxt1YsiCfebtlUwOe5dU91Det",4username: "bob",5name: "Bob",6location: "USA",7password: '$2y$10$mj1OMFvVmGAR4gEEXZGtA.R5wYWBZTis72hSXzpxEs.QoXT3ifKSq'8},9...10];1112export const posts = [13{14id: 1,15userId: "user_2IUxt1YsiCfebtlUwOe5dU91Det",16text: "Hello, World!"17},18...19{20id: 3,21userId: "user_2IUxt1YsiCfebtlUwOe5dU91Det",22text: "Lorem ipsum dolor sit amet. "23},24...25];
Run the server again with yarn dev
. Visit http://localhost:3000/posts
and you should be redirected to Clerk's login page.
After logging in, you'll be redirected back to the /posts
page. Verify that you can see only the posts corresponding to the logged in user.
Protecting Next.js routes with authentication is a vital part of developing any web app. However, manually creating authentication mechanisms can be tedious and time-consuming. A solution like Clerk comes with all the bells and whistles so that you don't need to worry about authentication and can focus on the core app instead.
Start completely free for up to 5,000 monthly active users and up to 10 monthly active orgs. No credit card required.
Learn more about our transparent per-user costs to estimate how much your company could save by implementing Clerk.
The latest news and updates from Clerk, sent to your inbox.