Frontend security is a field in web development that is rarely talked about by developers although a majority of web applications have one or more security patterns they use.
One niche in this field is authentication and there are various authentication patterns out there. From the oldest and most common one, password authentication to passwordless auth, single sign-on (SSO) auth, session-cookie auth, token-based auth, HTTP Basic Authentication, etc.
Here's how authentication works in general;
The user sends a request through the client — a web browser mostly — to a resource on the server, say a protected route like the web app's dashboard
If the user did not provide the credentials that allow them access to such resource, the server responds with an "unauthorized" error with the 401 status code. This pattern is common in the HTTP Basic Authentication pattern where you'd find developers passing an authorization parameter in the request header like so:
In scenarios where this pattern is absent, the user is required to input their credentials (email or username and password) for them to be authenticated.
When the user signs in successfully, a common pattern used by developers is assigning an authentication token that can be used for subsequent requests — with the Authorization parameter aforementioned — to the server in the app.
If the information supplied by the user matches what's on the server, access is granted to the user. If not, it returns with the 401: Unauthorized
status code.
There are tons of authentication patterns out there. But, the scope of this article focuses on this basic approach with Next.js' getServerSideProps
and cookies to preserve the authentication state.
Managing form and authentication state
The first time I tried implementing auth with Next.js I went the crude way. Talk about input validation with document.querySelector
, client-side auth-state with createContext
— which in turn led to a flash (around 500ms or so) of protected resources in the dashboard because the redirect was done on the client-side.
The last project I worked on — more like what I'm currently working on — sort of necessitated me to use a tool like Formik to handle the state of the forms, I highly recommend you give it a try.
Since Yup was recommended in the Formik docs for validation, I resorted to it as my go-to approach for validating user inputs. Although, I did not go with the approach that extended some form components of Formik for the sake of "reducing boilerplate" code.
I yearned for flexibility and the ability to be able to customize the little interactions resulting from the actions carried out by whoever interacts with the form's interface.
If you happen to be reading this article, I want to assume that you're not using an auth provider like next-auth, rather, you probably have an API endpoint that you're required to send a POST
request to — perhaps to sign a user up or sign them in to use your app with their credentials.
That will be the basis of this experience that I'm trying to walk you through.
Because, various security patterns concern client-side authentication, of the many approaches towards preserving auth state, the use of browser cookies seems to be the most secure way to keep the JSON Web Token (JWT) that is assigned to a user when they sign in.
I knew that interacting with the browser cookie — with document.cookie
— as means of storing the token may be futile since that will be more or less the first time I'll be doing so. After searching for a while, I found a package that seemed suitable with Next.js — nookies
It is a collection of cookie helper function for Next.js with SSR support. I can use it to store a cookie — setCookie()
, read the content of a cookie — parseCookies()
and delete a cookie — destroyCookie()
. This comes in handy when you want to implement a logout feature.
Setting up the form
The previous section was more of a primer on how the use of Formik and nookies makes the process a bit easier.
Ideally, one would use the state hook in react — useState()
— to track user inputs and handle the submit process. But, with Formik, you might not need to worry about that, and validation becomes a bit less strenuous with Yup.
Let's create a file that we can always fall back to in the course of this article. /utils/token.js|ts
will hold the token variable that we can always refer to.
The snippet above assumes that you have used setCookie()
— which you'll see in a short while — to store the token in a browser cookie with the identifier name, "auth-token"
. If you choose to give it a different name, then, token
becomes:
Now, that we've established how we'd access the token above. It is time to use it in the form.
In the snippet above, you'll see that the appropriate packages that this form depends on, have been imported. One important snippet to note is what's going on in the useEffect
hook.
If you look closely, you'll see that there's a condition that checks if the authentication token is present in the browser's cookie. If it is, the user is redirected to the dashboard.
Although this approach is completely okay for routes — like /signin
, or /signup
— that do not have protected resources like the dashboard, that flash of the signin page will come up.
Sometimes it may take a while, depending on your internet connectivity before you get redirected. But, if we're to consider a route that has protected resources — like the dashboard, it'll be important to consider implementing this on the server with getSetverSideProps
You can jump to the section that covers protected routes with getServerSideProps if that's what interests you.
In the snippet again, you'll notice how we're declaring some state variables that hold the error or success state of your request to the API endpoint. This is crucial for error handling.
Now, unto the part that may seem confusing; You may have noticed some: {...formik.getFieldProps}
and formik.errors.password
expressions flying around and wondered: "What the hell is going on here?".
Hold on a bit, let's walk through it together.
Remember how I mentioned something related to reducing boilerplate code and not wanting to go with that approach because I wanted to be able to customize the experience a bit?
So, this is it. In the snippet below, we'll walk through it.
The logic above depends solely on the useFormik
hook, you'll also notice how we're using Yup for validation. But, before we get into so many details, remember formik.errors
and formik.touched
?
They are methods extended from the formik hook that lets us track the error state of the form. But, one unique thing about Formik is how we can use formik.touched
to only render an error message for an input field that the user has touched or visited, i.e. a field that the user is currently typing in.
The validationSchema
with Yup only ensures that the email field is required and the password field accepts nothing less than six characters. If you want to implement a validationSchema
that handles a scenario where people have to confirm their password, the snippet below would suffice.
The validationSchema
above assumes that you have an email, username, password, and confirmPassword fields in your form. The important part of the schema is where we're using Yup.string().oneOf([Yup.ref("password")])
It simply means you need to have an input field that has a ref (attribute) value of "password" like so:
Now, onto {...formik.getFieldProps}
; The general way to use formik in a React form, would resemble the one in the snippet below, where you have to pass in some attributes like onChange
, onBlur
and value
to the element.
But, with {...getFieldProps}
we get to reduce the number of things we append to the input element, and still obtain the appropriate information of a particular form field. You can read more about it here
The onSubmit
method is where you'd place the signin logic of your form. In the snippet below, we're making a request to an API endpoint called /login
. Although that is not the exact URL, with Next.js' API routes, I can mask API endpoints in production.
From the snippet above, you'll see how we obtained the information from the request by destructuring them. Your API response may be entirely different, but, one thing you be on the lookout for is the auth token variable.
Because, that's what you'll use in preserving the authentication state of the user by storing it in a browser cookie with nookies, as seen in the snippet below. If the request is ok, with a status code of 200
or 201
Remember how we established that the token
variable should have "auth-token"
identifier in the browser cookie? Well, the setCookie
function from nookies helps us achieve that.
In it, we have the path
property that helps us ensure that the cookie remains active on some specific route. In our case here, it should work across all the routes. The maxAge
property specifies how long the cookie stays before it becomes invalid.
One last thing to note is how we're using router.push
to redirect the user to the dashboard. By setting the shallow
property to be true, we change the url of the page and redirect there without invoking any data-fetching methods or processes again.
Remember to call the handleSubmit
and isSubmitting
methods on the <form>
and <button>
elements to make the request to the endpoint and selectively disable the button when no info has been filled into the form, respectively.
And, in the input elements, whenever errors
and touched
conditions are met, a shake
class is added to the input field which shakes the input field and sort of draws the attention of the user to the field where there's an error
If you're using a UI library like ChakraUI you can customize your button component to appear intuitive and provide a good UX for the person using your form. One way is to show them a loading state like so:
Then in the form, you'll proceed to use it like so:
Protected routes with getServerSideProps
One of the benefits of using nookies is that you get to be able to parse the information in a cookie from the server side too. And, with that, you can implement a protected route without worrying about that flash of protected resource(s).
Here's what the /page/dashboard/index.tsx
file will appear like:
If you look closely, you'll notice that the way we're using nookies here is a bit different. Instead of the normal way, we're accessing the info from the req: Request
context of Next.js' GetServerSidePropsContext
In the return props of getServerSideProps
, if you don't happen to have any data fetching going on at the server-side, you can return an empty object like so:
Note: This is only needed if you're writing Typescript.
The use of GetServerSidePropsContext
was necessary because the type annotation of context
was any
and to get rid of that warning, I employed GetServerSidePropsContext
Wrapping up
Although, this may just appear as a walkthrough of auth in a Next.js app. You can decide on whichever pattern feels convenient enough for you.
If you want to implement a logout feature, make sure you pass the exact cookie information that you used while setting up the signup process. Something like the snippet below should suffice. But, feel free to improve it.
One more thing I'd suggest is the addition of a logic that helps you check when the cookie has expired, if it has, you show a popup or modal that has a higher z-index
than the page content with information on what's going on.
A "Your session has expired. You might want to log in again" text may be suitable in this case. Try this out when you get to this point, and tweet at me @kafLamed when you find a solution.
And, that's it — for now.
If you've followed this guide up to this point, you might want to check this guide that walks you through the process of persiting authentication state and choosing the right cookie wrappers.