CodeNotes
Back to library
authenticationJune 1, 20267 min read

GitHub OAuth Integration

Secure your codebase with GitHub authentication, including step-by-step route handler codes.

GitHub OAuth Integration

Why third-party OAuth is used

Traditional registration forms require users to type usernames, email addresses, and passwords. Managing passwords introduces significant security overhead: hashing passwords correctly, building password-reset links, verifying email ownership, and dealing with credential leaks.

OAuth (Open Authorization) lets users log in using their existing profiles on trusted external platforms (like GitHub or Google). Instead of storing passwords, your application delegates the authentication step to the external provider.


Mental Model

Think of OAuth as the Valet Key for your car.

  • Traditional login: You give a stranger the keys to your entire house (your main password).
  • OAuth login: When you park at a restaurant, you do not hand the valet your house keys. You hand them a Valet Key (an Access Token). This key only turns on the engine and lets them park the car. It cannot open your glove box, open your trunk, or access your house.
  • In web authentication, the user grants your app a Valet Key from GitHub. This key only allows your server to inspect their public email and profile name. It cannot access their private code repositories or change their account passwords.
1. Redirect to GitHub
User clicks Login & approves permissions
2. Callback Code
GitHub redirects to server with Auth Code
3. Token Exchange
Server exchanges code for User Access Token

Core Specifications

  • Why it exists: It was created as a secure delegation protocol to allow applications to interact with GitHub user accounts and API data without directly handling account passwords.
  • What problem it solves: It eliminates the risk of password leaks and lets users grant fine-grained permissions (scopes) to third-party applications, keeping other account details completely private.
  • How it works internally: The application redirects the user's browser to GitHub with a list of requested scopes. After user consent, GitHub redirects the browser back to the app callback URL with an authorization code. The app's backend then exchanges this code for an access token directly with GitHub's servers.
  • When to use it: Use it when building developer tools, portfolio sites, issue tracking dashboards, and platforms requiring direct integration with GitHub repositories or organizations.
  • How it is used in real projects: CI/CD deployment systems exchange authorization codes for access tokens, then verify the token scopes in the backend to ensure write access exists before installing deployment hook webhooks.

Codebase Integration

Here is a step-by-step guide to implementing GitHub OAuth from scratch in a TypeScript backend.

Step 1: Register GitHub OAuth Application

  1. Go to GitHub Settings > Developer Settings > OAuth Apps > Register new application.
  2. Set your Homepage URL (e.g. http://localhost:3000).
  3. Set your Authorization callback URL (e.g. http://localhost:3000/api/auth/github/callback).
  4. Generate and save the Client ID and Client Secret.

Step 2: Configure Environment Variables

Open your .env file and save the credentials.

# .env (Environment config)
GITHUB_CLIENT_ID="your_github_client_id_here"
GITHUB_CLIENT_SECRET="your_github_client_secret_here"

Step 3: Redirect User to GitHub

In your client or backend router, create an endpoint to redirect the user to GitHub's authorization consent screen.

// filepath: src/app/api/auth/github/route.ts
// Purpose: Redirect the client to GitHub to start the OAuth consent flow.

import { NextResponse } from 'next/server';

export async function GET() {
  const rootUrl = 'https://github.com/login/oauth/authorize';
  
  const options = {
    client_id: process.env.GITHUB_CLIENT_ID || '',
    redirect_uri: 'http://localhost:3000/api/auth/github/callback',
    scope: 'user:email', // Request access to the user's email address
    state: 'random_security_state_string_123',
  };

  const qs = new URLSearchParams(options).toString();
  return NextResponse.redirect(`${rootUrl}?${qs}`);
}

Step 4: Handle the Redirect Callback

After the user approves your application, GitHub redirects the browser back to your callback URL, passing a temporary parameter named code. Your backend must exchange this code for an Access Token.

// filepath: src/app/api/auth/github/callback/route.ts
// Purpose: Receive authorization code, fetch user details, and log the user in.

import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');

  if (!code) {
    return NextResponse.json({ error: 'Authorization code missing' }, { status: 400 });
  }

  try {
    // 1. Exchange temporary Authorization Code for an Access Token
    const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        client_id: process.env.GITHUB_CLIENT_ID,
        client_secret: process.env.GITHUB_CLIENT_SECRET,
        code,
      }),
    });

    const tokenData = await tokenResponse.json();
    const accessToken = tokenData.access_token;

    if (!accessToken) {
      return NextResponse.json({ error: 'Failed to retrieve access token' }, { status: 401 });
    }

    // 2. Fetch User Profile using Access Token
    const userResponse = await fetch('https://api.github.com/user', {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });
    const userData = await userResponse.json();

    // 3. Fetch User Email address
    const emailResponse = await fetch('https://api.github.com/user/emails', {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });
    const emails = await emailResponse.json();
    const primaryEmail = emails.find((e: any) => e.primary)?.email || userData.email;

    // 4. Log user in by setting session cookie (or saving profile details in SQL DB)
    const response = NextResponse.redirect(new URL('/dashboard', request.url));
    response.cookies.set('session_token', accessToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      maxAge: 60 * 60 * 24, // 1 day
    });

    return response;
  } catch (error) {
    console.error('GitHub OAuth Callback Error:', error);
    return NextResponse.json({ error: 'OAuth exchange failure' }, { status: 500 });
  }
}

Practical Project Use Cases

1. Scope Audit Verification Before Creating Webhooks

In a developer dashboard or CI/CD runner (like Vercel or Travis CI), your server might need permission to create hooks on user repositories. If your app attempts to create a webhook using a token with insufficient scopes, the GitHub API will return an unhelpful 403 Forbidden error.

  • Real-World Example: We request a minimal profile scope at initial login to lower user friction, but audit granted scopes dynamically in the backend using GitHub response headers. If a required scope (like write:repo_hook) is missing, we direct the user to an elevated consent screen.
// filepath: src/services/githubScopeAuditor.ts
// Purpose: Audit access token scopes dynamically before triggering GitHub webhook creation hooks.

export async function auditAndCreateWebhook(
  repoOwner: string, 
  repoName: string, 
  accessToken: string
) {
  // 1. Call GitHub API to fetch rate limits or profile, which returns scope headers
  const checkResponse = await fetch('https://api.github.com/user', {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      Accept: 'application/vnd.github+json',
    },
  });

  // GitHub reports active scopes in the 'X-OAuth-Scopes' header (e.g., "user:email, read:org")
  const scopesHeader = checkResponse.headers.get('x-oauth-scopes') || '';
  const grantedScopes = scopesHeader.split(',').map((s) => s.trim());

  const hasRequiredScope = grantedScopes.includes('write:repo_hook') || grantedScopes.includes('admin:repo_hook');

  if (!hasRequiredScope) {
    // Signal to the frontend to request elevated authorization from the user
    return {
      success: false,
      requiresUpgrade: true,
      upgradeUrl: `https://github.com/login/oauth/authorize?client_id=${
        process.env.GITHUB_CLIENT_ID
      }&scope=user:email,write:repo_hook&redirect_uri=${encodeURIComponent(
        'http://localhost:3000/api/auth/github/callback'
      )}`,
    };
  }

  // 2. Perform webhook creation since authorization scope is confirmed
  const webhookResponse = await fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/hooks`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      Accept: 'application/vnd.github+json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: 'web',
      active: true,
      events: ['push'],
      config: {
        url: 'https://codenotes.dev/api/webhooks/github',
        content_type: 'json',
        secret: process.env.GITHUB_WEBHOOK_SECRET,
      },
    }),
  });

  return {
    success: webhookResponse.status === 201,
    requiresUpgrade: false,
  };
}

2. Auto-linking Issues and Pull Requests

When users connect their GitHub profile to an issue-tracking tool (like Jira or Linear), the application can parse commit messages for patterns like Fixes #10 and automatically close related tickets inside the database.

  • Real-World Example: When a webhook notifies our server of a new GitHub commit, we map the author's GitHub ID back to our internal user ID and update the status of the associated ticket.

Common Pitfalls

1. Hardcoding the Redirect Callback URL

If you hardcode http://localhost:3000 inside your production API redirect code, the application will redirect production users to their local machine instead of your website domain.

  • Fix: Use environment variables to dynamicize callbacks (e.g. process.env.APP_URL).

2. Leaking Client Secret to Frontend Code

The GITHUB_CLIENT_SECRET must never be used in frontend files (like src/app/page.tsx). Anyone inspecting network requests could steal your key and forge logins.

  • Fix: Code the token-exchange step purely on your server backend endpoints.

Interview Questions

What is the purpose of the state parameter in OAuth?

The state parameter is a unique random string generated by the server. It prevents CSRF (Cross-Site Request Forgery) attacks. The server stores this string locally, passes it to the provider, and verifies that the returning callback contains the identical state value before running token validation.


Summary

  • Role: Stateless identity delegation avoiding storing user passwords.
  • Exchange: Exchanges code for user tokens via secure backend channels.
  • Security: Client secrets must stay hidden in backend-only .env files.

Keep Learning

authentication

Google OAuth Integration

Configure Google login inside a TypeScript codebase, with step-by-step route endpoints configurations.

7 min readRead
authentication

Clerk Authentication

Implement managed Clerk authentication inside your Next.js application, including client and route middleware configurations.

6 min readRead