Integrate GitHub OAuth With NextAuth.js in Next.js 13 With Custom Sign In / Out Pages
Andrew Shearer
Posted on September 16, 2023
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 .
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;
}
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"]
}
Update src/app/page.tsx
to this:
// src/app/page.tsx
const HomePage = () => {
return (
<div>
<h1>HomePage</h1>
</div>
);
};
export default HomePage;
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;
Install the NextAuth
package:
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:
// 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.
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: []
};
NEXTAUTH_SECRET
Let’s pause here, and in a terminal window run this command:
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
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=""
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'
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!
})
]
};
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;
// src/app/sign-out/page.tsx
const SignOutPage = () => {
return (
<div>
<h1>SignOutPage</h1>
</div>
);
};
export default SignOutPage;
// 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:
// 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:
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 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:
{
"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
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"
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"] };
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"
}
};
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;
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;
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;
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;
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;
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;
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;
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!
Posted on September 16, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 16, 2023