Next.js JWT authentication with React Hooks and Context API
January 31, 2020
A while ago I wrote this post about the setup of my NextJS app with JWT’s using TypeScript. While it received a good response, I have decided to update the project to give a better code structure, and use newer features of React. The old project can still be found on this branch, but the project relating to this post is the master branch.
This post will detail some of the updates made in terms of the authentication and global state management, as well as explaining why I decided to change some of the areas of the app.
Project overview
To give some quick context about the arrangement of the project - there’s a client side and a server side of this application. The client side is the Next JS installation, comprising of the Next JS server side rendered React application. There’s also a separate server which is a Node Express server, responsible for receiving API calls from the Next JS application.
This is still considered best practice by separating the concerns of the client and server side, as I explained in my last post, however it’s definitely possible to do everything in one Next JS application. You may want to look at the custom server guide on how to do this.
Registration
The login and registration process has not changed much since the previous iteration. Previously, I’d created a whole class for authentication called the AuthService
class, however this felt like I was cramming too much functionality in to one class which was not related enough. The class contained functionality to perform login and register requests, parse the token, redirect the user, etc.but actually the classes should be more lightweight and refined, serving a specific purpose.
Because of this, I’ve created independent services for token management, fetching, and navigation, keeping all related functionality grouped better. Here’s how the login and registration works using the new, refined structure.
Here’s the few steps to develop the registration:
- User enters their email address, password, and any other information you want to collect from them (adhering to the GDPR of course!)
- On the API server, we check the make sure the user isn’t already registered, and if they aren’t we add them to the “users” table
- A success/fail response is sent back to the Next JS app
First, the user registration process allows the user to submit their details to the server:
// src/pages/register.tsx
// ...imports
export default class Register extends React.Component<Props> {
render() {
return (
<main>
<Formik
initialValues={{
firstName: "",
lastName: "",
email: "",
password: "",
}}
onSubmit={(
values: IRegisterIn,
{ setSubmitting }: FormikHelpers<IRegisterIn>
) => {
FetchService.isofetch(
"/auth/register",
{
firstName: values.firstName,
lastName: values.lastName,
email: values.email,
password: values.password,
},
"POST"
)
.then((res) => {
// show success message
})
.catch()
}}
render={() => <Form>{/*form fields*/}</Form>}
/>
</main>
)
}
}
The user enters their details in the simple Formik form, and on submit the details are sent to the server using the fetch service. The fetch service is a class only responsible for submitting data to the API:
// src/services/Fetch.service.ts
import fetch from "isomorphic-unfetch"
import Cookies from "universal-cookie"
class FetchService {
public isofetch(url: string, data: object, type: string): Promise<any> {
return fetch(`${process.env.API_URL}${url}`, {
body: JSON.stringify({ ...data }),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
method: type,
})
.then((response: Response) => response.json())
.then(this.handleErrors)
.catch((error) => {
throw error
})
}
public handleErrors(response: string): string {
if (response === "TypeError: Failed to fetch") {
throw Error("Server error.")
}
return response
}
}
export default new FetchService()
Nothing interesting here - the request uses the isomorphic-unfetch package to do the API requests, allowing both client and server to action the requests depending on the current state of the SSR in the app. The API URL (and a lot of other variable information) is retrieved from a Git-ignored .env
file, which is much easier in Next JS with the latest version 9.
It’s also worth pointing out that these API reqursts to register the user are going to a separate server, not the Next JS server.
I won’t go in to detail with a lot of the server side functionality as it’s not really the point of this post, but feel free to check it out over at the repo for specifics on how the user information is processed.
Login and restricting access to logged-in areas
Now the user is registered, but all we’ve done is stored their email, name and chosen hashed password in the database. We’ve not touched a JWT or cookie yet, as the actual authentication ony happens when the user logs in. Here are the steps we want to take for the login and getting the user authenticated:
- User enters their email address and password
- On the server, details are compared against database values, and if successful a JWT is generated which holds basic user information
- The JWT is sent back to the front end of the application where it is saved in a cookie
- The user is then directed to their account page
- This account page (and every other logged in page) uses Next JS
getInitialProps()
method to check the JWT contained inside the cookie, and validate the JWT to decide if the user should stay or be redirected
Similar to the registration process, the login uses Formik to pull together a simple form and take the user’s email and password:
// login.tsx
// ... imports
interface Props {}
export default class Login extends React.Component<Props> {
render() {
return (
<main>
<Formik
initialValues={{
email: "",
password: "",
}}
onSubmit={(
values: ILoginIn,
{ setSubmitting }: FormikHelpers<ILoginIn>
) => {
FetchService.isofetch(
"/auth/login",
{
email: values.email,
password: values.password,
},
"POST"
)
.then((res) => {
// log user in or show error
})
.catch()
}}
render={() => <Form>// form fields</Form>}
/>
</main>
)
}
}
The server then compares the user’s details and generates the JWT:
// server side - src/api/v1/auth.ts
router.post("/login", (req, res) => {
const user = new User()
const email = req.body.email
const password = req.body.password
if (!email || !password) {
return errors.errorHandler(res, "You must send all login details.", null)
}
return (
user
// compare the user's email and password with that of the database
.loginUser({ email, password })
.then((theUser: IUser) => {
const authToken = user.createToken(theUser)
return Promise.all([authToken]).then((tokens) => {
return {
authToken: tokens[0],
}
})
})
.then((theUser: IUser) => {
return (
theUser &&
res.send({
success: true,
authToken: theUser.authToken,
})
)
})
.catch((err: any) => {
return errors.errorHandler(res, err.message, null)
})
)
})
// server side - src/api/services/User.ts
class User {
createToken(user: IUser) {
return jwt.sign(_.omit(user, "password"), config.authSecret, {
expiresIn: "10m",
})
}
}
The token is created here storing all user information in the database, except the password which has been omitted using Lodash omit()
. You may have more user info in your app so be sure not to store too much in the JWT as it’s good to keep them kind of lean. Remember, anyone can view the contents of a JWT without a password - the password/secret is required to validate the JWT, not view it’s contents.
Also you’ll notice I am using a good ol’ promise chain here, as I do in a lot of my applications. For some reason I just like using promises more than the async await syntax. I feel it keeps my on my toes, and looks more elegant than the async/await syntax.
Next, the JWT is sent to the front end (the Next JS app), and stored in a cookie:
// login.tsx
// ... imports
interface Props {}
export default class Login extends React.Component<Props> {
render() {
return (
<main>
<Formik
initialValues={{
email: "",
password: "",
}}
onSubmit={(
values: ILoginIn,
{ setSubmitting }: FormikHelpers<ILoginIn>
) => {
FetchService.isofetch(
"/auth/login",
{
email: values.email,
password: values.password,
},
"POST"
)
.then((res) => {
if (res.success) {
// save token in cookie for subsequent requests
const tokenService = new TokenService()
tokenService.saveToken(res.authToken)
Router.push("/account")
}
})
.catch()
}}
render={() => <Form>// form fields</Form>}
/>
</main>
)
}
}
// src/services/Token.service.ts
import { NextPageContext } from "next/types"
import Cookies from "universal-cookie"
import FetchService from "../services/Fetch.service"
import NavService from "../services/Nav.service"
class TokenService {
public saveToken(token: string) {
const cookies = new Cookies()
cookies.set("token", token, { path: "/" })
return Promise.resolve()
}
}
export default TokenService
The JWT is stored using the universal-cookie
package which I prefer over others like react-cookie
- I had issues with some others in the past and this has never let me down! So we now have a cookie, which by nature can be accessed both on the client side and server side. We can use this to perform checks on the JWT on subsequent pages, server side, before the page is rendered, and one such page I’ve made is called the “account” page:
// src/pages/account.tsx
import React from "react"
import { NextPageContext } from "next"
import TokenService from "../services/Token.service"
interface Props {}
export default class Account extends React.Component<Props> {
static async getInitialProps(ctx: NextPageContext) {
const tokenService = new TokenService()
await tokenService.authenticateTokenSsr(ctx)
return {}
}
render() {
return <main>My account</main>
}
}
Next JS privides us with the getInitialProps()
lifecycle method of a React component so we can perform actions on the server and render the application there instead of all on the client side - SSR, so we can leverage this to validate the JWT which is stored in the cookie. Next JS gives us access to this by the “Page Context” ctx
value. We pass this in to our next method in the TokenService
class called authenticateTokenSsr()
:
// src/services/Token.service.ts
import { NextPageContext } from "next/types"
import Cookies from "universal-cookie"
import FetchService from "../services/Fetch.service"
import NavService from "../services/Nav.service"
class TokenService {
public saveToken(token: string) {
const cookies = new Cookies()
cookies.set("token", token, { path: "/" })
return Promise.resolve()
}
public checkAuthToken(token: string): Promise<any> {
return FetchService.isofetchAuthed(`/auth/validate`, { token }, "POST")
}
public async authenticateTokenSsr(ctx: NextPageContext) {
const cookies = new Cookies(ctx.req ? ctx.req.headers.cookie : null)
const token = cookies.get("token")
const response = await this.checkAuthToken(token)
if (!response.success) {
const navService = new NavService()
navService.redirectUser("/", ctx)
}
}
}
export default TokenService
The authenticateTokenSsr()
method gets the cookie from the ctx
object, and we know this is 100% being handled on the server as it was called with getInitialProps()
. The token is then passed to another method, checkAuthToken()
, which validates the token.
Since getting the token back from the API when the user first logged in, all the functionality has been performed on the client side of the application in Next JS, but now we’re sending the token to the API which will do the validation. Why the API? And why not just validate on the Next JS side? Well, it makes sense to keep the separation of concerns here and have that kind of processing handled on the other server. I already have the jsonwebtoken
package inatalled on the API server in Express anyway, so it makes sense, but it really can be handled any which way you want.
If the token is invalid ( if (!response.success)
in the above snippet), we can redirect the user away from the authenticated area which will stop the page from being rendered in Next SSR. If the token is valid, the getInitialProps()
returns an empty object as usual, and the site will continue to load the “account” area.
As the user now has a valid JWT in their cookie which can be accessed by the browser or server in Next JS, we can perform all sorts of auth checks and showing the user different types of information based on their user ID or account privileges, but that’s for another time.
Global state management and persisting state on page reload
We have the user accessing a restricted area using Next JS server side rendering of our application, but that’s all we’re doing. We want to expand this app so we can access the user information anywhere in the app so we can make a user login component to show their name and profile picture for example. This is where global state management comes in.
Global state management fascinates me because it’s approached in so many different ways, and I often see it over engineered and messy. This is why my aim for this repo was to use a solution which is concise and lean. I love Redux, and despite their being talk over the last couple of years of it dying out, I’ve seen more hype about it than ever recently. However, I wanted to go for a different solution with this project, and instead use React’s Context API.
Here’s what we’re aiming for:
- When the user logs in,
dispatch
their user information to the Context API - The Context API is passed down through the React component tree by using the Provider
- The information stored in the context is then accessed anywhere in the application via component context (in a user profile widget in the header, for example)
- The user info is also stored in
localStorage
- When the user refreshes their browser, the same information is retrieved from localStorage if exists, and added back in to the Context API
There’s a lot to take in here if you’ve not used the Context API and React Hooks, so head over to the docs if you want to know more.
I’ve taken out a fair amount of TypeScript from this snippet to keep it simple, so see the repo to view the full version.
// src/services/Auth.context.ts
import React, { useReducer, useContext, useEffect } from "react"
// create the context
export const AuthStateContext = React.createContext({})
// set up initial state which is used in the below `AuthProvider` function
const initialState = { firstName: "", lastName: "", email: "" }
// set up the reducer - same as Redux, allows us to process more complex changes
// to the state within the context API
const reducer = (state, action) => {
switch (action.type) {
case "setAuthDetails":
return {
firstName: action.payload.firstName,
lastName: action.payload.lastName,
email: action.payload.email,
}
case "removeAuthDetails":
return {
firstName: initialState.firstName,
lastName: initialState.lastName,
email: initialState.email,
}
default:
throw new Error(`Unhandled action type: ${action.type}`)
}
}
// create and export the AuthProvider - this is imported to the _app.js file
// and wrapped around the whole app, providing context to the whole app, and
// is called each time this specific context is accessed (updated or retrieved)
export const AuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<AuthStateContext.Provider value={[state, dispatch]}>
{children}
</AuthStateContext.Provider>
)
}
// useContext hook - export here to keep code for global auth state
// together in this file, allowing user info to be accessed and updated
// in any functional component using the hook
export const useAuth: any = () => useContext(AuthStateContext)
The AuthProvider
function is exported, from this file and wrapped around the main app component in _app.tsx
:
// src/pages/_app.tsx
import Layout from "../components/Layout"
import { AuthProvider } from "../services/Auth.context"
function MyApp({ Component, pageProps }) {
return (
<AuthProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AuthProvider>
)
}
export default MyApp
The AuthProvider
function in the Auth.context.ts
file takes the Context API values, which is state
and dispatch
, and passes them down the component tree. We have access to state
and dispatch
because of the useReducer
function which is part of React Context API. It’s essentially a trimmed down version of Redux which is why it’s so fun to use!
So how do we access the data ( state
and dispatch
) passed down to the component tree? This is where the Context API and React Hooks work together really well. The data can be accessed in a few ways, but it will depend on where you’re accessing.
Accessing the Context API inside a class component - updating global state when the user logs in
I still like working with class components, even though hooks make it possible not to even touch one. An example in our app where we might want to access the Context is when the user logs in to the app and we want to store their data:
// login.tsx
// ... imports
interface Props {}
export default class Login extends React.Component<Props> {
// enable context type to get the auth state from context
static contextType = AuthStateContext
render() {
return (
<main>
<Formik
initialValues={{
email: "",
password: "",
}}
onSubmit={(
values: ILoginIn,
{ setSubmitting }: FormikHelpers<ILoginIn>
) => {
FetchService.isofetch(
"/auth/login",
{
email: values.email,
password: values.password,
},
"POST"
)
.then((res) => {
if (res.success) {
// save token in cookie for subsequent requests
const tokenService = new TokenService()
tokenService.saveToken(res.authToken)
// get dispatch function out of context so we can
// store the user's info in global state - can't
// use hooks in class component
const [state, dispatch] = this.context
dispatch({
type: "setAuthDetails",
payload: {
firstName: res.firstName,
lastName: res.lastName,
email: res.email,
},
})
Router.push("/account")
}
})
.catch()
}}
render={() => <Form>// form fields</Form>}
/>
</main>
)
}
}
The important part about this snippet is the:
static contextType = AuthStateContext;
and the dispatch
function in the success handler of the API call. We’re easily able to access the context with this.context
. We can access the state here if we wanted (the user’s name and email address if it already existed), but here we just want to access the dispatch
function to update our state via the reducer.
Accessing the Context API inside a functional component - accessing global state to show basic user information
We can use React hooks to access the context in any functional component too, which is possible because we exported useContext()
hook in the Auth.context.ts
:
export const useAuth: any = () => useContext(AuthStateContext);
This useAuth()
exported function is used in our project to access user information in the UserInfo
component:
// src/components/UserInfo.tsx
import * as React from "react"
import { useAuth } from "../services/Auth.context"
type IProps = {}
const UserInfo: React.FunctionComponent<IProps> = () => {
// use the "useContext()" hook to get user info
const [state, dispatch] = useAuth()
return state.firstName ? (
<div>
{state.firstName} {state.lastName}
</div>
) : null
}
export default UserInfo
We now have user info accessible and updatable anywhere in our application, but if we refresh our browser the page is rendered again and we loose all our state. With Redux this was easily handled with redux-persist
but we don’t have that luxury here. Luckily for us, we can implement a solution with only a few lines of code very easily!
This is a snippet from the Auth.context.ts
file from earlier:
// src/services/Auth.context.ts
export const AuthProvider = ({ children }: any) => {
let localState = null
if (typeof localStorage !== "undefined" && localStorage.getItem("userInfo")) {
localState = JSON.parse(localStorage.getItem("userInfo") || "")
}
const [state, dispatch] = useReducer(reducer, localState || initialState)
if (typeof localStorage !== "undefined") {
useEffect(() => {
localStorage.setItem("userInfo", JSON.stringify(state))
}, [state])
}
return (
<AuthStateContext.Provider value={[state, dispatch]}>
{children}
</AuthStateContext.Provider>
)
}
This AuthProvider
function is called on each re-render of the page, which in turn also updates the localStorage in the browser using useEffect()
. The useEffect()
function runs as a kind of “fire and forget” function after the component is updated, which is perfect for storing these values.
In the same update, the localStorage is used to populate the Context, meaning that on page refresh, we’ll always be getting the most recent value from the localStorage injected right in to our global state.
Other approaches to achieve the same result - Higher order components?
My initial version of this repo mentioned earlier in the post approached the authentication and global state in a different way. I initially used a huge class based component for the AuthProvider
and manually handled the updating of the values with the class component state. The new solution in this post uses the power of the reducer pattern instead.
The initial version also used a different way of passing the data down the component tree, by using Higher Order Components. While I still love the HOC pattern, it can often be a bit less clean, and could be seen to add an unnecessary additional layer of abstraction in to a component. I previously decided to pass the Context down to the HOC, and use the HOC to distribute the data down to the components by wrapping each component in the HOC. The new solution by using the Context API directly keeps the code a lot cleaner, in my opinion, and could also lead to easier debugging in a larger, production ready application.
Finally, the previous version contained logic for checking and validating the JWT in the getInitialProps()
of the _app.js
file - in order words, on each page load. The Next JS team have said on their docs that performing checks here can have a perf impact on the loading of the SSR aspect of the app, so I’ve moved all that logic in to the getInitialProps()
of each protected page. The new solution means having to manually add the check in each protected page’s getInitialProps()
, but there’s now no need to wrap each page in a HOC, so… swings and roundabouts!
Final thoughts
That’s it for this post, I hope you found it useful. If you’re stuck on anything in Next JS, it’s likely there will be a solution in the examples section of the GutHub repo. If you have any questions please get in touch!
Senior Engineer at Haven