danilodev
Published on

User authentication with NextJS and SSR

Authors

First of all, what is SSR(server side rendering)?

"Server-side rendering with JavaScript libraries like React is where the server returns a ready to render HTML page and the JS scripts required to make the page interactive." - source

SSR has numerous benefits, such as better SEO, better site performance, better TTI, FCP, and more.

During SSR, you can query your or third-party APIs to get the data you need, and then you can inject that data into your components. This process is pretty straightforward until you get to point where you need to show some data based on the user requesting the page.

In this post, I will try to explain the process of authenticating the user while using SSR with NextJS, Prisma, and cookies.

General flow

Let's break down what is happening when someone accesses your page www.yourdomain.com in the browser

  1. Browser sends a GET request to that URL
  2. Server receives the request
  3. Request is routed to the specific page handler by nextjs(In this case, that would we file in pages/index.js)
  4. Page handler executes getServerSideProps, injects the data into the routed page, and returns the HTML output.

We must understand that the browser automatically sends this first request in the flow. There is no way of executing any javascript before that request or modifying any headers sent by the browser. Because of this, we need to use cookies as a mechanism to transfer access token for the user, as they are automatically attached to every request sent by the browser.

Use case

Use-case for this post will be a news feed with two types of users: guest users and registered users. Registered users are then divided into two groups: subscribed and not subscribed. We will have three pages:

  • Homepage - This is where all the posts will be listed.
  • Login page - This is a page where the user can log in.
  • News page - This is a page where users can see more details about a specific post

And on the backend we will have following routes

  • GET /api/posts - This is a route that will return all the posts
  • POST /api/users/login - This is a route that will create a JWT token upon successful login
  • DELETE /api/users/logout - This is a route that will clear user's cookie

Getting our hands dirty :)

As a database client we will use prisma.

Let's use SQLite as DB provider as it's most simple to get up and running.

Data about registered users will be saved in User table, and data about posts will be saved in Post table.

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}
generator client {
  provider = "prisma-client-js"
}


model User {
  id        String   @id @default(uuid())
  email     String?  @unique
  password  String
  subscribed Boolean? @default(false)
}

model Post {
  id        String   @id @default(uuid())
  title     String
  content   String
  premium   Boolean? @default(false)
}

In prisma/seeds there is a script that can seed the test data for you

Breaking down the login route:

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  // only allow POST requests
  handleRequestMethod(req, `POST`)
  // get email and password from the request body
  const { email, password } = req.body

  // find the user with the email
  const user = await prisma.user.findFirst({ where: { email } })

  // compare the password with the hashed password
  const isValid = await bcrypt.compare(password, user.password)
  if (!user || !isValid) {
    return res.status(401).json({ error: `Invalid credentials` })
  }
  // create jwt token
  const token = jwt.sign({ sub: user.id }, process.env.JWT_SECRET, {
    expiresIn: `7d`,
  })
  // attach created token as cookie to the response header
  return res
    .setHeader(`Set-Cookie`, serialize(`token`, token, { path: `/` }))
    .status(200)
    .json(user)
}

Let's create a helper function inside the lib folder. This function will verify the user's token and return the user object from DB if the token is valid or throw an error in case the token is invalid.

export const authenticate = (req) => {
  // get token from cookies
  const token = req.cookies.token
  return new Promise((resolve, reject) => {
    jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
      if (err) {
        reject(new Error(`Unauthorized`))
      } else {
        prisma.user
          .findUnique({
            where: { id: decoded.sub },
            select: {
              id: true,
              email: true,
              subscribed: true,
            },
          })
          .then((user) => {
            if (user) {
              return resolve(user)
            } else {
              reject(new Error(`Unauthorized`))
            }
          })
          .catch(reject)
      }
    })
  })
}

Now lets go over to the home page.

Inside src/pages/index.tsx we have our React Home component and getServerSideProps function. getServerSideProps is executed everytime someone makes a request for that page and result is passed to the Home component via props. In there we try to authenticate the current user from the request by looking at the JWT token from cookies. If we manage to authenticate the user, we first check to see if the user is subscribed or not to know if he should be able to see all posts or just free ones. If we don't manage to authenticate the user, we return free posts for guest users.

Also, along with the data about posts, getServerSideProps returns user object so that we can display UI specific to that user e.g., show user email/subscription status)

export const getServerSideProps: GetServerSideProps = async ({ req }) => {
  let posts = []
  let authUser = null
  try {
    const user = await authenticate(req)
    authUser = user
    if (user.subscribed) {
      posts = await getAllPosts()
    } else {
      posts = await getFreePosts()
    }
  } catch (e) {
    posts = await getFreePosts()
  }

  return {
    props: {
      posts,
      user: authUser,
    },
  }
}

The logic is similar for the Posts page. By looking at the JWT token from the cookies, authenticate the user and query the DB to get the specific post.

How to log out the user?

To log out the user, we simply need to send a DELETE request to /api/users/logout endpoint, and refresh the page after that. The cookie will be deleted and server will execute the page handler again, now without the cookie.

Logout route implementation:

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  handleRequestMethod(req, `DELETE`)
  return res
    .setHeader(`Set-Cookie`, serialize(`token`, ``, { path: `/`, maxAge: 0 }))
    .status(200)
    .json({ message: `Logged out` })
}

Notice that this code is not production-ready. If you are looking for a more out-of-the-box solution, you should look at NextAuth.

Source code for this project is available on Github