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
| Mistake | Risk | Fix |
|---|---|---|
| No state verification | CSRF login attack | Always generate, store, and verify state |
| Exposed client secret | Attacker impersonates your server | Server-to-server only, .env file, gitignore |
| Access token in browser | Token theft via XSS | Keep tokens server-side |
| Auto-link unverified email | Account takeover | Only link on verified emails |
| Redirect URI from request | Code theft via redirect | Use env-configured base URL |
| Missing error handling | Crashes and bad UX | Check every callback parameter |
| Committed .env | Secret exposure | gitignore, 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?