ProNextJS
    Loading
    lesson

    Add Authentication with NextAuth.js

    Jack HerringtonJack Herrington

    The next step in building our application is to add authorization. For this, we'll use the NextAuth library. We'll go step by step.

    Add NextAuth Library and Set Environment Variables

    From the terminal, add the NextAuth.js library with pnpm:

    pnpm add next-auth
    

    Next, we need to configure our environment variables in a file called .env.development.local inside of the root src folder.

    We're going to add a couple of variables for NEXTAUTH_URL which will be http://localhost:3000 and NEXTAUTH_SECRET which can be any string value.

    Using OpenSSL to generate a random number that's base64 encoded and 32 in size is a popular choice for secrets, which you can do by running:

    openssl rand -base64 32
    

    Copy the output and paste it into the NEXTAUTH_SECRET variable.

    Here's what .env.development.local should look like:

    NEXTAUTH_URL=http://localhost:3000
    NEXTAUTH_SECRET=your-secret
    

    Add GitHub as a Login Method

    Now that we have our environment variables set up, need a way to log in. One easy way to do this is to use a social authentication provider, so we'll use GitHub.

    Head over to your GitHub settings and then navigate to Developer Settings. This is where we will create new GitHub apps.

    GitHub requires separate tokens for development and production, so we'll create an app for each.

    Development App Settings

    For the development version, use localhost:3000 as the homepage URL and http://localhost:3000/api/auth/callback/github as the callback URL. The callback URL here would change based on the provider you're using with NextAuth.

    Once you hit "Create GitHub App," you'll get a Client ID and Client Secret. Add these to your .env.development.local file as GITHUB_ID and GITHUB_SECRET respectively:

    GITHUB_ID=your-client-id
    GITHUB_SECRET=your-client-secret
    

    Production App Settings

    The steps for creating the production app are similar to the development app. This time, for the homepage URL you'll use the deployment domain from Vercel. You can find this in the Vercel dashboard in the Project Settings tab. It will probably look like something.vercel.app.

    After hitting "Create App", we'll need to copy and paste the Client ID and Client Secret into the Vercel environment variables.

    Inside of the Project Settings tab, navigate to the Environment Variables section and add the GITHUB_ID and GITHUB_SECRET variables. Generate a new openssl secret for this variable.

    With our environment variables all set up, it's time to integrate NextAuth in our application.

    Create a NextAuth Route Handler

    Back in VS Code, we'll create a new file in the src/app directory that includes a catch all route:

    // creating a new file inside of src/app
    
    api/auth/[...nextauth]/route.ts
    

    The [...nextauth] above is a catch all route that will take anything after api/auth and assign it to a variable called nextauth. This means that anything after api/auth in the application is going to be associated with the parameter nextauth and passed to route.ts.

    We use route.ts instead of page.tsx because we're not returning a page. This is an API endpoint, so we want access to the raw request. That's why we create a route handler instead of a page handler.

    Inside of the route.ts file, import NextAuth and the GitHub provider, then we'll set up our options:

    import NextAuth, { CallbacksOptions } from "next-auth";
    import GitHubProvider from "next-auth/providers/github";
    
    const authOptions = {
      providers: [
        GitHubProvider({
          clientId: process.env.GITHUB_ID ?? "",
          clientSecret: process.env.GITHUB_SECRET ?? "",
        }),
      ],
    };
    
    const handler = NextAuth(authOptions);
    
    export { handler as GET, handler as POST};
    

    In the authOptions, we specify the GitHubProvider and give it the clientId and clientSecret from our environment variables.

    The handler is created by calling NextAuth with the authOptions object, then we export it as both a GET and POST handler. These handlers will be called appropriately based on the request method. We'll look at this in more detail later.

    Now that the API endpoint is set up, we need a way to log in.

    Add a Session Provider to Layout

    Back in layout.tsx, let's try importing the SessionProvider from next-auth/react and using it to wrap our <html>:

    import { SessionProvider } from "next-auth/react";
    
    export default function RootLayout({
      children: React.ReactNode;
    }>) {
      return (
        <SessionProvider>
          <html lang="en">
            ...
    

    We end up getting an error that "React Context is unvailable in Server Components".

    This error happens because RootLayout is a React Server Component. This is the default behavior in a NextJS application, unless you specifically mark a component as a client component. Server components are not able to use context or React Hooks, because those are only for the client.

    The SessionProvider from NextAuth is not marked as a client component, so Next tries to treat it as a server component, which is what causes the error.

    Create a New Session Provider

    To fix this, we'll create a new app/components directory and add a new file called SessionProvider.tsx.

    Inside of the file, we'll specify it as a client component by adding "use client"; at the top of the file, then export everything from next-auth/react:

    // inside of app/components/SessionProvider.tsx
    
    "use client";
    export * from "next-auth/react";
    

    With the file created, we need to update the import in the layout.tsx to point at it:

    // inside layout.tsx
    
    import { SessionProvider } from "app/components/SessionProvider";
    

    With this fix, our application is working as expected!

    The last step is to provide a mechanism for logging in.

    Add UI for Login

    Let's bring in some components from shadcn to create our login UI. Run the add command in the terminal:

    npx shadcn-ui@latest add button avatar dropdown-menu
    

    After the installation finishes, create a new UserButton.tsx file inside of the components directory. At the top of the file, specify that it is a client component, then we'll import the components we just installed. We'll also bring in the useSession and signIn and signOut functions from next-auth/react:

    "use client";
    import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
    import { Button } from "@/components/ui/button";
    import {
      DropdownMenu,
      DropdownMenuContent,
      DropdownMenuItem,
      DropdownMenuTrigger,
    } from "@/components/ui/dropdown-menu";
    
    import { useSession, signIn, signOut } from "next-auth/react";
    
    

    This helper function will create an avatar if the person doesn't have an image set on their account:

    function getFirstTwoCapitalLetters(str?: string | null) {
      const match = (str || "").match(/[A-Z]/g);
      return match ? match.slice(0, 2).join("") : "GT";
    }
    

    The default export for this file will be called UserButton. Inside of the component we'll get the session and status by calling useSession(). If the user is authenticated, we'll display the avatar and a dropdown menu with a sign out button. If the user is not authenticated, we'll display a sign in button:

    export default function UserButton() {
      const { data: session, status } = useSession();
    
      return (
        <div>
          {status === "authenticated" && (
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Avatar>
                  <AvatarImage src={session?.user?.image!} />
                  <AvatarFallback>
                    {getFirstTwoCapitalLetters(session?.user?.name)}
                  </AvatarFallback>
                </Avatar>
              </DropdownMenuTrigger>
              <DropdownMenuContent>
                <DropdownMenuItem
                  onClick={() => {
                    signOut();
                  }}
                >
                  Sign Out
                </DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          )}
          {status === "unauthenticated" && (
            <Button onClick={() => signIn()}>Sign in</Button>
          )}
        </div>
      );
    }
    

    With the UserButton implemented, we need to add it to the layout.

    Adding the Login Button to the Layout

    Back inside of src/app/layout.tsx, import the UserButton and place it on the right side of the header:

    // inside of src/app/layout.tsx
    
    import UserButton from "./components/UserButton";
    
    // inside of the component return:
    
    <header className="...">
      <div className="flex flex-grow">
        <Link href="/">GPT Chat</Link>
        <Link href="/about" className="ml-5 font-light">
          About
        </Link>
      </div>
      <div>
        <UserButton />
      </div>
    </header>
    

    With the button added, save your work and reload the homepage.

    Testing the Application

    After hitting the "Sign In" button, you should be redirected to a "Sign in with GitHub" page. After signing in, you should be redirected back to the homepage and see your avatar in the top right corner.

    Clicking your avatar should give you the option to sign out:

    sign out link

    The login and logout functionality is working as expected, but we need to add security before we push to production.

    Secure the Application

    Back in the api/auth/[...nextauth]/route.ts file, we need to add in a callbacks key and signIn function to our authOptions.

    When the signIn function is called, this callback function will run and check if the user logging in is a specific user. If the check fails, the application will not log in the user.

    In this case, I'm using jherr as the specific user, but you should use your GitHub username:

    const authOptions = {
      callbacks: {
        async signIn({ profile }: { profile: { login: string } }) {
          return profile.login === "jherr";
        },
      } as unknown as CallbacksOptions,
      providers: [
        GitHubProvider({
          clientId: process.env.GITHUB_ID ?? "",
          clientSecret: process.env.GITHUB_SECRET ?? "",
        }),
      ],
    };
    

    Note that the as unknown as CallbacksOptions is added to make TypeScript happy. Saving the file, we can refresh the application and see that the login functionality works as expected.

    Push to Production

    Finally, we can commit our changes and push them in order for Vercel to build and deploy the updated application:

    If this process doesn't work, check your environment variables on Vercel. The names and values for GITHUB_ID and GITHUB_SECRET are potential sources of issues.

    With authorization now set up, our application is more interactive and secure. Next up, we'll add our ChatGPT functionality to further enhance the app's interactivity.

    Transcript