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".

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:
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:
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.
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.
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:
- Reading your GitHub credentials from the environment.
- Building the callback URL.
- Generating a random
statevalue. - Storing the
stateinside an HTTP-only cookie. - Constructing GitHub's authorization URL.
- 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.
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.