Securing APIs: NextAuth
by Tomas Trescak· APIs

0 / 1590 XP
The external project has not been reviewed yet
Please follow instructions below to submit your project

Instructions

In the previous section, we set up the NextAuth with a Github provider. In this section, we want to show you, how easy it it to authenticate your users using NextAuth package.

Please fork and clone the following repository:

https://github.com/WesternSydneyUniversity/comp3036-api-security

Make sure all the tests pass!

🧠 Solution: Route Authentication in NextAuth

If you struggle to solve the exercise, check out our solution!

Part 1: Insecure API

First, let's create an API route that is not secure and is not aware of the user using it. In your src/app/api/tasks/route.ts  create the following route:

import { NextResponse, type NextRequest } from "next/server";

// In-memory data store for tasks
const tasks = [
  {
    userId: "1",
    id: 1,
    description: "Complete the project report",
    completed: false,
  },
  { userId: "1", id: 2, description: "Clean the house", completed: true },
];

async function listTasks(req: NextRequest, res: NextResponse) {
  return NextResponse.json(tasks);
}

export { listTasks as GET };

Run your exercise and test with CURL. 

curl http://localhost:3000/api/tasks

This is the result:

> curl http://localhost:3000/api/tasks
[{"userId":"1","id":1,"description":"Complete the project report","completed":false},{"userId":"1","id":2,"description":"Clean the house","completed":true}],{"userId":"2","id":3,"description":"Dominate universe","completed":false}]% 

You can see that all tasks from all users have been listed. Let's fix this by returning tasks of only the logged-in user.

Part 2: Secure API

To authenticate a current user all you need to do is to import and call the following function (add it at the top of your src/app/api/tasks/route.ts file):

import { auth } from "../../auth/[...nextauth]/options";

Then, you can modify your listTasks route handler as following:

import { NextResponse, type NextRequest } from "next/server";
import { auth } from "../auth/[...nextauth]/options"; // <- This

...

async function listTasks(req: NextRequest, res: NextResponse) {
  // check if user is logged in
  const session = await auth(); // <- That's it!
  
  // only logged in users can view their tasks
  if (session == null) {
    return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.json(tasks.filter((task) => task.userId === session.user.id));
}

Please save the file and observe in the terminal that your server was automatically compiled and reloaded.  Please run the following command:

curl http://localhost:3000/api/tasks

You should now receive the following error:

{"message":"Unauthorized"}

Now, we need to implement login. Simple. First, let's create a server action handling login. We place it in /app/auth/[…nextauth]/actions.ts

"use server";

import { redirect } from "next/navigation";
import { signIn } from "./options";

export async function clientSignIn(formData: FormData) {
  try {
    await signIn("credentials", formData);
  } catch (err) {
    return {
      error: "Failed to sign in",
    };
  } finally {
    redirect("/"); // Redirect to the home page after sign-in
  }
}

Then, we create the login screen in out home page /app/page.tsx

import { clientSignIn } from "./api/auth/[...nextauth]/actions";
import { auth } from "./api/auth/[...nextauth]/options";

export function SignIn() {
  return (
    <form action={clientSignIn}>
      <label>
        User
        <input name="username" type="text" />
      </label>
      <label>
        Password
        <input name="password" type="password" />
      </label>
      <button>Sign In</button>
    </form>
  );
}

async function UserInfo() {
  const session = await auth();

  if (session) {
    return (
      <div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
        <p>Logged in as {JSON.stringify(session.user)}</p>
        <a href="/api/auth/signout">Sign out</a>
      </div>
    );
  } else {
    return <SignIn />;
  }
}

export default function Page() {
  return (
    <div>
      <h1>Welcome to the app!</h1>
      <UserInfo />
    </div>
  );
}

Now, open your project in the browser, navigate to http://localhost:3000 and sign in with the following credentials:

usernameadmin
passwordpassword123

Check out the options.ts in the auth folder for why this must bespecified.

curl http://localhost:3000/api/tasks

OH OH! Still unauthorised! What's going on!

Well, the problem is, that when you login, your login cookie is issued and stored in your browser. Your terminal does not have access to this cookie. So, how can we solve it?

Solution #1: Browser Console

Just like your terminal, you can run commands in your browser developer console. The commands are “javascript” and not “bash” command. Please open your developer console by pressing “CMD+SHIFT+I” on Mac or “ALT+SHIFT+I” on Windows. Then, open your “console” by click on the “console” tab. In the console type the following command and press enter:

await (await fetch("/api/tasks")).json()

The result should look like the following:

It's not very exciting, but we are now using the user's credentials to receive only the user's tasks! 

Solution #2: Curl with Cookies

We know that the browser has the information about the logged-in user. This information is stored in the browser “Cookie”. We need to extract and reuse that cookie. Please re-open your browser console, and this time, open the “Application” tab. On the left, under the “Storage” section, expand the “Cookies” section and then click on “http://localhost:3000”. You should see the following:

There might be some more cookies, but we are after the “next-auth.session-token,” so please copy its entire value. My value of the session token is the following:

176e99d3-2fe2-4464-a047-4fdeb3dfe276

We are now ready to send authorised requests to our API backend by including the value of this cookie in the curl request. Please run the following command, replacing the value of the cookie with your session token from above. 

curl http://localhost:3000/api/tasks --cookie "next-auth.session-token=YOUR_SESSION_TOKEN"

💰 PROFIT! You are now able to send authorised requests

👾 Continue Exercise

Please finish the exercise as per the required test. One more hint. Make sure you validate your schema in a try/catch block and return errors when occurred:

try {
  // Validate incoming data
  const body = await req.json();
  const validatedData = postSchema.parse(body);
  // ...
} catch (error) {
  if (error instanceof z.ZodError) {
    return NextResponse.json(
      { error: "Invalid Data", errors: error.errors },
      { status: 400 }
    );
  }
  return NextResponse.json({ error: "Server error" }, { status: 500 });
}

 

Maggie

Discuss with Maggie
Use the power of generative AI to interact with course content

Maggie is a generative AI that can help you understand the course content better. You can ask her questions about the lecture, and she will try to answer them. You can also see the questions asked by other students and her responses.

Discuss with Others
Ask questions, share your thoughts, and discuss with other learners

Join the discussion to ask questions, share your thoughts, and discuss with other learners
Setup
React Fundamentals
10 points
Next.js
10 points
Advanced React
Databases
10 points
React Hooks
Authentication and Authorisation
10 points
APIs
CI/CD and DevOps
Testing React
Advanced Topics