Integrate GitHub OAuth With NextAuth.js in Next.js 13 With Custom Sign In / Out Pages
Tags: nextjs, oauth, github, webdev
Published: 2023-09-16 16:09:13

Link to NextAuth docs **[here](https://next-auth.js.org/getting-started/introduction)**

This page will show you how to set up basic authentication using NextAuth while using custom sign in and out pages. We will use just GitHub for this simple demo, no email / password.

## Initial Setup

Quickly download and setup the latest Next.js TypeScript starter:

```bash
npx create-next-app@latest --ts .
```

If you are getting warnings in your CSS file complaining about unknown CSS rules, follow **[these steps here](https://github.com/tailwindlabs/tailwindcss/discussions/5258#discussioncomment-1979394)**

Still in `globals.css`, update the code with this reset from **[Josh Comeau](https://www.joshwcomeau.com/css/custom-css-reset/)**

```css
/* src/app/globals.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

*, *::before, *::after {
  box-sizing: border-box;
}

* {
  margin: 0;
	padding: 0;
}

body {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select {
  font: inherit;
}

p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

#root, #__next {
  isolation: isolate;
}
```

Update `tsconfig.json` to this:

```json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "Node",
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
```

Update `src/app/page.tsx` to this:

```tsx
// src/app/page.tsx

const HomePage = () => {
  return (
    <div>
      <h1>HomePage</h1>
    </div>
  );
};

export default HomePage;
```

Update `src/app/layout.tsx` to this:

```tsx
// src/app/layout.tsx

import { Inter } from "next/font/google";

import type { Metadata } from "next";
import type { ReactNode } from "react";

import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  description: "Generated by create next app",
  title: "NextAuth Demo"
};

type RootLayoutProps = {
  children: ReactNode;
};

const RootLayout = ({ children }: RootLayoutProps) => {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
};

export default RootLayout;
```

Install the `NextAuth` package:

```bash
npm i next-auth
```

## Auth Route Handler

For this demo, we won’t be using a database

Additionally, we’ll be using the default settings for JWT

In your text editor, right click on the `app` directory, and paste this in (or create this manually if need be)

```
api/auth/[...nextauth]/route.ts
```

This will create the `route.ts` file at the correct location

**NOTE: Make sure the `api` folder is the `app` directory!!**

Add this dummy code for now:

```tsx
// src/api/auth/[...nextauth]/route.ts

import NextAuth from "next-auth"

const handler = NextAuth()

export { handler as GET, handler as POST }
```

In Next.js, you can define an API route that will catch all requests that begin with a certain path. Conveniently, this is called **[Catch all API routes](https://nextjs.org/docs/api-routes/dynamic-api-routes#catch-all-api-routes)**.

When you define a `/pages/api/auth/[...nextauth]` JS/TS file, you instruct NextAuth.js that every API request beginning with `/api/auth/*` should be handled by the code written in the `[...nextauth]` file.

## NextAuth `options`

Link to Options in the docs **[here](https://next-auth.js.org/configuration/options)**

The `NextAuth` function needs an `options` argument, which is an object of type `AuthOptions`

You can do this in the same file, but we’ll separate it out so we can export it

This will benefit us later as we’ll need it in other files as well

The options object needs at least `providers`, which is an array of `Provider` objects

In the `[...nextauth]` directory, create another file called `options.ts`

Add this code to it:

```tsx
// src/app/api/auth/[...nextauth]/options.ts

import type { AuthOptions } from "next-auth";

export const options: AuthOptions = {
  providers: []
};
```

## `NEXTAUTH_SECRET`

Let’s pause here, and in a terminal window run this command:

```bash
openssl rand -base64 32
```

This value will be used to encrypt the NextAuth.js JWT, and to hash email verification tokens. This is the default value for the secret option in NextAuth and Middleware.

At the root level, create an `.env.local` file, and add the following:

```
NEXTAUTH_SECRET=VALUE_CREATED_FROM_OPENSSL_COMMAND
```

## Setting up GitHub OAuth

Link to OAuth in the docs **[here](https://next-auth.js.org/configuration/providers/oauth)**

For this demo, we’ll be using a built-in OAuth Provider - GitHub

Link to GitHub page in the docs **[here](https://next-auth.js.org/providers/github)**

Visit **[this page](https://github.com/settings/apps)**

Click **OAuth Apps** in the left-hand nav menu

Click **New OAuth App**

Name: `nextauth-demo`

Homepage URL: `http://localhost:3000/`

- **NOTE:** Make sure to change this URL once the app is deployed!

Application description (optional)

Authorization callback URL: `http://localhost:3000/api/auth/callback/github`

- This is basically saying where it’s going to send you after you authenticate with GitHub

Leave **Enable Device Flow** unchecked

Click **Register application**

On the next page, we should have a **Client ID**

Copy and paste this into a file for now, such as the README

We also need a Client Secret, which we can generate by clicking **Generate a new client secret**

Confirm by entering your GitHub password again

Copy and paste the value into the env file (as well as the client):

```
// .env.local

NEXTAUTH_SECRET=""
GITHUB_SECRET=""
GITHUB_ID=""
```

## Configuring Providers in options

Now that we have the Client ID and Secret, we can finish configuring the NextAuth options object

We’ll need the GitHubProvider from next-auth, so let’s import it:

```tsx
// src/app/api/auth/[...nextauth]/options.ts

import GitHubProvider from 'next-auth/providers/github'
```

Now let’s update the `providers` array:

```tsx
// src/app/api/auth/[...nextauth]/options.ts

import GitHubProvider from "next-auth/providers/github";
import type { AuthOptions } from "next-auth";

export const options: AuthOptions = {
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!
    })
  ]
};
```

TypeScript will complain that when using the environment variable, `type string | null is not assignable to type string`. So, we use the `!` to tell TypeScript it is definitely there. You could also use `as string` as well.

And that’s it for the GitHub setup!

## Creating Pages

Now let’s set up the pages we’ll need

In the `app` directory, create a `sign-in`, `sign-out`, and `profile` folder

And then in each of those folders, create a `page.tsx` file. The code can look like this for now:

```tsx
// src/app/sign-in/page.tsx

const SignInPage = () => {
  return (
    <div>
      <h1>SignInPage</h1>
    </div>
  );
};

export default SignInPage;
```

```tsx
// src/app/sign-out/page.tsx

const SignOutPage = () => {
  return (
    <div>
      <h1>SignOutPage</h1>
    </div>
  );
};

export default SignOutPage;
```

```tsx
// src/app/profile/page.tsx

const ProfilePage = () => {
  return (
    <div>
      <h1>ProfilePage</h1>
    </div>
  );
};

export default ProfilePage;
```

The `sign-in` and `sign-out` pages should be self explanatory. The `profile` page will be used to display basic info about the user from their GitHub profile page. This page should only be accessible once logged in.

While we’re at it, let’s create a simple `Navbar` component:

```tsx
// src/components/Navbar.tsx

import Link from "next/link";

const Navbar = () => {
  return (
    <nav className="bg-indigo-600 p-4">
      <ul className="flex gap-x-4">
        <li>
          <Link href="/" className="text-white hover:underline">
            Home
          </Link>
        </li>

        <li>
          <Link href="/sign-in" className="text-white hover:underline">
            Sign In
          </Link>
        </li>

        <li>
          <Link href="/profile" className="text-white hover:underline">
            Profile
          </Link>
        </li>

        <li>
          <Link href="/sign-out" className="text-white hover:underline">
            Sign Out
          </Link>
        </li>
      </ul>
    </nav>
  );
};
```

All the links / pages will be accessible for now, but we’ll change that soon.

Back in the `route.ts` file, let’s import the options and pass them into `NextAuth` function so we don’t see the red underline anymore:

```tsx
import NextAuth from "next-auth";
import { options } from "./options";

const handler = NextAuth(options);

export { handler as GET, handler as POST };
```

## Send a `GET` request to `/api/auth/providers`

Let’s finally start up the app! Run `npm run dev`

Then, you can either:

- Go to [**http://localhost:3000/api/auth/providers**](http://localhost:3000/api/auth/providers**) in the browser
- Send a `GET` request with a service like Postman (to that same URL, just make sure the dev server is running)

And you should get the following JSON response:

```json
{
    "github": {
        "id": "github",
        "name": "GitHub",
        "type": "oauth",
        "signinUrl": "<http://localhost:3000/api/auth/signin/github>",
        "callbackUrl": "<http://localhost:3000/api/auth/callback/github>"
    }
}
```

## Apply NextAuth with Middleware

Link to the docs regarding `matcher` **[here](https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher)**

Using middleware is one of the easiest ways to apply NextAuth to the entire site

In the src directory, create a file called `middleware.ts`

This file runs on the edge, and we only need to add one line to this file:

```tsx
// src/middleware.ts

export { default } from "next-auth/middleware"
```

Now, with just this one line, NextAuth is applied to ALL pages

You can of course set this for a few select pages using a `matcher` like so:

```tsx
// src/middleware.ts

export { default } from "next-auth/middleware";

// applies next-auth only to matching routes
export const config = { matcher: ["/profile"] };
```

## Custom Sign In / Out Pages

Link to `pages` option in the docs **[here](https://next-auth.js.org/configuration/pages)**

Let’s go back to the `options.ts` file and update our options variable with the `pages` property:

```tsx
// src/app/api/auth/[...nextauth]/options.ts

export const options: AuthOptions = {
  providers: [
    GitHubProvider({
      name: "GitHub",
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!
    })
  ],
  pages: {
    signIn: "/sign-out",
    signOut: "/sign-out"
  }
};
```

## Adding Sign In / Out Functionality

Let’s start with the sign-in page.

But before we do, let’s be aware of something.

Since we are no longer using the default settings provided by NextAuth, we will not get the default “Login with GitHub” button which is styled.

So, we will need to create this component ourselves. Furthermore, this component will need to be a client component since we’ll be using the `onClick` event handler.

We can keep the sign-in page as a server component, and then add in the button as a child component.

This is my preferred approach: the page (parent) is a server component, and then any child components can be client components if need be.

In the `components` folder, create a `SignInButton.tsx` and `SignOutButton.tsx`:

```tsx
// src/components/SignInButton.tsx

"use client";

import { signIn } from "next-auth/react";

const SignInButton = () => {
  return (
    <button
      className="bg-slate-600 px-4 py-2 text-white"
      onClick={() => signIn("github", { callbackUrl: "/profile" })}
      type="button"
    >
      Sign In With GitHub
    </button>
  );
};

export default SignInButton;
```

For the `signIn` function, we need to pass in at least an `id`. Since we are using GitHub as our OAuth provider, we’ll use `github`. If you ran the GET request earlier, you would’ve seen what ids are available to you.

We’ll also specify the `callbackUrl` to tell NextAuth where to redirect the user once they are logged in. By default, you’ll be redirected to the same page that you logged in from. This is typically not the behaviour you want, so that is why we specify where to go.

The `SignOutButton` component:

```tsx
// src/components/SignOutButton.tsx

"use client";

import { signOut } from "next-auth/react";

const SignOutButton = () => {
  return (
    <button
      className="bg-slate-600 px-4 py-2 text-white"
      onClick={() => signOut({ callbackUrl: "/" })}
      type="button"
    >
      Sign Out of GitHub
    </button>
  );
};

export default SignOutButton;
```

Since this is just a simple login using OAuth, we do not need an entire form with other fields. We just need a button to click to sign us in and out.

Next, in the `SignInPage`, update the code to this:

```tsx
// src/app/sign-in/page.tsx

import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { options } from "../api/auth/[...nextauth]/options";

import SignInButton from "@/components/SignInButton";

const SignInPage = async () => {
  const session = await getServerSession(options);

  if (session) {
    redirect("/profile");
  } else {
    return (
      <div>
        <h1>SignInPage</h1>

        <SignInButton />
      </div>
    );
  }
};

export default SignInPage;
```

Inside the component, we await the `getServerSession`, and pass in our `options`

We assign the result of this to the `session` variable

Then we conditionally render the JSX based on whether or not a session is active

You might be wondering why we specify a `callbackUrl` in the `signIn` function, as well as use the `redirect` function here in the page component.

The reason for this is that without this conditional rendering, we could still access the `sign-in` page while logged in by changing the URL manually. And as you’ll see shortly, the same applies to the sign-out page. We could still access the sign-out page, even though we are already logged out. So this is a nice extra layer of protecting our routes.

The `SignOut` page:

```tsx
// src/app/sign-out/page.tsx

import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { options } from "../api/auth/[...nextauth]/options";

import SignOutButton from "@/components/SignOutButton";

const SignOutPage = async () => {
  const session = await getServerSession(options);

  if (!session) {
    redirect("/");
  } else {
    return (
      <div>
        <h1>SignOutPage</h1>

        <SignOutButton />
      </div>
    );
  }
};

export default SignOutPage;
```

From here, we can also update our Navbar component based on whether or the user is logged in (a session is active):

```tsx
// src/components/Navbar.tsx

import Link from "next/link";
import { getServerSession } from "next-auth/next";
import { options } from "@/app/api/auth/[...nextauth]/options";

const Navbar = async () => {
  const session = await getServerSession(options);

  return (
    <nav className="bg-indigo-600 p-4">
      <ul className="flex gap-x-4">
        <li>
          <Link href="/" className="text-white hover:underline">
            Home
          </Link>
        </li>

        {!session ? (
          <li>
            <Link href="/sign-in" className="text-white hover:underline">
              Sign In
            </Link>
          </li>
        ) : (
          <>
            <li>
              <Link href="/profile" className="text-white hover:underline">
                Profile
              </Link>
            </li>

            <li>
              <Link href="/sign-out" className="text-white hover:underline">
                Sign Out
              </Link>
            </li>
          </>
        )}
      </ul>
    </nav>
  );
};

export default Navbar;
```

## Next.js Images from an OAuth Provider

Next, let’s display some basic information about our GitHub profile on the profile page:

```tsx
// src/app/profile/page.tsx

import Image from "next/image";
import { getServerSession } from "next-auth";
import { options } from "../api/auth/[...nextauth]/options";

const ProfilePage = async () => {
  const session = await getServerSession(options);

  return (
    <div>
      <h1>ProfilePage</h1>

      <div>
        {session?.user?.name ? <h2>Hello {session.user.name}!</h2> : null}

        {session?.user?.image ? (
          <Image
            src={session.user.image}
            width={200}
            height={200}
            alt={`Profile Pic for ${session.user.name}`}
            priority={true}
          />
        ) : null}
      </div>
    </div>
  );
};

export default ProfilePage;
```

When we display images that are being fetched from a 3rd party, you need to make sure to update the next.config.js file with that service’s domain:

```tsx
// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ["avatars.githubusercontent.com"]
  }
};

module.exports = nextConfig;
```

And that’s it! Thank you so much for reading this article, and I hope you found this simple demo helpful. This is my first time using NextAuth, so if you have any suggestions for improvement, please let me know. The repo can be found **[here](https://github.com/andrews1022/demo-nextjs-13-nextauth-custom)**, which you can use as a reference in case you get stuck.

Cheers, and happy coding!