Understanding Next.js
Next.js, a powerful React framework has rapidly gained popularity due to its versatility and ability to build both static and dynamic web applications.
Featuring server-side rendering (SSR) and static site generation (SSG) capabilities, Next.js makes creating and deploying websites easy with its tight integration with Vercel.
This article will cover how Next.js handles rendering and authentication, discuss some authentication strategies, and how to implement an authentication solution in Next.js.
Authentication in Next.js
While the concept of authentication and its strategies are the same for all JavaScript frameworks, including Nextjs, what sets Nextjs and other frameworks like it apart are the patterns available for preventing unauthenticated users from accessing protected routes, commonly referred to as authentication patterns.
To understand what authentication means for Nextjs, you must first understand how Nextjs renders things.
With Nextjs, three types of rendering are available: client-side rendering, server-side rendering, and static site generation.
-
CSR: With client side rendering, a minimal HTML file and the bundled JavaScript code are sent to the browser. The JavaScript is then executed in the browser to render the page.
-
SSR: With server-side rendering, as the name implies the full HTML for the page content is rendered on the server upon a user’s request and is then sent to the browser.
-
SSG: In static site generations the HTML for each page is generated at build time and sent to the browsers at the user’s request.
Authentication vs. Authorization in Next.js
Authentication and authorization are two crucial concepts in application security, but they serve two distinct purposes:
Authentication: Process that verifies the identity of a user, answering the question “Who are you?”. Authentication typically involves logging in with credentials (like username and password), or through a third-party service (like Google, Facebook, GitHub…).
Once authenticated, the user is recognized by the app, allowing access to resources tied to their account.
Authorization: Once the user is authenticated, authorization determines what they are allowed to do (comparable to permissions), answering the question “What action can you perform?”.
Authorization involves checking whether the authenticated user has the necessary permissions to access specific resources or perform certain actions. For example, an authenticated user might be authorized to view their profile, but not authorized to access the admin dashboard.
You can learn more about the differences between the two in our blog.
Nextjs Authentication Stratergies
There are two common authentication stratergies when preventing unauthenticated users from accessing proctected routes in Nextjs. These included static generation and server-side authentication and server-side authentication. It is important to choose the a stratergy that suits your application and requirements.
Static generation pattern
In Next.js, when a page doesn’t contain blocking functions such as getStaticProps
and getInitialProps
, it is automatically prerendered to static HTML. Based on this, for the static generation pattern, when a page is requested, a loading state that has been statically generated is first served, followed by an attempt to fetch a user’s data in the client.
If successful, the requested page is displayed. Otherwise, the user is redirected to the login page and will see errors explaining why the check failed.
This pattern has the disadvantage of displaying secure content to an unauthenticated user.
Server-side pattern
In this pattern, when the browser requests a server-side rendered page, a request is sent to the backend API to get a user session. If successful, the server pre-renders the requested page on the server and sends it to the browser; otherwise, the user is redirected to the login page.
Unlike the static generation pattern, with this pattern, there will be no flash of unauthenticated content (FOUC) or a need to use a loading indicator.
One disadvantage is that the rendering will be blocked until the backend API responds. This is because getServerSideProps is blocking until the request resolves.
Setting up Next.js Authentication with SuperTokens
(Recommended) Use create-supertokens-app
create-supertokens-app
is a command line tool created by the SuperTokens team that lets you create new projects with SuperTokens already integrated. This is the fastest way to get started with SuperTokens. To use this tool, run the following command:
npx create-supertokens-app@latest
The tool will prompt you for your tech stack, you can choose between the app directory and the pages directory.
The rest of this article is relevant only if you are adding SuperTokens to your app manually and if you have not used the tool mentioned above. If you used create-supertokens-app
, the setup is already complete and you can continue building the rest of the application. To know details about Next.js integration with SuperTokens, refer to their official documentation
Manually Setting up the Next.js project
Run the following command:
npx create-next-app <PROJECT_NAME>
create-next-app
will prompt you to select details about your project, for the sake of this example we are:
- Using Typescript
- Using ESLint
- Not using Tailwind CSS
- Not using the
src/
directory - Using the App router
- Not using import aliases
After the tool has finished, you should have a basic Next.js app setup. You can run the project with npm run dev
.
Installing and setting up SuperTokens
Install supertokens-auth-react
and supertokens-node
as node dependencies:
npm i supertokens-node supertokens-auth-react
In this example, we are going to be using the Social login + email password recipe of SuperTokens.
Step 1: Initialise SuperTokens for our APIs
SuperTokens’ node SDK provides a set of APIs that we can use for logging in our users and managing their sessions. We will follow the quick setup guide to initialise the backend SDK. Create a /app/config/appinfo.ts
file and add the following code to it:
export const appInfo = {
appName: 'SuperTokens Next.js demo app',
apiDomain: 'http://localhost:3000',
websiteDomain: 'http://localhost:3000',
apiBasePath: '/api/auth',
websiteBasePath: '/auth',
};
apiDomain
tells SuperTokens where the APIs are exposed fromwebsiteDomain
is the base URL of the frontendapiBasePath
is the path at which the backend SDK exposes all its APIswebsiteBasePath
is the path where the frontend SDK will add its routes
Because Next.js serves both the frontend and backend on the same base URL, it is important to make sure that the apiBasePath
and websiteBasePath
are not the same.
Create a /app/config/backend.ts
file and add the following code to it:
import SuperTokens from "supertokens-node";
import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword";
import Session from "supertokens-node/recipe/session";
import { TypeInput } from "supertokens-node/types";
import { appInfo } from "./appinfo";
export const backendConfig = (): TypeInput => {
return {
appInfo,
supertokens: {
connectionURI: "https://try.supertokens.io",
},
recipeList: [
ThirdPartyEmailPassword.init({
/**
* These are development credentials provided by SuperTokens, make sure
* to use your own credentials when you deploy to production.
*/
providers: [
{
config: {
thirdPartyId: "google",
clients: [{
clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com",
clientSecret: "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"
}]
}
},
{
config: {
thirdPartyId: "github",
clients: [{
clientId: "467101b197249757c71f",
clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd"
}]
}
},
{
config: {
thirdPartyId: "apple",
clients: [{
clientId: "4398792-io.supertokens.example.service",
additionalConfig: {
keyId: "7M48Y4RYDL",
privateKey:
"-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----",
teamId: "YWQCXGJRJL",
}
}]
}
},
],
}),
Session.init(),
],
};
}
let initialized = false;
export function ensureSuperTokensInit() {
if (!initialized) {
SuperTokens.init(backendConfig());
initialized = true;
}
}
The ThirdPartyEmailPassword
recipe adds email password and social login to our project and the Session
recipe adds the functionality of managing users sessions and refreshing their sessions.
In our APIs we will use ensureSuperTokensInit
to make sure SuperTokens is initialised before we use any functionality from the SDK.
Also modify the dev
script in the package.json
to:
{
"scripts": {
"dev": "next dev -p 3000",
},
}
We do this because in this example we want the project to run on port 3000
, if you want to keep the default port or use another port you can modify this script. Keep in mind that if you use a different port, you also need to modify the apiDomain
and websiteDomain
values inside the /app/config/backend.ts
file.
Step 2: Adding SuperTokens’ API routes
The SuperTokens backend SDKs expose a set of API routes that we can use in our project, this makes it easier to add login to our app because we don’t have to build this logic ourselves.
Start by creating a new file /app/api/auth/[...path]/route.ts
. Adding [...path]
as a folder makes sure that the route.ts
file is called for all network calls made to the /auth/*
paths. Add the following content to the route.ts
file:
import { getAppDirRequestHandler } from 'supertokens-node/nextjs';
import { NextRequest, NextResponse } from 'next/server';
import { ensureSuperTokensInit } from '../../../config/backend';
ensureSuperTokensInit();
const handleCall = getAppDirRequestHandler(NextResponse);
export async function GET(request: NextRequest) {
const res = await handleCall(request);
if (!res.headers.has('Cache-Control')) {
// This is needed for production deployments with Vercel
res.headers.set(
'Cache-Control',
'no-cache, no-store, max-age=0, must-revalidate'
)
}
return res;
}
export async function POST(request: NextRequest) {
return handleCall(request);
}
export async function DELETE(request: NextRequest) {
return handleCall(request);
}
export async function PUT(request: NextRequest) {
return handleCall(request);
}
export async function PATCH(request: NextRequest) {
return handleCall(request);
}
export async function HEAD(request: NextRequest) {
return handleCall(request);
}
We call ensureSuperTokensInit
to make sure SuperTokens is always initialised when any of the SuperTokens API routes are called. getAppDirRequestHandler
is a helper function that the SuperTokens SDK provides, this adds request parsing, error handling etc so that we dont have to manually check for individual API results.
And thats it, the SuperTokens backend SDK is setup. To verify that we have set it up correctly you can open http://localhost:3000/api/auth/signup/email/[email protected] in your browser, if it is all working correctly you should see a response similar to:
{"status":"OK","exists":false}
You can see the full API spec to know all the API routes that the SuperTokens backend SDKs expose.
Step 3: Adding SuperTokens to the frontend
supertokens-auth-react
provides a pre built UI that gets added as part of your project so that you dont have to build the login UI yourself. If you want to build your own UI you can use functions exposed by the SDK to interact with your APIs, either way you dont need to call the APIs exposed by the backend SDKs manually.
The full set of steps for initialising SuperTokens with the pre-built UI can be found in the official frontend setup guide
Create a /app/config/frontend.ts
file and add the following code to it:
import ThirdPartyEmailPassword, {
Google,
Github,
Apple,
} from 'supertokens-auth-react/recipe/thirdpartyemailpassword';
import Session from 'supertokens-auth-react/recipe/session';
import { appInfo } from './appinfo';
import { useRouter } from 'next/navigation';
import { SuperTokensConfig } from 'supertokens-auth-react/lib/build/types';
const routerInfo: { router?: ReturnType<typeof useRouter>; pathName?: string } =
{};
export function setRouter(
router: ReturnType<typeof useRouter>,
pathName: string,
) {
routerInfo.router = router;
routerInfo.pathName = pathName;
}
export const frontendConfig = (): SuperTokensConfig => {
return {
appInfo,
recipeList: [
ThirdPartyEmailPassword.init({
signInAndUpFeature: {
providers: [
Google.init(),
Github.init(),
Apple.init(),
],
},
}),
Session.init(),
],
windowHandler: (orig) => {
return {
...orig,
location: {
...orig.location,
getPathName: () => routerInfo.pathName!,
assign: (url) => routerInfo.router!.push(url.toString()),
setHref: (url) => routerInfo.router!.push(url.toString()),
},
};
},
};
}
Create a /app/components/supertokensProvider.tsx
file and add the following contents to it:
'use client';
import React from 'react';
import { SuperTokensWrapper } from 'supertokens-auth-react';
import SuperTokensReact from 'supertokens-auth-react';
import { frontendConfig, setRouter } from '../config/frontend';
import { usePathname, useRouter } from 'next/navigation';
if (typeof window !== 'undefined') {
// we only want to call this init function on the frontend, so we check typeof window !== 'undefined'
SuperTokensReact.init(frontendConfig());
}
export const Providers: React.FC<React.PropsWithChildren<{}>> = ({
children,
}) => {
setRouter(useRouter(), usePathname() || window.location.pathname);
return <SuperTokensWrapper>{children}</SuperTokensWrapper>;
};
We check for window
not being undefined because we only want to initialise the SuperTokens React SDK on the client side. The SuperTokensWrapper
component adds some context to the rest of our application so that we can access the session from anywhere.
SuperTokens
wrapper initialises a React context for sessions, because of this we make this component a client component by adding 'use client'
to the top of the file.
Modify the /app/layout.tsx
file: