Auth.js tutorial for Nextra and Next.js with GitHub
These are my random notes about the tutorial OAuth tutorial (opens in a new tab) that teaches how setting Auth.js 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
export { default } from "next-auth/middleware"
//export const config = { matcher: ["/clases", "/labs"] }
- See the section Middleware (opens in a new tab) on the Next.js docs
- See also https://next-auth.js.org/configuration/nextjs#basic-usage (opens in a new tab)
Creating the server config
Create the following API route (opens in a new tab) file. 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,
}),
],
})
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.
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
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 authjs section:
https://authjs.dev/getting-started/providers/oauth-tutorial#2-configuring-oauth-provider (opens in a new tab)
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 main
fields defines 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 a SessionProvider
component:
main: ({ children }) => { // See https://github.com/shuding/nextra/discussions/1508#discussioncomment-4990229
// eslint-disable-next-line react-hooks/rules-of-hooks
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>
);
},
I've got the idea from this discussion (opens in a new tab) at the Nextra github repo.
Here is the full code of theme.config.tsx
:
const config: DocsThemeConfig = {
logo: <span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="w-6 h-6">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
/>
</svg>
</span>,
project: {
link: 'https://github.com/ULL-ESIT-PL-2324',
},
chat: {
link: 'https://meet.google.com/eha-yfij-zmo',
icon: <svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
width="20"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="w-6 h-6">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>
},
docsRepositoryBase: 'https://github.com/ULL-ESIT-PL-2324/pl-nextra/blob/main/',
footer: {
text: 'Notes for the Computer Science degree ULL 23/24 course on Programming Languages',
},
main: ({ children }) => { // See https://github.com/shuding/nextra/discussions/1508#discussioncomment-4990229
// eslint-disable-next-line react-hooks/rules-of-hooks
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>
);
},
/*
components: { // See https://nextra.site/docs/docs-theme/theme-configuration#mdx-components
SessionProvider, //????XXXX
}
*/
}
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 aboout 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