Concepts covered: Next.js, Node.js, JWT, Cookies, React HOC
Authentication is a challenge, and implementing it from server-side rendered (SSR) applications adds some additional twists!
For this example, we’ll have a login screen that redirects to a home page when a user is authenticated. If you would like to follow along, the file structure and npm dependencies are on the left. This article assumes you are familiar with Express route handling and the axios library.
Take a look at the flowchart below which shows when endpoints in the Node layer are called, when a cookie is set, and when the higher order component is rendered.
Logging in and Generating a JWT
Let’s take a look at a handleSubmit
function when a user enters their email and password:
We create a user
object that will be added as options to the axios.post
request. If res.status
comes back 200, we will redirect the user to the home page via window.location = '/home'
. If there is an error we display the error message. So how does the user get authenticated in between? As you can see in the flowchart above, the axios request to /signin
is first routed to the routes.js
file. There are only 2 route handlers for this example:
Inside the signIn
function is where we generate a JSON Web Token and set it as a cookie in the browser. The first step is creating another user
object to be sent back in the response on a successful login. Since we are not actually fetching a user from a database, I am defaulting the status to 200 so the login will succeed every time.
Below the user
object, we create the important first step in our authorization setup — the JSON web token! Install the jsonwebtoken
module and require it in the file. We can call the jwt.sign()
method and pass it 3 arguments: The first is the payload (our user object), the second is a secret key used to decode/authenticate the token, and the third is an optional expiry date for the token. All of this is information is stored inside a variable called token
.
Tip: Since you never want to make the secret key public, as that would defeat the purpose of the token, it is common to store it as an environment variable in a .env
file.
Next up is the try/catch statement, in which we use res.cookie()
to set our web token as a cookie called userToken
. On a successful login the cookie will be set and the user
object sent back in the response. With that in place, we can finally move on to the grittier parts of SSR authentication.
Higher Order Components, Middleware, and getInitialProps
At this point, let’s take a step back and look at how Next.js handles our requests after a successful login and the user is redirected to /home.js
. There are 2 places for authentication to be implemented here. One is to prevent users from accessing a private page in the browser, and the other is to prevent users from accessing Express endpoints with tools like Postman.
The first case mentioned can be addressed by creating a Higher Order Component that handles authenticating the JSON web token. Every page in your application that you want locked down (like our home page) can be wrapped by this HOC. Pretty efficient, right? The second case of authentication happens with middleware just before the Home
component renders. Middleware gives access to the request
and response
objects and allows for code to be executed in between the request/response cycle. We can create some authentication middleware that will run when the home page makes an initial GET request to /helloUser
.
SSR Alert! Both of these cases make use of getInitialProps
. This is a Next.js method for injecting props into a component such that the component is rendered with data already populated from the server.
First let’s look at the higher order component, authHOC
:
The gist of what RequireAuthentication
is doing is returning a React component with getInitialProps
, validating the cookie and, if valid, returning the wrapped component’s getInitialProps
function and rendering the Home
component. Here’s a more detailed explanation…
If there is a cookie called userToken
, its value is taken from ctx.req.headers.cookie
and stored in the token
variable. Then, within the try/catch statement, we can use the jwt.verify()
method to verify our session token by passing it the token
variable and our secret key. If you’re curious what’s returned from jwt.verify()
, it is the user
object created in signin.js
! The function then returns the child component’s getInitialProps
with context. If the token is not verified, isAuthenticated
is undefined, which will redirect the user back to the login page.
By wrapping all of our protected pages with this HOC, the user’s session token from cookies is validated before rendering the protected route. Now with a verified token, let’s look at the Home.js
page:
The Home
component has its own getInitialProps
function that makes a GET request to /api/helloUser
and returns a greeting
prop. Notice after the url, we include an authReqHeader(ctx)
function with the request. This function simply takes our userToken
cookie and converts it to a conventional authorization token beginning with 'Bearer ‘
:
With that in place, the GET request is routed to the routes.js
file. This is where we pass our auth
middleware. auth
protects our app from that second case of some unauthorized person trying to get access to your Express endpoints with a tool like Postman.
The authentication looks very similar to what was done in the higher order component. Inside the try/catch statement, we create a token
variable that holds the value of the bearer token in the request. Once again, jwt.verify()
verifies token
against the secret key, and stores the value in decoded
.
If decoded
does contain a user object, we can send decoded.user
along in the request headers. If the token cannot be verified, our catch statement will send an error message to the user, and the next()
method will not be called.
Tip: Do not forget to call next()
at the end of your middleware function, or else the next middleware in line will not run!
With the auth
middleware successfully passing, the request to /api/helloUser
simply grabs some data off of that user
object and sends it back with the greeting
prop in the response:
And your Next.js authentication has been implemented! The Home
component now has access to the user’s email along with the greeting.
What just happened?
There is a lot going on with JWT authentication. Server rendered applications add an extra layer of complexity since we have to keep in mind what client-side functionality isn’t available to us (like localStorage
).
To summarize: Our application was authenticated at 2 places — once in the higher order component, and once in the auth
middleware.
- Upon logging in, a JSON Web Token is created, set as a cookie, and returned in the response along with a user object.
- When the user is redirected to the home page, they are first authenticated in the higher order component. Once authenticated, the HOC renders the
Home
component. - The
Home
component usesgetInitialProps
to retrieve some data via a GET request. This is when the component is authenticated a second time with middleware.