DEV Community

Cover image for Integrate GitHub OAuth With NextAuth.js in Next.js 13 With Custom Sign In / Out Pages
andrew shearer
andrew shearer

Posted on

Integrate GitHub OAuth With NextAuth.js in Next.js 13 With Custom Sign In / Out Pages

Link to NextAuth docs here

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:

npx create-next-app@latest --ts .
Enter fullscreen mode Exit fullscreen mode

If you are getting warnings in your CSS file complaining about unknown CSS rules, follow these steps here

Still in globals.css, update the code with this reset from Josh Comeau

/* 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;
}
Enter fullscreen mode Exit fullscreen mode

Update tsconfig.json to this:

{
  "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"]
}
Enter fullscreen mode Exit fullscreen mode

Update src/app/page.tsx to this:

// src/app/page.tsx

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

export default HomePage;
Enter fullscreen mode Exit fullscreen mode

Update src/app/layout.tsx to this:

// 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;
Enter fullscreen mode Exit fullscreen mode

Install the NextAuth package:

npm i next-auth
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

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

import NextAuth from "next-auth"

const handler = NextAuth()

export { handler as GET, handler as POST }
Enter fullscreen mode Exit fullscreen mode

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.

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

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:

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

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

export const options: AuthOptions = {
  providers: []
};
Enter fullscreen mode Exit fullscreen mode

NEXTAUTH_SECRET

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

openssl rand -base64 32
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Setting up GitHub OAuth

Link to OAuth in the docs here

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

Link to GitHub page in the docs here

Visit this page

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=""
Enter fullscreen mode Exit fullscreen mode

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:

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

import GitHubProvider from 'next-auth/providers/github'
Enter fullscreen mode Exit fullscreen mode

Now let’s update the providers array:

// 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!
    })
  ]
};
Enter fullscreen mode Exit fullscreen mode

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:

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

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

export default SignInPage;
Enter fullscreen mode Exit fullscreen mode
// src/app/sign-out/page.tsx

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

export default SignOutPage;
Enter fullscreen mode Exit fullscreen mode
// src/app/profile/page.tsx

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

export default ProfilePage;
Enter fullscreen mode Exit fullscreen mode

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:

// 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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

const handler = NextAuth(options);

export { handler as GET, handler as POST };
Enter fullscreen mode Exit fullscreen mode

Send a GET request to /api/auth/providers

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

Then, you can either:

And you should get the following JSON response:

{
    "github": {
        "id": "github",
        "name": "GitHub",
        "type": "oauth",
        "signinUrl": "<http://localhost:3000/api/auth/signin/github>",
        "callbackUrl": "<http://localhost:3000/api/auth/callback/github>"
    }
}
Enter fullscreen mode Exit fullscreen mode

Apply NextAuth with Middleware

Link to the docs regarding matcher here

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:

// src/middleware.ts

export { default } from "next-auth/middleware"
Enter fullscreen mode Exit fullscreen mode

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:

// src/middleware.ts

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

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

Custom Sign In / Out Pages

Link to pages option in the docs here

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

// 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"
  }
};
Enter fullscreen mode Exit fullscreen mode

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:

// 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;
Enter fullscreen mode Exit fullscreen mode

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:

// 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;
Enter fullscreen mode Exit fullscreen mode

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:

// 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;
Enter fullscreen mode Exit fullscreen mode

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:

// 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;
Enter fullscreen mode Exit fullscreen mode

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

// 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;
Enter fullscreen mode Exit fullscreen mode

Next.js Images from an OAuth Provider

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

// 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;
Enter fullscreen mode Exit fullscreen mode

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:

// next.config.js

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

module.exports = nextConfig;
Enter fullscreen mode Exit fullscreen mode

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, which you can use as a reference in case you get stuck.

Cheers, and happy coding!

Top comments (0)