- Published on
User authentication with NextJS and SSR
- Authors
- Name
- Danilo Gačević
- @danilothedev
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
- Browser sends a GET request to that URL
- Server receives the request
- Request is routed to the specific page handler by nextjs(In this case, that would we file in pages/index.js)
- 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