hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses OAuth and Social Login

Why OAuth?

  • The Problem with Passwords
  • OAuth 2.0 in Plain English
  • The Authorization Code Flow, Step by Step
  • Project Setup

GitHub Login

  • Register a GitHub OAuth App
  • The Authorization Redirect
  • The State Parameter
  • The Callback Handler
  • Fetching the User Profile
  • Creating or Linking Accounts
  • The Complete Flow

Google Login

  • Register a Google OAuth App
  • Building Google Login

Production Concerns

  • Multiple Providers, One User
  • Combining OAuth with Password Auth
  • Error Handling
  • Logout and Token Cleanup
  • Common Mistakes
  • Capstone: Multi-Provider Login Page

The Authorization Redirect

The first route

When the user clicks “Log in with GitHub,” the browser hits GET /auth/github. This route does one thing: redirect the user to GitHub’s authorization page.

Create src/routes/github.ts:

// src/routes/github.ts
import { route, group } from "@hectoday/http";
import { env } from "../env.js";

export const githubRoutes = group([
  route.get("/auth/github", {
    resolve: (c) => {
      const state = crypto.randomUUID();

      // TODO: store state so we can verify it in the callback

      const params = new URLSearchParams({
        client_id: env.githubClientId,
        redirect_uri: `${env.baseUrl}/auth/github/callback`,
        scope: "read:user user:email",
        state,
      });

      const url = `https://github.com/login/oauth/authorize?${params}`;

      return Response.redirect(url, 302);
    },
  }),
]);

Let’s walk through each piece.

Building the authorization URL

The URL has a base (https://github.com/login/oauth/authorize) and four query parameters.

client_id: Your app’s public identifier. GitHub uses this to look up your app’s name, logo, and registered callback URL.

redirect_uri: Where GitHub should send the user after they approve. We build this from env.baseUrl so it works in both development (http://localhost:3000) and production.

scope: What permissions you are requesting. We ask for two:

  • read:user lets us read the user’s public profile (name, avatar, bio)
  • user:email lets us read the user’s email addresses (including private ones)

Scopes are provider-specific. GitHub’s scopes are documented at docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps. Request only what you need. Users see the scopes on the consent screen, and broad permissions make them hesitant to approve.

state: A random string for CSRF protection. We generate it with crypto.randomUUID() and will verify it in the callback. The next lesson explains why this matters.

URLSearchParams

We use URLSearchParams to build the query string. This is a Web Standard API that handles encoding special characters in parameter values. Without it, you would have to manually encode the redirect_uri (which contains :// and /).

const params = new URLSearchParams({
  client_id: "abc",
  redirect_uri: "http://localhost:3000/auth/github/callback",
});

console.log(params.toString());
// "client_id=abc&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fgithub%2Fcallback"

Response.redirect

Response.redirect(url, 302) creates a Response with a 302 Found status and a Location header pointing to the URL. The browser follows the redirect automatically.

This is a Web Standard method on the Response class. No framework helper needed.

Wire it into the app

Update src/app.ts:

// src/app.ts
import { setup, route } from "@hectoday/http";
import { githubRoutes } from "./routes/github.js";

export const app = setup({
  routes: [
    route.get("/", {
      resolve: () =>
        new Response(`<h1>OAuth Course</h1><p><a href="/auth/github">Log in with GitHub</a></p>`, {
          headers: { "content-type": "text/html" },
        }),
    }),

    route.get("/health", {
      resolve: () => Response.json({ status: "ok" }),
    }),

    ...githubRoutes,
  ],
});

Try it

With your server running and your .env filled in, visit http://localhost:3000 and click “Log in with GitHub.”

You should be redirected to GitHub’s authorization page. It will show your app name and the scopes you requested. If you click “Authorize,” GitHub will redirect you to /auth/github/callback, which will 404 because we have not built it yet.

That is expected. We build the callback handler in two lessons (after covering the state parameter).

The state problem

There is a gap in the current code. We generate a state value but we have a TODO where we need to store it. Without storing it, we cannot verify it in the callback, and the state parameter is useless.

The next lesson explains what the state parameter protects against and how to store and verify it.

Exercises

Exercise 1: Open your browser’s network tab (Developer Tools > Network) before clicking “Log in with GitHub.” Watch the redirect. You should see a 302 response from your server with a Location header pointing to GitHub. Click on the request and inspect the full redirect URL to see all four parameters.

Exercise 2: Try removing the scope parameter from the URL. Log in and see what GitHub shows on the consent screen. Without explicit scopes, GitHub grants only public profile access by default.

Why do we use URLSearchParams to build the authorization URL instead of string concatenation?

← Register a GitHub OAuth App The State Parameter →

© 2026 hectoday. All rights reserved.