Web Development

Understanding GitHub OAuth Authentication in Next.js (Part 1)

In this article, we'll build GitHub OAuth Authentication from scratch without relying on authentication libraries like Auth.js or NextAuth. The goal is not just to get it working, but to understand what actually happens behind the scenes whenever a user clicks "Continue with GitHub".

KUZUE
July 3, 2026
4 min read
16 views

In this article, we'll build GitHub OAuth Authentication from scratch without relying on authentication libraries like Auth.js or NextAuth.

The goal is not just to get it working, but to understand what actually happens behind the scenes whenever a user clicks "Continue with GitHub."

I recommend keeping the GitHub OAuth documentation open while following along.

GitHub OAuth Documentation: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps


Registering Your GitHub OAuth Application

Before writing any code, you need to register your application on GitHub.

Every registered OAuth application receives two credentials:

  • Client ID
  • Client Secret

The Client ID identifies your application, while the Client Secret is used later to securely exchange an authorization code for an access token. Because it is a secret, it should never be exposed on the client.

Navigate to:

text
GitHub
 └── Profile
      └── Settings
            └── Developer settings
                  └── OAuth Apps
                        └── New OAuth App

Most of the fields can be filled in however you like, but one field deserves special attention:

Authorization Callback URL

This is the URL GitHub redirects users back to after they successfully authenticate.

For example:

text
http://localhost:3000/Oauth/github/callback

GitHub only redirects to callback URLs that you've registered. This prevents attackers from redirecting users to malicious websites and stealing authorization codes.


GitHub's Authorization Endpoint

GitHub exposes an authorization endpoint that starts the OAuth flow.

text
GET https://github.com/login/oauth/authorize

This endpoint accepts several query parameters.

| Parameter | Description | |-----------|-------------| | client_id | The Client ID assigned to your GitHub OAuth application. | | state | A randomly generated string used to prevent Cross-Site Request Forgery (CSRF) attacks. | | redirect_uri | The callback URL in your application where GitHub should redirect the user after authentication. | | scope | The permissions your application is requesting, such as user:email, repo, or read:org. |


Our OAuth Routes

We'll create two API routes.

text
Oauth/
└── github/
      ├── authorize/
      │      └── route.ts
      │
      └── callback/
             └── route.ts

The name of the authorize route is completely up to you.

However, the callback route must match the callback URL you registered in your GitHub Developer Settings.


Building the Authorization Route

When a user clicks "Continue with GitHub", this route is responsible for:

  1. Reading your GitHub credentials from the environment.
  2. Building the callback URL.
  3. Generating a random state value.
  4. Storing the state inside an HTTP-only cookie.
  5. Constructing GitHub's authorization URL.
  6. Redirecting the user's browser to GitHub.

One important thing to notice is the use of encodeURIComponent().

We use encodeURIComponent() whenever we're inserting a dynamic URL into another URL. It converts special characters into a safe format so the browser doesn't confuse them with other query parameters.

ts
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";

export async function GET(req: NextRequest) {
  try {
    const client_id = process.env.GITHUB_CLIENT_ID;
    const client_secret = process.env.GITHUB_CLIENT_SECRET;

    if (!client_id || !client_secret) {
      return NextResponse.json({
        success: false,
        message:
          "client_id or client_secret is missing. Configure them in your .env file."
      });
    }

    const redirectUri = `${req.nextUrl.origin}/Oauth/github/callback`;

    const state = crypto.randomUUID();

    const cookieStore = await cookies();

    const isProduction = process.env.NODE_ENV === "production";

    cookieStore.set("oauth_state", state, {
      httpOnly: true,
      secure: isProduction,
      sameSite: "lax",
      path: "/",
      maxAge: 600,
    });

    const gitHubAuthorizeUrl =
      `https://github.com/login/oauth/authorize?client_id=${client_id}&redirect_uri=${encodeURIComponent(
        redirectUri
      )}&state=${state}&scope=user:email`;

    return NextResponse.redirect(gitHubAuthorizeUrl);

  } catch (error) {
    console.error(error);

    return NextResponse.json(
      {
        success: false,
        error: "Internal server error",
      },
      { status: 500 }
    );
  }
}

What Happens Next?

Once the browser is redirected to GitHub, one of two things happens.

If the user is not already signed in, GitHub asks for their username, password, and any required two-factor authentication.

After a successful login, GitHub displays a consent screen asking the user whether they want to grant your application access to the requested permissions.

For example:

My Awesome App wants to:

  • Read your email address

Cancel | Authorize

If the user clicks Authorize, GitHub redirects them back to the callback URL we configured earlier.

In the next article, we'll build the callback route, exchange the authorization code for an access token, retrieve the user's GitHub profile, and finally create a session in our own application.

Comments (0)

No comments yet. Be the first to share your thoughts.