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:userlets us read the user’s public profile (name, avatar, bio)user:emaillets 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?