Authentication and Authorization with SSR in Next.js
Authentication and authorization are two basic security requirements, and while there are many ways to go about them, this article will show a modern way, using SSR (Server Side Rendering) in a React app built with Next.js
Spot abnormal user behaviors and iron out the bugs early with OpenReplay. Dive into session replays and reinforce your front-end against vulnerabilities that hackers search for.
Discover how at OpenReplay.com.
How to Handle Authentication and Authorization in Next.js with Server-Side Rendering
The importance of authentication and authorization cannot be overemphasized in every web application. Authentication and authorization is an integral part of a development process. Authentication is verifying a user’s identity; it simply checks if a user is who they claim to be. A typical example of authentication is using an ID to buy age-restricted items in a store. There are various methods of authenticating your users in web applications, including usernames and passwords, social logins, and token-based authentication. Authorization works hand in hand with authentication, while authentication verifies who a user is, authorization determines what a user can access or do with the application. Authentication and authorization ensure that users’ data are secure and private and get personalized experiences tailored to their accounts. In this article, we will learn how to handle authentication and authorization in Next.js with server-side rendering.
Authentication Approaches in Next.js
Next.js is a popular framework for building Single-Page Applications (SPAs). It primarily offers two approaches to authentication: client-side authentication and server-side authentication.
Client-Side Authentication
In client-side authentication, user credentials and logic are handled entirely in the browser. Typically, when this process happens, a loading indicator shows the user that a process is running in the background; this enhances the user experience in your application. It is simply the process of verifying a user’s identity within the client-side application without needing server-side interaction during the initial authentication step.
Server-Side Authentication
Server-side authentication is the process of verifying users’ identities on the server side of an application. In this approach, authentication logic is implemented and executed on the server, which is responsible for validating the credentials provided by the client and granting access accordingly.
Implementing Authentication in Next.js with NextAuth.js
NextAuth.js is an open-source library explicitly designed for authentication in Next.js applications. It offers a secure and flexible way to handle user logins, signups, and authorization within your Next.js project. To start with NextAuth.js
, we first need to create a next.js
project by running the following command on your terminal.
npx create-next-app@latest my-nextjs-app
After successfully creating the Next.js project, we have to install NextAuth.js in our project. We can install it by running the following command on our terminal
npm install next-auth
Adding Authentication Provider
NextAuth.js provides various ways of authentication; in our app, we will be using a built-in OAuth provider, Github OAuth. Github OAuth allows users to sign in to your application using a GitHub account. First, we need to create a Github OAuth app. Click on Register a new app and fill out the form.
Application name: This is simply your application’s name; it should be descriptive and able to identify your application to both users and GitHub.
- Homepage URL: The homepage URL is the full URL to the homepage of our app; since we are still in development mode, it should be
http://localhost:3000
. - Authorization callback URL: This is the URL where GitHub will redirect users after they have authorized your application. The URL should be
http://localhost:3000/api/auth/callback
, your homepage, plus/api/auth/callback
. After you fill in the fields, Github will provide you with aClient ID
and aClient Secret.
Configure NextAuth.js.
To add nextauth.js
to your project, create a [...nextauth].js
file in your pages/api/auth
directory and add the following code:
import NextAuth from "next-auth";
import GitHubProvider from "next-auth/providers/github";
export default NextAuth({
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
secret: process.env.NEXTAUTH_SECRET,
});
In the code above, we configured nextauth.js
with the GitHub provider. The clientId
and client secret are obtained from the GitHub OAuth app we registered earlier. The JWTs (JSON WEB TOKENS) are used by NextAuth.js to securely manage user sessions and authorization.
When a user successfully signs in using GitHub, NextAuth.js creates a JWT
containing user information; however, this dynamically generated token won’t be persistent across server restarts. This simply means that whenever your server restarts, a newly generated token is made, thereby invalidating all your signed-in tokens and sessions. Therefore, using your own NEXTAUTH_SECRET
variable is highly recommended for consistent session and token management in production environments.
To generate your own NEXTAUTH_SECRET
, enter the following command on your terminal. openssl rand -base64 32
. Once this is done, create a .env.local
file in the root folder of your application and store the environment variables. This ensures you don’t expose your environment’s secrets.
GITHUB_ID=<!-- Your client ID -->
GITHUB_SECRET=<!-- Your client secret -->
NEXTAUTH_URL = <!-- http://localhost:3000 -->
NEXTAUTH_SECRET=<!-- your generated JWT secret -->
Configure shared state
We have to configure the state at the top level of our application. To do this, create a provider.js
file in your app/provider.folder
with the following code:
"use client";
import { SessionProvider } from "next-auth/react";
export default function Providers({ children }) {
return <SessionProvider>{children}</SessionProvider>;
}
And then wrap the provider
component in your layout.js
file:
import type, { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Link from "next/link";
import Providers from "./providers";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
}
In the code above, the <SessionProvider/>
component from nextauth, provides session data across all components in our application. It takes a session
object as a prop and makes it available to all components in our application. This session object contains information about the authenticated user, such as their ID, email, name, and other relevant user data.
With the help of the usesession
hook, we can now access the section data and status within any component. The useSession
hook allows us to check if a user is authenticated and accesses their information. To access the session data, create a login.js
file in your app folder and copy the following code:
"use client";
import { useSession, signIn, signOut } from "next-auth/react";
export default function Login({ children }) {
const { data: session } = useSession();
return session ? (
<>
<div className="flex flex-col items-center justify-center h-screen">
<div className="text-center">
<p className="mb-4">Welcome, {session.user.name}!</p>
<img
src={session.user.image}
className="w-24 h-24 rounded-full mx-auto mb-4"
/>
<button
onClick={() => signOut()}
className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Sign out
</button>
</div>
</div>
</>
) : (
<>
<div className="text-center">
<p className="mb-4">You need to sign in to access this page</p>
<button
onClick={() => signIn("github")}
className="bg-gray-800 hover:bg-gray-900 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Sign in with GitHub
</button>
</div>
</>
);
}
We used the useSession
hook to access the user’s data. The sign in
hook redirects a user to Github, while the sign-out hook clears the user’s authentication and logs the user out. If you have successfully reached this stage, you should be able to log in with your GitHub account.
After clicking the button, GitHub will ask you to authorize your application, and when you do, you will see the image below:
Authorization in Next.js With SSR
Role-based access control (RBAC) is a security strategy that restricts access to features and functionalities within your application based on a user’s assigned role. This ensures that only authorized users can perform specific actions or view sensitive information. An example of (RABC) is an employee management application. HRs can access all employees’ data, while employees can access their personal information. One primary benefit of RBAC is the improved data security it provides.
Role-Based Authorization with Next.js Middleware
In Next.js, middleware refers to a function that runs before handling a request. It allows you to intercept and manipulate incoming HTTP requests and outgoing responses.
Middleware allows you to run code before a request is completed. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly- Nextjs official doc
Middleware functions are commonly used for authentication, authorization, logging, error handling, and data parsing. Here, we will focus on using middleware to authorize our users and control their access to specific parts of our app. Now, let’s add middleware to our application that can restrict access to some specific users. First, we need to create some pages. In your app
folder, create an admin
folder, inside it a page.js
file, and add the following code to it.
export default function Admin() {
return (
<div className="items-center flex flex-col justify-center mb-4">
This is the Admin page!
</div>
);
}
Also, do the same for the profile
route:
export default function Profile() {
return (
<div className="items-center flex flex-col justify-center mb-4">
This is the Profile page!
</div>
);
}
Next is to define middleware in our application, create a middleware.js
file in the root directory of our project, and add the following code:
import { withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";
export default withAuth(
function middleware(req) {
if (
req.nextUrl.pathname === "/admin" &&
req.nextauth.token?.role !== "admin"
) {
return new NextResponse(
"Permission Denied⚠️⚠️⚠️ , this page can only be viewed by an admin user !"
);
}
},
{
callbacks: {
authorized: (params) => {
let { token } = params;
return !!token;
},
},
}
);
export const config = { matcher: ["/admin", "/profile"] };
If you look closely at the code above, we imported withAuth
from the next-auth/middleware
package and NextResponse
from next/server
used for handling authentication and HTTP response in Nextjs. Within the withAuth function, we have a middleware function that takes a req
as a parameter. It checks if the pathname of the requested URL is /admin
, and if the user’s role obtained from the authentication token is not admin
, it returns a response with a message indicating permission denial. It also takes a configuration object as its second argument.
This object contains an authorized callback function that determines whether a user is authorized based on the parameters it receives, particularly the authentication token
. It returns true if the token
exists, indicating that the user is authorized.Next is to update our [...nextauth].js
file with the following callback function to text our authorization logic.
import NextAuth from "next-auth";
import GitHubProvider from "next-auth/providers/github";
export default NextAuth({
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
callbacks: {
async jwt({ token }) {
token.role = "member";
return token;
},
},
});
In the code above, we defined our jwt
callback function, and we can access the token from the parameters and set the token.role = "member"
to test our code; when a user clicks on the admin
, it should restrict access because it is not an admin user. Finally, we must add the route pages as links for easy accessibility. In your layout.js
file update it with the following code:
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Link from "next/link";
import Providers from "./providers";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>
<nav className="flex space-x-4 px-6 py-4">
<Link href="/">Home</Link>
{` `}
<Link href="/admin">Admin </Link>
{` `}
<Link href="/profile">Profile</Link>
</nav>
{children}
</Providers>
</body>
</html>
);
}
If you have reached this stage, you should be able to log in, and when you click on the admin
, it should show Permission denied because we set the role to member.
Authentication using Credentials Provider
So far, we have done authentication with Github OAuth and authorization with Next.js middleware. Now we need to add authentication using credentials provider, that is, email and password. And for this, we will need an API to handle our login request. In our case, we will be using an external API. To add a credentials provider to our already existing provider, update your pages/api/auth/[...nextauth]
.js file with the following code:
import NextAuth from "next-auth";
import GitHubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";
import { login } from "@/services/apiServices";
export default NextAuth({
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
CredentialsProvider({
credentials: {
email: { label: "Email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
try {
const res = await login(credentials.email, credentials.password);
if (res) {
return {
id: res.id,
name: res.name,
email: res.email,
role: res.role,
};
} else {
return null;
}
} catch (error) {
// Handle any errors
console.error("Authentication error:", error);
return null;
}
},
}),
],
callbacks: {
async jwt({ token }) {
token.role = token.role || "member";
return token;
},
},
});
In the code above, The authorize callback function is defined for CredentialsProvider, which is responsible for authenticating users based on the provided credentials that is email and password. It calls the login function from our apiServices
and if successful, returns an object containing user information (id, name, email, and role). If authentication fails, it returns null.
Next, we must access the session
in the serverside component. To access the session
in a Severside component, in your pages directory, create a file, api/public/page.js
and copy the following code:
import { getServerSession } from "next-auth/next";
import { GetServerSidePropsContext } from "next";
import { login } from "@/services/apiServices";
export default function Protected({ user }) {
return (
<div>
<div>
{user ? (
<h1>Hi {user.name}!</h1>
) : (
<a href="/api/auth/signin">Sign in</a>
)}
</div>
</div>
);
}
export async function getServerSideProps(context) {
const session = await getServerSession(context);
const user = session ? session.user : null;
return {
props: {
user,
},
};
}
In the code above, the getServerSideProps
function fetches the user session on the server side before rendering the page; with getServerSession
, we can get the current user’s session information. If a user session is found, the user object is extracted from the session and passed as props to the Protected
component. If no user session is found, the sign-in link is displayed to prompt the user to authenticate.
Conclusion
Authentication and authorization remain crucial parts of the application development process. NextAuth.js provides various easy ways to integrate authentication into our applications. In this article, we have implemented authentication with NextAuth.js using Github OAuth, a credentials provider, and have also seen how we can use middleware in Nextjs to authorize our users. Thanks for reading this far. Happy coding!!!
Secure Your Front-End: Detect, Fix, and Fortify
Spot abnormal user behaviors and iron out the bugs early with OpenReplay. Dive into session replays and reinforce your front-end against vulnerabilities that hackers search for.