DEV Community

Ivan V.
Ivan V.

Posted on • Updated on

Protecting static pages in Next.js application

In this article, I will explain how to structure your Next.js application so you can protect your static pages from unauthenticated access.
I will concentrate on the implementation logic and authentication flow, and not on any particular authentication provider, so we won't be dealing with client-side tokens or cookies (which I will probably do in a separate blog post).

You can interact with the example
Or you can check out the repository

Authentication Setup

We will create a couple of pages:

  • public pages accessible to everyone
  • protected pages where the user if not authenticated, will be automatically redirected to a signin page.
  • signin page with a simple sign-in form.
  • signout the page where the user will be redirected to after successfully sign out.
src/pages/
├── _app.tsx
├── index.tsx
├── protected.tsx
├── protected-two.tsx
├── public.tsx
├── signin.tsx
└── signout.tsx
Enter fullscreen mode Exit fullscreen mode

Sign In Flow

It goes like this when the user lands on a public page (or index) those pages will immediately render. If the user lands on a protected page while not signed in, she will be redirected to the sign in page, and after the successful authorization, she will be redirected back to the protected page where she started.

There are two authentication components AuthProvider and AuthGuard, they work in tandem to provide all the logic needed for protecting the pages.

AuthProvider Component

AuthProvider will utilize React context and as I said, we will not concentrate on a particular authentication provider (Firebase, Auth0, AWS Cognito, etc.) rather we will have an abstract demo provider that will expose a hook useAuth that will return a couple of properties, were most important ones are a user object (with all the user data you need) and an initializing property to indicate if the AuthProvider is in the process of initializing i.e. trying to determine if the user is signed in or not ( in case of third party providers it would contact authentication provider service to validate currently present tokens or cookies).

In general you should always abstract any third-party providers that you use in your application so you can always replace them if the need arises with as little refactoring as possible.

So AuthProvider component will be the source of truth for all user authentication data and it will wrap the whole application (it will live in _app.tsx) so we can access user data from anywhere in the application (both on protected and public pages).

AuthGuard Component

AuthGuard component is the second part of the authentication setup. It wraps every page that needs to be protected, and it holds the logic to determine what to do depending if the user is authenticated or not.

Since it is wrapped by AuthProvider (the whole application is) it has access to the user data and it will not render a protected page if the user does not exist rather, it will remember what page the user tried to access while not being authenticated (so it can redirect the user back to that same page after a successful sign-in) and immediately redirect to the signin page. Then after the user sign in (on the signin page) she will be redirected back to where she started.

It is important to note that AuthGuard in this current implementation is redirecting to the sign-in page, but it is very easy to change the component to do something else, for example, to show a sign in form immediately (on the protected page) I just think the best UX is to redirect to a special page that handles the sign-in process.

// AuthGuard.tsx
import { useAuth } from "components/AuthProvider"
import { useRouter } from "next/router"
import { useEffect } from "react"

export function AuthGuard({ children }: { children: JSX.Element }) {
  const { user, initializing, setRedirect } = useAuth()
  const router = useRouter()

  useEffect(() => {
    if (!initializing) {
      //auth is initialized and there is no user
      if (!user) {
        // remember the page that user tried to access
        setRedirect(router.route)
        // redirect
        router.push("/signin")
      }
    }
  }, [initializing, router, user, setRedirect])

  /* show loading indicator while the auth provider is still initializing */
  if (initializing) {
    return <h1>Application Loading</h1>
  }

  // if auth initialized with a valid user show protected page
  if (!initializing && user) {
    return <>{children}</>
  }

  /* otherwise don't return anything, will do a redirect from useEffect */
  return null
}


Enter fullscreen mode Exit fullscreen mode

Creating Protected Pages

It is important to note that protected pages have no logic regarding the protection process. So in our case, it is just a regular Next.js page. If the page component is rendered it means that the user exists and is authorized to access the page. Remember, since that page is wrapped inside the AuthGuard and AuthProvider, all that logic is already resolved by the time the page is rendered.
So by now, you might be asking "How AuthGuard knows what pages to protect?" Good question. The answer is very simple, every Nextjs component has a special property requireAuth set to true.

import { PageLinks } from "components/PageLinks"

export default function Protected() {
  return (
    <div>
      <h1>Protected Page One</h1>
      <PageLinks />
    </div>
  )
}

Protected.requireAuth = true
Enter fullscreen mode Exit fullscreen mode

Then, inside the _app.tsx we check if the component has the requireAuth property and if true we wrap it with the AuthGuard component.

// _app.tsx

export default function MyApp(props: AppProps) {
  const {
    Component,
    pageProps,
  }: { Component: NextApplicationPage; pageProps: any } = props

  return (
   <AuthProvider>
     {/* if requireAuth property is present - protect the page */}
     {Component.requireAuth ? (
       <AuthGuard>
         <Component {...pageProps} />
       </AuthGuard>
     ) : (
       // public page
       <Component {...pageProps} />
     )}
   </AuthProvider>
  )
}

Enter fullscreen mode Exit fullscreen mode

And that's all it takes to have protected and public static pages in Next.js.

But remember, the only source of truth for all your application data is the server, so even though the pages will not render for unauthorized users, your backend server should always check for authorization before doing any CRUD.

You can interact with the example
Or you can check out the repository

Top comments (13)

Collapse
 
prajwalkulkarni profile image
Prajwal Kulkarni

Great writeup. However, I've tweaked your code a bit to fit my needs(using redux-state) instead of context, and I'm facing an issue. I'm using getStaticProps on a dynamic route which is a protected route, wrapped within AuthGuard. When I hit the protected url, I'm successfully redirected to the Signin page if I'm not authenticated, but when I inspect the network tab, I can see that the contents of the protected page are downloaded on the client (as JSON). Also, If I'm authenticated, I'm unable to reach the protected routes. Any suggestions on how do I go about this? Thanks.

Collapse
 
ivandotv profile image
Ivan V.

Since pages are static they are always downloaded, there is no way around this except to have server-rendered pages where you check if the user is authenticated server-side, and respond accordingly.
You should not have your static pages contain any hardcoded data that is security-sensitive, rather you should load all sensitive data when the user is authenticated (by sending fetch requests to load sensitive data)
A great example of this is the Vercel dashboard.

Collapse
 
prajwalkulkarni profile image
Prajwal Kulkarni

The static pages are used for a dynamic route, meaning there could be multiple entries that would reach this page, NextJS is forcing me to use SSG using getStaticProps and getStaticPaths , and hence not able to do much after the component mounts. Regarding the data in the page, it's the data that belongs to the user, so it's pretty sensitive. Given the scenario, is server-side authentication check the only workaround?

Thread Thread
 
ivandotv profile image
Ivan V.

How are you getting the data for the user? There is no request/response objects in getStaticPaths

Thread Thread
 
prajwalkulkarni profile image
Prajwal Kulkarni

Currently in development phase, so as of now, I'm just testing with a dummy static data within GSP, but when the database and the backend is ready, I'm planning to replace the dummy data with fetch pointing to an API endpoint.

Collapse
 
snakepy profile image
Fabio

Hey I am currently trying to implement this into next 13.4 and I have troubles implementing the part where I would use Component.requireAuth, because in the new NextJs set up the component is not injected into the global layout. Any idea on how this could be refactored?

Collapse
 
alonl profile image
Alon Lavi

Thanks for this post!
Just a note for people who use Auth0 - you can use withPageAuthRequired from the nextjs-auth0 library.

Collapse
 
matd1 profile image
MatD1

This is awesome, Is there any chance this can be changed to js, I have never used typescript and I am unsure how to convert the two. Sorry this may seem silly but I am rather new to this stuff

Collapse
 
ivandotv profile image
Ivan V.

Just convert typescript files to javascript file and it will work :)

Collapse
 
ramirezsandin profile image
Jorge

Nice article. How much better would it be to use this approach instead of a hoc component and wrap the exported page that you want to protect?

Collapse
 
ivandotv profile image
Ivan V. • Edited

I don't think it would be any better, there is no added functionality if you go that way, I also think that the code would look more convoluted.

Collapse
 
cn_d_ profile image
Cal

Nice pattern, thanks for writing this up. I just noticed that in the linked example, the browsers back button no longer works when going onto a protected page but not logging in.

Collapse
 
ivandotv profile image
Ivan V.

It works, but the router redirects you back to sign in immediately ( you can see it in the URL bar for a brief moment). That is a UX problem. I have chosen to redirect to sign when a non-authenticated user lands on the protected page immediately. You could instead of a redirect, show the button to login (on the protected page)