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

Common Mistakes

1. Not verifying the state parameter

The most common OAuth security mistake. Without state verification, your callback endpoint is vulnerable to CSRF:

// WRONG — no state check
route.get("/auth/github/callback", {
  resolve: async (c) => {
    const code = new URL(c.request.url).searchParams.get("code");
    // Exchange code for token immediately...
  },
});

// CORRECT — verify state before doing anything
route.get("/auth/github/callback", {
  resolve: async (c) => {
    const url = new URL(c.request.url);
    const state = url.searchParams.get("state");
    if (!state || !verifyState(state)) {
      return Response.json({ error: "Invalid state" }, { status: 403 });
    }
    // Now proceed...
  },
});

Without state, an attacker can craft a callback URL with their own authorization code and trick a victim into visiting it. The victim ends up logged in as the attacker.

2. Exposing the client secret

The client secret must only appear in server-to-server requests. If it ends up in client-side code, browser-visible URLs, or public repos, anyone can impersonate your server.

// WRONG — secret in client-side code
const params = new URLSearchParams({
  client_id: "...",
  client_secret: "...", // ← this is in the browser redirect URL
});

// CORRECT — secret only in server-to-server fetch
const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
  method: "POST",
  body: JSON.stringify({
    client_id: env.githubClientId,
    client_secret: env.githubClientSecret, // ← server-to-server only
    code,
  }),
});

If you suspect your secret has been exposed, regenerate it immediately in the provider’s settings.

3. Storing access tokens in the browser

The access token is for your server to call the provider’s API. It should never be sent to the browser:

// WRONG — returning the access token to the client
return Response.json({ accessToken, user });

// CORRECT — keep the token server-side, only return user data
return Response.json({ user });

If the access token reaches the browser, any XSS attack can steal it and use it to access the user’s provider data.

4. Auto-linking by unverified email

If a provider returns an email without verifying it, do not use it for account linking:

// WRONG — trusting any email from the provider
const email = githubUser.email; // might be unverified!
const existing = findByEmail(email);
if (existing) existing.githubId = githubUser.id; // account takeover risk

// CORRECT — only trust verified emails
const email = getPrimaryEmail(githubEmails); // checks verified flag

An attacker could create a GitHub account with someone else’s email (without verifying it) and use your auto-linking to take over the victim’s account.

5. Not validating the redirect_uri

Always construct the redirect_uri from a trusted source (your environment variable), not from the request:

// WRONG — using a URL from the request
const redirectUri = new URL(c.request.url).origin + "/auth/github/callback";

// CORRECT — using your configured base URL
const redirectUri = `${env.baseUrl}/auth/github/callback`;

If the request URL is spoofed (through a reverse proxy misconfiguration or host header injection), the attacker could redirect the authorization code to their own server.

6. Not handling all error paths in the callback

The callback can receive error parameters (user denied consent), missing parameters (malformed redirect), or empty state (CSRF attempt). If you only handle the happy path, users get cryptic error pages or the server crashes:

// WRONG — only handling the happy path
const code = url.searchParams.get("code")!; // crashes if null

// CORRECT — checking each step
const error = url.searchParams.get("error");
if (error) return handleError(error);

const code = url.searchParams.get("code");
if (!code) return handleError("missing_code");

7. Committing .env files

This happens more than you think. The .env file contains your client secret. If it is committed to a public repository, anyone can read it.

# .gitignore — add this before your first commit
.env

If you accidentally committed a .env file, deleting it from the repository is not enough. The secret is in the Git history. Regenerate the secret immediately.

Summary

MistakeRiskFix
No state verificationCSRF login attackAlways generate, store, and verify state
Exposed client secretAttacker impersonates your serverServer-to-server only, .env file, gitignore
Access token in browserToken theft via XSSKeep tokens server-side
Auto-link unverified emailAccount takeoverOnly link on verified emails
Redirect URI from requestCode theft via redirectUse env-configured base URL
Missing error handlingCrashes and bad UXCheck every callback parameter
Committed .envSecret exposuregitignore, regenerate if leaked

An attacker creates a GitHub account with [email protected] (unverified). They log in to your app. Later, the real owner of that email logs in with Google. What happens if you auto-link by unverified email?

← Logout and Token Cleanup Capstone: Multi-Provider Login Page →

© 2026 hectoday. All rights reserved.