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:
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.