OAuth authentication with oauth.js for Next.js

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:

  1. The application requests authorization to access service resources from the user
  2. If the user authorized the request, the application receives an authorization grant
  3. The application requests an access token from the authorization server (API) by presenting authentication of its own identity, and the authorization grant
  4. 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.
  5. The application requests the resource from the resource server (API) and presents the access token for authentication
  6. 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.js
export { default } from "next-auth/middleware"
//export const config = { matcher: ["/clases", "/labs"] }

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:

pages/api/auth/[...nextauth].js
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:

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:

.env.local
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:

theme.config.tsx
  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:

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

  1. https://next-auth.js.org/configuration/providers/oauth#authorization-option (opens in a new tab)
  2. 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:

  1. You can either set authorization to be a full URL, like "https://example.com/oauth/authorization?scope=email".
  2. Use an object with url and params 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