ProNextJS
    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

    Our next step in building out an application is to add authorization to it. To do that, we are going to bring in the NextAuth library. And in this section, I recommend that you follow along step by step. So let's go back to VS Code. Then in our terminal, we're going to add the NextAuth library. To do that, I'm going to use pnpm add and then NextAuth as the library name.

    Now we need to set up our environment. So we're going to do that in a file called .env.development.local. This is going to be the environment variables that are going to get set whenever we run the dev command. So why .env.development.local?

    Well, that's exactly where you want to put local environment variables. And there's a couple of them we want to put here. First is the NextAuth URL. That one's pretty easy because it's just localhost 3000. Next thing we need is a NextAuth secret. This can be any string value you want. A lot of folks use OpenSSL for this.

    I'm going to ask for a random number that's base64 encoded. The size is 32, hit return. And now we get this random string that we can plop into our NextAuth secret. Okay, cool. So now we're all set up and now we need some way to log in. So what's an easy way to do that? Well, you can connect yourself to a social auth provider. And well, what do we all have?

    Well, we all have GitHub. So let's use GitHub for that. So I'm going to go back over to GitHub. I'll go to my settings. And then down here at the bottom, we've got developer settings. And there are a couple options. So we're going to use GitHub apps. Now you have to give it a name. So I'm going to use the name Jahur CarDev. It can be anything you want.

    It just has to be unique. It's going to be Jahur, that's my username. Car for GBT app router. And then dev, because this is going to be our dev token. You actually have to have two tokens, one for development and one for production. That's actually unique to GitHub in this case.

    GitHub only has one URL that you can put on here for the homepage. So in our case, we're going to put for the development version, localhost 3000. I'll just copy that out here. Now, of course, in production, we're going to have the versal URL. So you can only have one URL.

    So that's why we have to have two different keys, one for dev and one for prod. You also need to specify the callback URL. So we'll start again with localhost 3000. And then we're going to put our auth code on API auth. And within that, you're going to specify callback.

    Because this is going to be GitHub calling us back when somebody is logged in. And then you give it the provider. So in this case, GitHub. Now, the API auth section is up to us. We can put it anywhere we want. The callback GitHub is something specific to NextAuth. That's the format that NextAuth is looking for.

    It's looking for callback as kind of the verb. And then whatever the provider is after that. So in this case, that's GitHub. Now we don't need down here a webhook. So we don't need to specify that. We now create the GitHub app. And there we go. We've got our dev token. So there's a couple important pieces of information here. First, the client ID, we're going to copy that.

    I'm going to go back into our development local, specify that we have a GitHub ID. And then give it that value. We also need a GitHub secret. How do we get that? Well, we go over here to our client secret, generate the new client secret, copy it,

    paste it in, and there we go. All right. Now this is all set up, which is great, but let's actually go and kind of get ahead of ourselves a little bit by going back over into our GitHub and creating the prod version and then connecting that over into prod. So we'll create another one, call it jiracar-prod.

    Now, what do we use for our homepage URL? Let's go back over to our deployment system. If we go over into project, our URL for that is under domains. You don't want to get the deployment URL because that is specific to a specific deployment. You want to get the domain.

    This is going to be the main deploy. So let's hit copy on that. And I'll go back over into GitHub. That's going to be our homepage URL. In callback URL, well, again, they do that exact same thing.

    API off, callback GitHub, and no webhook. And we'll create that GitHub app. All right. Now we need to go and take our client ID. Now, where do we put this over on Vercel? Let's go over into settings. And now we have in our environment variables tab.

    Awesome. So we can go put in here GitHub ID, give it our ID and add another, GitHub secret. Let's go get that.

    Copy that again, paste it in here. And then we do need that next off secret. So let's go and add that as well. For that, I'm going to go back into my terminal and generate another secret.

    Paste that in there, save, and we're all set. Good to go. So now that we're all set up with our environment variables, let's go and integrate next off into our application and get us logging in. So we'll go over to source and in the app directory, I'm going to create a new file.

    So I'm going to use that new file trick again. We're going to create an API directory. And within that directory, we're going to create an off directory. And then after that, we're going to use what's called a catch all route.

    So in Next.js, if you use brackets and then dot, dot, dot, it will take anything after API off and assign it to whatever the variable name you want is. So in this case, next off. So now anything after API off is going to be associated with the parameter

    next off and passed to route.ts. So why route.ts and not page.tsx? Well, we're not returning a page here. This is a API endpoint. So we want access to the raw request. And to do that in Next.js, you would create a route handler as

    opposed to a page handler. So we're creating a route.ts file. So in this file, I'm going to bring in next off, and then I'm going to bring in the GitHub provider, and we're going to set up our options. So we're going to call that off options. And the only options we're going to provide in this case is if we want to say GitHub provider, and then we're going to give it the ID and the secret environment variables that we set before.

    Then we'll go and create a next off handler and we'll pass those back to Next.js as both get and post. Now we're going to take a look a little bit later on at API handlers in more detail, but suffice to say, you're going to be exporting named functions in this

    case, get and post, and those are going to handle the verbs that correspond to those names. So that get handler is going to get called whenever you get a get request and post is going to get called whenever you get a post request. And that's how route handlers work in Next.js. Now that we have our API endpoint all set up, we need to have a way to log in.

    And that means going back over to our layout and adding a session provider to our layout, because we want our session, that'd be the next off session, who are you and all that to be provided anywhere in our application. So we want to put the provider in layout so that it's provided on all of the routes in our application.

    So we're going to bring in the session provider from next off, and then we're going to wrap our application in it. So I'm going to go down here and wrap our app in our session provider.

    All right, now let's bring this up. Just make sure that everything's working, go into our local host. So now we get this weird error. React context is unavailable in server components. So what does that mean?

    Let's go back in our app. Now I've added a session provider. So let's try and take that out and see if that gets rid of the error. Yes, it does. Okay. So the clearly the issue is a session provider. So what's the deal? Well, route layout is what's called a React server component.

    So by default, any component that you build in an XJS application, unless you specify it as a client component, is a server component and it runs only on the server and React server components can't access context because that's a client

    thing and can't use hooks because that's also a thing that runs on the client. And what's happening here is a session provider that we get from next off React isn't marked as a client component. So it can't use context. So how do we get it to be able to use context? So I have a solve for that.

    I'm going to create a new directory under app called components. And then within that session provider.tsx. And I'm going to start off this file by saying, use client. That's going to mark any component that's in here as a client component. And then I'm simply going to re-export anything from next off React from this file.

    Now I can go over to my layout. I can bring in that component session provider. I'll bring in session provider from that, hit save. And when you know it, it works. Now we've declared the session provider is a client component by simply wrapping

    it in another module that says use client and we are good to go. Now that we've got our session provider and our API, next thing we need to do is provide a UI mechanism to log in. So how do we do that? Well, I'm going to put a sign in button in the header up here.

    To do that, I'm going to bring in some components from ShadCN. To do that, I'm going to again, use MPX with ShadCN UI at latest for the module name, and then I'm going to use the add verb to specify the different components that I want. In this case, I want button. It's going to have that sign in button avatar.

    That's going to show my face and then drop down menu. I'm going to use drop down menu so that when you click on my face and you're logged in, it'll go and have that sign out so that you can then sign out as well as sign in. Now I've gone and installed those. We can see over in the components directory, we now have avatar, button, and drop down menu.

    Now I'm going to bring in the user button component. You can find the code for this in the instructions. To do that, I'm going to create a new file, call it user button, and I'll bring in the code and I'll walk you through it. So right at the top here, we're going to bring in our ShadCN components, our avatar, our button, our drop down menu.

    Then we're going to bring in some handy functions from NextAuth React. First is use session. That's a hook that gives you access to the session in a client component. There is a way to get it from the server and I'll show you that in just a bit. And then there's also the sign in and sign out functions. Those you call when you want to either sign in or assign someone out.

    Then there's a handy helper function to get the first capital letters of the person's name. We're going to put that in the avatar if they don't actually have an avatar. Then down in our client component, user button, we're going to first get our session and then we're going to look at the status. And if we have a status of authenticated, we're going to have a drop down menu

    that's going to be associated with an avatar that's going to have my face on it or your face when you log in. And then inside of that drop down menu, we're going to have a sign out button that you can sign out with. But if we're not authenticated, then we're just going to have a button that calls sign in when you click it. So to use this, I'm going to go back over into our layout.

    I'm going to import our user button. And then I'm going to put it down in this div. That's going to put it on the right hand side of the header. We'll hit save. And now we'll launch our server. And now we get our sign in button. Cool.

    Let's give it a try. See if it works. Hit sign in up. This is a good sign. Sign in with GitHub. Awesome. Yes, I want to authorize our app. And we're logged in. There you go. Got our drop down menu. Sign out. Awesome. This is great. Okay. Now the next thing we want to do is push this to production.

    But if we push it to production as it is now, then really anyone with a GitHub user ID can log into our app and start hitting us with chat GPT requests, which is probably not great. So what I'm going to do is I'm going to add some additional security to our login to do that, I'll go back into our route.

    And in addition on the options, I'm going to add this callback section. Callbacks happen whenever an event happens. So in this case, when I say, okay, when someone's signed in, I'm actually going to take a look at their login to see who they are. And I'm just going to say, if they're not me, so not using my username, just say no.

    So only I can ever log in and use this app. Now to just make sure TypeScript is happy. I'll bring in callback options and let's take a look. All right. I'll try and log in and log out again. Just to make sure that it works. Okay. So it likes me and that's great. Let's go and push this to Versal.

    All right. To do that, I'm going to stop my server. Again, I'm going to add all my changed files. I'm going to commit it as auth, and then I'm going to push that to main, which is going to start the rebuild on Versal. All right, let's go have a look. So take a look at deployments. We're building, find out in just a bit, we did it right.

    All right. We're built. Let's go have a look. I'll refresh. Ooh, got our sign in button. That's a good sign. Sign into the GitHub. I like it. Now we're looking at prod as opposed to our dev. That's a real good sign. Let's hit authorize. And we're logged in. How cool is that?

    So if that doesn't work, your issue is probably in the environment variables of Versal, could be the names, could be the values that you put in there for the GitHub ID or the GitHub secret. One of those things is probably going to be the issue. Next up, we're going to add even more interactivity to our application by actually adding our chat GPT functionality to our app.

    I'll see you in the next section.