Part 1: Implement OrderCloud authentication using the next-auth module

The previous article set up the admin portal’s basic UI structure. In this article, we will learn how to implement OrderCloud user authentication for the admin portal using the next-auth module.

You can clone the GitHub repository to refer to the code I’m discussing in this article for implementing OrderCloud authentication.

In the previous article, we did create the AppConfiguration class which provides access to the environment variables.

I’ve created a few more helper functionalities to use throughout the implementation of the admin portal. You can find them in the library folder if you have cloned the GitHub repository mentioned above.

Before we move on to the authentication implementation, we need to install the next-auth module.

To do so, open the root directory of the code base in the terminal and run the following command.

npm install next-auth

Once the next-auth module is installed, we need to create the file called [...nextauth].ts in the directory pages/api/auth. This file handles all the requests to /api/auth/* (signIn, callback, signOut, etc.) automatically.

Next, we will implement the authentication logic in the [...nextauth].ts file. Following is the code snippet which shows the implementation, which we will go through step-by-step.

import appConfiguration from "@/library/configuration";
import { NextAuthOptions } from "next-auth";
import { AdapterUser } from "next-auth/adapters";
import NextAuth from "next-auth/next";
import Credentials from "next-auth/providers/credentials";
import {
  AccessToken,
  Auth,
  Me,
  MeUser,
  Tokens,
} from "ordercloud-javascript-sdk";

const authOptions: NextAuthOptions = {
  providers: [
    Credentials({
      credentials: {
        username: { label: "Username", type: "text", placeholder: "Username" },
        password: {
          label: "Password",
          type: "password",
          placeholder: "Password",
        },
      },
      async authorize(credentials, request) {
        try {
          if (credentials) {
            const tokenResponse: AccessToken = await Auth.Login(
              credentials?.username,
              credentials?.password,
              appConfiguration.sellerAppAPIClientId,
              appConfiguration.sellerAdminScopes
            );
            if (tokenResponse && tokenResponse.access_token) {
              Tokens.SetAccessToken(tokenResponse.access_token);
              const meResponse = await Me.Get<MeUser>();
              if (meResponse) {
                const user: AdapterUser = {
                  me: meResponse,
                  name: `${meResponse.FirstName} ${meResponse.LastName}`,
                  email: meResponse.Email,
                  token_type: tokenResponse.token_type ?? "bearer",
                  access_token: tokenResponse.access_token ?? "",
                  refresh_token: tokenResponse.refresh_token ?? "",
                  expires_in: tokenResponse.expires_in ?? 36000,
                  id: meResponse.ID,
                  emailVerified: new Date(),
                };
                return user;
              }
            }
          }
          return null;
        } catch (error) {
          console.log("ERROR");
          console.log(error);
        }
        return null;
      },
    }),
  ],
  secret: process.env.NEXTAUTH_SECRET,
  pages: {
    signIn: "/login",
    signOut: "/logout",
  },
  callbacks: {
    async jwt({ token, user }) {
      if (token && user) {
        const adapterUser = user as AdapterUser;
        return {
          ...token,
          access_token: adapterUser.access_token,
          refresh_token: adapterUser.refresh_token,
          token_type: adapterUser.token_type,
          expires_in: adapterUser.expires_in,
          me: adapterUser.me,
        };
      }
      return token;
    },
    async session({ session, token, user }) {
      if (session && token) {
        session.user.access_token = token.access_token;
        session.user.refresh_token = token.refresh_token;
        session.user.expires_in = token.expires_in;
        session.user.token_type = token.token_type;
        session.user.me = token.me;
      }
      return session;
    },
  },
};

export default NextAuth(authOptions);

In the above code, you can see that we first configured the NextAuthOptions through the instance named authOptions. In the options, the providers section shows we used the Credentials provider to implement the OrderCloud authentication through username and password. The instance of the Credentials provider has the attributes credentials, async authorize method.

The credentials object has a username and password attributes that are used on the out-of-the-box login page provided by next-auth. We are implementing our own login page, and hence this section is of no use to us, but I’ve kept that in the code for the understanding purpose.

The most important one is the async authorize method. This method is the heart of the next-auth which allows us to implement the credentials-based authentication. In our code, we are using OrderCloud’s Auth.Login method available in the JavaScript SDK, provided by OrderCloud.

As you can see in the above code, we are passing the username, password, seller app API client id, and the seller admin scopes to the Auth.Login method. The seller app API client id and the seller admin scopes are configured in the .env.local file as discussed in the previous article.

It is a good practice to configure the sensitive information into the environment variables and not to hard code in the code file.

The Auth.Login can be called from both the server and client sides. In our code, we are executing this method from the […nextauth].ts API endpoint which executes this method on the server side.

After successful authentication, we get the access token from OrderCloud in the response. This access token is required for all the API requests to perform different operations. As you can see in the code, we set this access token through the following line.

Tokens.SetAccessToken(tokenResponse.access_token);

The Tokens.SetAccessToken() method is provided in the JavaScript SDK by OrderCloud which stores the access token on the server side if executed through the server-side code. While executed on the client side, it stores the access token in the cookie named ordercloud.access-token.

Now, once the access token is received and set through the Tokens.SetAccessToken() method, we fetch the user profile information using Me.Get<MeUser>() and set the user instance with appropriate values to return from the async authorize method. We return the user object in case of successful authentication else return null to show the error message on the login page.

If you see the code carefully, I’ve created the user object using AdapterUser type. The out-of-the-box AdapterUser type has no properties like me, access_token, token_type, refresh_token, expires_in. To get these properties available, we have to extend the AdapterUser type provided by OrderCloud. Similarly, we need to extend Session and JWT types as well.

To extend the OrderCloud types, create a directory named types in the /src folder and create the next-auth.d.ts file in the types directory, and put the following code in the next-auth.d.ts file.

import { DefaultSession } from "next-auth";
import NextAuth, { DefaultSession, DefaultUser, User } from "next-auth";
import { DefaultJWT } from "next-auth/jwt";
import OrderCloud from "ordercloud-javascript-sdk";

declare module "next-auth" {
  interface Session {
    user: {
      access_token: string | "";
      refresh_token: string | null;
      token_type: string | "";
      expires_in: number;
      me: OrderCloud.MeUser;
    } & DefaultSession["user"];
  }
}

declare module "next-auth/adapters" {
  interface AdapterUser extends User {
    access_token: string | "";
    refresh_token: string | null;
    token_type: string | "";
    expires_in: number;
    me: OrderCloud.MeUser;
  }
}

declare module "next-auth/jwt" {
  interface JWT extends Record<string, unknown>, DefaultJWT {
    access_token: string | "";
    refresh_token: string | null;
    token_type: string | "";
    expires_in: number;
    me: OrderCloud.MeUser;
  }
}

As shown in the code, the secret attribute in the authOptions used to encrypt the NextAuth.js JWT token. Again, the value of the secret attribute is read from the environment variable considering it a piece of sensitive and secure information.

The pages attribute, as shown in the code, has two properties, signIn and signOut which takes the string value of the login and logout routes. These properties tell the next-auth to use the custom login and logout routes instead of the out-of-the-box ones.

The callbacks section shows the implementation of the async jwt({ token, user }) and async session({ session, token, user }) method implementation. After successful login, the async jwt method is called and you can set any additional attributes to the JWT token object before it is returned to the client. As shown in the code, we set the me, access_token, refresh_token, token_type and expires_in attributes of the JWT instance. Similarly, we set session attributes in the async session method.

The JWT callback will be invoked only if you are using a JWT session and not when you persist sessions in the database. The requests to /api/auth/signin, /api/auth/session and calls to useSession(), getSession(), getServerSession() will invoke this function.

Once the authOptions are set, we export the NextAuth instance as shown in the code.

So far, we have set up the very backbone of the authentication system to use the Credentials provider of next-auth module to authenticate the user using username and password against OrderCloud.

Now, we need the login page to show a login form to the user. For the login page, please refer the login.tsx file in the GitHub repo.

The loginSubmit handler takes the responsibility to invoke the signIn function of the next-auth by providing the provider name, username, password, callbackUrl and redirect option in the parameters.

await signIn("credentials", {
          redirect: true,
          username: usernameInputRef.current.value,
          password: passwordInputRef.current.value,
          callbackUrl: "/admin/dashboard",
        });

The true value for redirect option makes sure that upon successful authentication user will be redirected to the /admin/dashbord page.

The logout.tsx page implements the functionality to sign out the user from next-auth session as well as OrderCloud. Please refer to the file to see the implementation.

In the second part of this article, we will see how to implement the middleware and server-side checks to make sure the Next.js application serves the secure pages to authenticated users only.

Jatin Prajapati's Blog

Some little contribution to Sitecore community