Protecting Routes

In practice, a SaaS application often includes protected routes, pages, or components that should only be accessible to authenticated users.

In order to implement such logic, we need a mechanism to verify whether a user is signed in. Auth.js provides a straightforward way to handle this, making it easier to secure your application.

Checking if the user is signed in

When the user signs in, a piece of information, referred to as session, will be created and saved inside the database. The session will persist until it expires or the user manually signs out. As long as the session exist, the user is seen as authenticated.

Auth.js comes with a method that allows you to easily retrieve the session. Go back to libs/auth.js and notice that an auth() method is exported from here.

libs/auth.js

javascript
1. . .
2
3export const { handlers, signIn, signOut, auth } = NextAuth({
4  adapter: PrismaAdapter(prisma),
5  providers: [. . .],
6  pages: {. . .},
7});

The method doesn't require any additional argument, just execute it inside server components, API routes, or server actions, and it will return the user session.

If the session exist, the user is authenticated, and if not, the user is not authenticated. You can then implement the logic accordingly.

JavaScript
1import { auth } from "@/libs/auth";
2
3const session = await auth();
4
5if (session) {
6    // User is signed in
7} else {
8    // User is NOT signed in
9}

One thing to note is that auth() does not work on the client side, and that is by design. You should not be handling sessions in client components, or you risk exposing sensitive information.

Protecting server components

Here is an example on how to apply this authorization logic to server components:

page.js

jsx
1import { auth } from "@/libs/auth";
2
3export default async function Page() {
4  const session = await auth();
5
6  return (
7    <div className=". . .">
8      <div className=". . .">
9        {!session ? (
10          <>
11            <h1 className=". . .">User Not Verified</h1>
12            <p className=". . .">
13              You are not signed in. Please sign in to access your account.
14            </p>
15            <a href="/signin" className=". . .">
16              Sign In
17            </a>
18          </>
19        ) : (
20          <>
21            <h1 className=". . .">User Verified</h1>
22            <p className=". . .">
23              Welcome back, {session.user.email}! You are now signed in.
24            </p>
25            <a href="/dashboard" className=". . .">
26              Go to Dashboard
27            </a>
28            <form action=". . ." className=". . .">
29              <button type="submit" className=". . .">
30                Sign Out
31              </button>
32            </form>
33          </>
34        )}
35      </div>
36    </div>
37  );
38}

Recall that A ? B : C is the conditional operator. It is a shortcut for if else statements.

In the above example, if !session returns true, which means session does NOT exist and user is NOT authenticated, the following JSX code will be returned:

jsx
1<>
2  <h1 className=". . .">User Not Verified</h1>
3  <p className=". . .">
4    You are not signed in. Please sign in to access your account.
5  </p>
6  <a href="/signin" className=". . .">
7    Sign In
8  </a>
9</>

User not authenticated

And if !session returns false, which means the session exists and the user is authenticated, a different JSX code will be returned.

jsx
1<>
2  <h1 className=". . .">User Verified</h1>
3  <p className=". . .">
4    Welcome back, {session.user.email}! You are now signed in.
5  </p>
6  <a href="/dashboard" className=". . .">
7    Go to Dashboard
8  </a>
9  <form action=". . ." className=". . .">
10    <button type="submit" className=". . .">
11      Sign Out
12    </button>
13  </form>
14</>

User Authenticated

This approach enables you to design unique user interfaces tailored for both authenticated and unauthenticated users.

Protecting API routes & client components

As for the client components, things get a bit complicated as you can not use auth() directly. In this case, you should set up an API route.

text
1src/app
2├── api
3│   ├── auth
4│   └── is-authenticated
5│       └── route.js        <===== Create an API endpoint here
6├── check-your-email
7├── favicon.ico
8├── globals.css
9├── layout.jsx
10├── page.jsx
11├── signin
12└── client
13    └── page.jsx            <===== Client component

Wrap the route handler inside the auth() function.

route.js

javascript
1import { NextResponse } from "next/server";
2import { auth } from "@/libs/auth";
3
4export const GET = auth(function GET(req) {
5  if (req.auth) {
6    // If user is authenticated.
7    return NextResponse.json(req.auth);
8  } else {
9    // If user is not authenticated.
10    return NextResponse.json(
11      { message: "User not authenticated." },
12      { status: 401 }
13    );
14  }
15});

This gives you access to the session information inside the API route, which can be accessed using req.auth.

In the above example, if the user is authenticated, the session information will be returned in the response, and if not, an error message and a 401 status code will be returned.

Go back to your client component and call this API. You can use a side effect or an event handler. How you design the logic is entirely up to you. As an example, we are using an event handler:

page.jsx

jsx
1"use client";
2
3import { useState } from "react";
4
5export default function Page() {
6  const [apiResponse, setApiResponse] = useState(null);
7  const [error, setError] = useState(null);
8
9  const handleApiRequest = async () => {
10    try {
11      const response = await fetch("/api/is-authenticated");
12      if (response.ok) {
13        const data = await response.json();
14        setApiResponse(data);
15        setError(null);
16      } else {
17        throw new Error("Failed to fetch data");
18      }
19    } catch (err) {
20      setError(err.message);
21      setApiResponse(null);
22    }
23  };
24
25  return (
26    <>
27      {/* Add a button to send a request to the API */}
28      <button onClick={handleApiRequest} className=". . .">
29        Fetch Protected Data
30      </button>
31
32      {/* Display the API response or error */}
33      {apiResponse && (
34        <div className=". . .">
35          <h2 className=". . .">API Response:</h2>
36          <pre className=". . .">{JSON.stringify(apiResponse, null, 2)}</pre>
37        </div>
38      )}
39      {error && (
40        <div className=". . .">
41          <h2 className=". . .">Error:</h2>
42          <p className=". . .">{error}</p>
43        </div>
44      )}
45    </>
46  );
47}

When the user click on the button, the handleApiRequest event handler will be activated, sending a request to /api/is-authenticated.

The API endpoint will check if the user is authenticated, and send responses accordingly, and different user interfaces will be displayed based on the response.

Our demo should give the following result:

If the user is not authenticated,

API not verified

And if the user is authenticated.

API verified

Setting session expiry date

By default, the session expires after a month, and after that, the user must sign in again. You can customize this by setting a session.maxAge option. For example:

libs/auth.js

javascript
1. . .
2
3export const { handlers, signIn, signOut, auth } = NextAuth({
4  adapter: PrismaAdapter(prisma),
5  providers: [. . .],
6  pages: {. . .},
7  session: {
8    maxAge: 7884000, // Three month
9  },
10});

maxAge accepts an integer value, which represents the number of seconds the session will be valid, until it expires.

In the above example, the session will be valid for three months before the user must sign in again.

Signing out

Aside from letting the session expire, you should also implement a mechanism that allows the user to sign out manually. This is the job for the signOut utility function, which we exported before from libs/auth.js.

jsx
1import { auth, signOut } from "@/libs/auth";
2
3export default async function Page() {
4  const session = await auth();
5
6  return (
7    <div className="...">
8      <div className="...">
9        {!session ? (
10          <>. . .</>
11        ) : (
12          <>
13            <h1 className="...">User Verified</h1>
14            <p className="...">
15              Welcome back, {session.user.email}! You are now signed in.
16            </p>
17            <a href="/dashboard" className="...">
18              Go to Dashboard
19            </a>
20            <form
21              action={async () => {
22                "use server";
23                await signOut();
24              }}
25              className="...">
26              <button type="submit" className="...">
27                Sign Out
28              </button>
29            </form>
30          </>
31        )}
32      </div>
33    </div>
34  );
35}

The signOut function works similar to signIn, they both need to be executed on the server. Here we put it inside a server action, and when the form is submitted, the server action will be executed.