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
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.
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
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:
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</>
And if !session
returns false
, which means the session exists and the user is authenticated, a different JSX code will be returned.
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</>
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.
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
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
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,
And if the user is authenticated.
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
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
.
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.