Auth.js tutorial for Nextra and Next.js with GitHub
These are my notes about the tutorial OAuth tutorial (opens in a new tab) that teaches how setting Auth.js (opens in a new tab) in a web application to be able to log in with GitHub.
Introduction. The concepts
Without going into too much detail, the OAuth flow generally has 6 parts:
- The application requests authorization to access service resources from the user
- If the user authorized the request, the application receives an authorization grant
- The application requests an access token from the authorization server (API) by presenting authentication of its own identity, and the authorization grant
- If the application identity is authenticated and the authorization grant is valid, the authorization server (API) issues an access token to the application. Authorization is complete.
- The application requests the resource from the resource server (API) and presents the access token for authentication
- If the access token is valid, the resource server (API) serves the resource to the application
Installing NextAuth.js
npm install next-auth
Middleware
Middleware allows you to run code before a request is completed. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly.
Use the file middleware.ts
(or middleware.js
) in the root of your project to define
Middleware.
For example, at the same level as pages
or app
, or inside src
if applicable.
export { default } from "next-auth/middleware"
export const config = { matcher: ["/clases", "/labs"] }
When we want to secure certain pages, we export a config
object with a matcher
property.
Now we will still be able to visit every page, but only /clases
and /labs
will require authentication
but I can visit /topics
.
On platforms like Vercel, Middleware is run at the Edge (opens in a new tab).
- See the section Middleware (opens in a new tab) on the next-auth.js docs
- See also https://next-auth.js.org/configuration/nextjs#basic-usage (opens in a new tab)
- https://nextjs.org/docs/pages/building-your-application/routing/middleware (opens in a new tab)
Creating the server config
Create the following
API route (opens in a new tab) file pages/api/auth/[...nextauth].js
.
This route contains the necessary configuration for NextAuth.js, as well as the dynamic route handler:
import NextAuth from "next-auth"
import GithubProvider from "next-auth/providers/github"
export default NextAuth({
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
})
pages/api
Any file inside the folder pages/api
is mapped to
/api/*
and will be treated as an API endpoint instead of a page.
They are server-side only bundles. For instance:
➜ pl2324-apuntes git:(main) ✗ tree pages/api
pages/api
├── auth
│ └── [...nextauth].js
└── getteams.js
A request in a client component like /components/teams.jsx
executing a code like:
const { data, error } = useSWR('/api/getteams', fetcher)
will be handled by the file pages/api/getteams.js
in the server side.
dots in the file name
When you add ...
inside brackets (like [...nextauth]
),
it tells Next.js to create a catch-all route.
This allows the route to match multiple segments in the URL after /api/auth/
.
In this case, the route pages/api/auth/[...nextauth].js
would match:
/api/auth/
(the base route)/api/auth/signin
/api/auth/signout
/api/auth/callback/google
- And so on...
Routes
Behind the scenes, this creates all the relevant OAuth API routes within /api/auth/*
so that auth API requests to:
- GET
/api/auth/signin
(opens in a new tab) - POST
/api/auth/signin/:provider
(opens in a new tab) - GET/POST
/api/auth/callback/:provider
(opens in a new tab) - GET
/api/auth/signout
(opens in a new tab) - POST
/api/auth/signout
(opens in a new tab) - GET
/api/auth/session
(opens in a new tab) - GET
/api/auth/csrf
(opens in a new tab)- CSRF stands for Cross-Site Request Forgery, which is a type of security threat in web applications. It's not directly a part of authorization mechanisms but rather a vulnerability that can occur when proper authorization controls are not in place or are circumvented.
- GET
/api/auth/providers
(opens in a new tab)
can be handled by NextAuth.js. In this way, NextAuth.js stays in charge of the whole application's authentication request/response flow.
NextAuth.js is customizable. The NextAuth guides (opens in a new tab) teaches you how to set it up to handle auth in different ways. All the possible configuration options are listed here (opens in a new tab).
Adding environment variables: Development
Notice we are using environment variables in the code example above.
We take the value of GITHUB_ID
and GITHUB_SECRET
from the GitHub Developer OAuth Portal.
See Configuring OAuth Provider (opens in a new tab) section on how to get those.
If you haven’t used OAuth before, you can read the beginners step-by-step guide on how to setup "Sign in with GitHub" with Auth.js (opens in a new tab).
In your project root, create a .env.local
file and add the NEXTAUTH_SECRET
environment variable:
NEXTAUTH_SECRET="This is an example"
NEXTAUTH_SECRET
is a random string used by the library to encrypt tokens and email verification hashes,
and it's mandatory to keep things secure! 🔥 🔐 . You can use
openssl rand -base64 32 (opens in a new tab)
which generates 32 random bytes using a cryptographically secure pseudo random number generator
(CSPRNG) and performs base64 (opens in a new tab) encoding on the output.
openssl rand -base64 32
or https://generate-secret.vercel.app/32 (opens in a new tab) to generate a random value for it.
The source of this section is at Auth.js sections
Adding environment variables: Production. Vercel
In production, you can set environment variables in your hosting dashboard. For example, on Vercel, you can set them in the Environment Variables ↗️ (opens in a new tab) section of your project's settings.
When setting the variables for vercel it is easy to copy-paste the values from the .env.local
file. This leads to easily copy the protocol as 'http' for the Authorization callback URL instead of 'https'. Make sure to use the correct values:
Adding environment variables: Production. Netlify
In production, you can set environment variables in your hosting dashboard. For example, on Netlify, you can set them in the Environment Variables ↗️ (opens in a new tab) section of your project's settings.
See https://authjs.dev/reference/core/providers/netlify#resources (opens in a new tab)
Nextra: Wrapping with SessionProvider
The theme.config.tsx
configuration file contains a main
field which
has to be a React Functional component React.FC<{ children: React.ReactNode }>
.
Here we use the main
entry to add a title
and description
and wrap the children
with an instance of the SessionProvider (opens in a new tab) component
provided by next-auth/react
import { useConfig } from 'nextra-theme-docs'
import { SessionProvider } from "next-auth/react"
export default config: DocsThemeConfig = {
banner: { /* ... */ },
/* ... */
main: ({ children }) => { // See https://github.com/shuding/nextra/discussions/1508#discussioncomment-4990229
const { frontMatter } = useConfig();
return (
<SessionProvider>
<main className="">
<h1 className="nx-mt-2 nx-text-4xl nx-font-bold nx-tracking-tight">
{frontMatter?.title}
</h1>
<p>{frontMatter?.description}</p>
<div className="">{children}</div>
</main>
</SessionProvider>
);
},
}
The SessionProvider
component provides a
React Context (opens in a new tab)
provider to wrap the web app (pages/
) to make session data available anywhere.
When used, the session state is automatically synchronized across all open tabs/windows and
they are all updated whenever they gain or lose focus
or the state changes (e.g. a user signs in or out)
when refetchOnWindowFocus (opens in a new tab) is true
.
SessionProvider
is for client-side use only and when using Next.js App Router (app/
) (opens in a new tab) you should prefer the auth()
export.
See the code of the SessionProvider
component at file
/nextauthjs/next-auth/packages/next-auth/src/react.tsx) (opens in a new tab).
Here is the full code of theme.config.tsx (opens in a new tab) for this web site.
Original idea
I've got the idea for this approach to authentication with Nextra from this discussion (opens in a new tab) at the Nextra github repo.
The signOut() method
- Client Side: Yes
- Server Side: No
In order to logout, use the signOut()
method to ensure the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
It reloads the page in the browser when complete.
import { signOut } from "next-auth/react"
export default () => <button onClick={() => signOut()}>Sign out</button>
See an example in this repo at page user.mdx (opens in a new tab) and the component user.jsx (opens in a new tab)
Scopes
One of my goals is to get from GitHub additional information about the user. Help is needed here.
See
- https://next-auth.js.org/configuration/providers/oauth#authorization-option (opens in a new tab)
- https://github.com/nextauthjs/next-auth/discussions/4557 (opens in a new tab)
authorization: {
url: "https://example.com/oauth/authorization",
params: { scope: "email" }
}
File next-auth/providers/github.ts
The next-auth code for the types of the GitHub provider is at https://github.com/nextauthjs/next-auth/blob/v4/packages/next-auth/src/providers/github.ts (opens in a new tab)
export interface GithubProfile extends Record<string, any> {
login: string
id: number
node_id: string
...
email: string | null
}
export default function Github<P extends GithubProfile>(options: OAuthUserConfig<P>): OAuthConfig<P> {
return {
id: "github",
name: "GitHub",
type: "oauth",
authorization: {
url: "https://github.com/login/oauth/authorize",
params: { scope: "read:user user:email" },
},
token: "https://github.com/login/oauth/access_token",
userinfo: {
url: "https://api.github.com/user",
async request({ client, tokens }) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const profile = await client.userinfo(tokens.access_token!)
if (!profile.email) {
// If the user does not have a public email, get another via the GitHub API
// See https://docs.github.com/en/rest/users/emails#list-public-email-addresses-for-the-authenticated-user
const res = await fetch("https://api.github.com/user/emails", {
headers: { Authorization: `token ${tokens.access_token}` },
})
if (res.ok) {
const emails: GithubEmail[] = await res.json()
profile.email = (emails.find((e) => e.primary) ?? emails[0]).email
}
}
return profile
},
},
profile(profile) {
return {
id: profile.id.toString(),
name: profile.name ?? profile.login,
email: profile.email,
image: profile.avatar_url,
}
},
style: { logo: "/github.svg", bg: "#24292f", text: "#fff" },
options,
}
Whenever you configure a custom or a built-in OAuth provider, you have the following options available:
interface OAuthConfig {
/**
* OpenID Connect (OIDC) compliant providers can configure
* this instead of `authorize`/`token`/`userinfo` options
* without further configuration needed in most cases.
* You can still use the `authorize`/`token`/`userinfo`
* options for advanced control.
*
* [Authorization Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414#section-3)
*/
wellKnown?: string
/**
* The login process will be initiated by sending the user to this URL.
*
* [Authorization endpoint](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1)
*/
authorization: EndpointHandler<AuthorizationParameters>
/**
* Endpoint that returns OAuth 2/OIDC tokens and information about them.
* This includes `access_token`, `id_token`, `refresh_token`, etc.
*
* [Token endpoint](https://datatracker.ietf.org/doc/html/rfc6749#section-3.2)
*/
token: EndpointHandler<
UrlParams,
{
/**
* Parameters extracted from the request to the `/api/auth/callback/:providerId` endpoint.
* Contains params like `state`.
*/
params: CallbackParamsType
/**
* When using this custom flow, make sure to do all the necessary security checks.
* This object contains parameters you have to match against the request to make sure it is valid.
*/
checks: OAuthChecks
},
{ tokens: TokenSet }
>
/**
* When using an OAuth 2 provider, the user information must be requested
* through an additional request from the userinfo endpoint.
*
* [Userinfo endpoint](https://www.oauth.com/oauth2-servers/signing-in-with-google/verifying-the-user-info)
*/
userinfo?: EndpointHandler<UrlParams, { tokens: TokenSet }, Profile>
type: "oauth"
/**
* Used in URLs to refer to a certain provider.
* @example /api/auth/callback/twitter // where the `id` is "twitter"
*/
id: string
version: string
profile(profile: P, tokens: TokenSet): Awaitable<User>
checks?: ChecksType | ChecksType[]
clientId: string
clientSecret: string
/**
* If set to `true`, the user information will be extracted
* from the `id_token` claims, instead of
* making a request to the `userinfo` endpoint.
*
* `id_token` is usually present in OpenID Connect (OIDC) compliant providers.
*
* [`id_token` explanation](https://www.oauth.com/oauth2-servers/openid-connect/id-tokens)
*/
idToken?: boolean
region?: string
issuer?: string
client?: Partial<ClientMetadata>
allowDangerousEmailAccountLinking?: boolean
/**
* Object containing the settings for the styling of the providers sign-in buttons
*/
style: ProviderStyleType
}
Authorization option
Configure how to construct the request to the Authorization endpoint ↗ (opens in a new tab).
There are two ways to use this option:
- You can either set authorization to be a full URL, like
"https://example.com/oauth/authorization?scope=email"
. - Use an object with
url
andparams
like so
authorization: {
url: "https://example.com/oauth/authorization",
params: { scope: "email" }
}
If your Provider is OpenID Connect (OIDC) compliant, we recommend using the wellKnown option instead.
References
- nextauthjs/docs (opens in a new tab) repo with the docs