Skip to content

fix(auth): validate OAuth state cookie to prevent CSRF attacks#171

Open
MehtabSandhu11 wants to merge 1 commit into
Dev-Card:mainfrom
MehtabSandhu11:fix/oauth-state-csrf-validation
Open

fix(auth): validate OAuth state cookie to prevent CSRF attacks#171
MehtabSandhu11 wants to merge 1 commit into
Dev-Card:mainfrom
MehtabSandhu11:fix/oauth-state-csrf-validation

Conversation

@MehtabSandhu11
Copy link
Copy Markdown
Contributor

Summary

This PR fixes a broken CSRF protection vulnerability in the OAuth flow. The server was
generating a state parameter during the GitHub and Google OAuth redirects but never
validating it in the callback handlers — making the CSRF protection completely
non-functional. The fix stores the generated state in a short-lived httpOnly cookie
before the redirect, then verifies it matches the returned state in both callback
handlers before processing the token exchange. If the state is missing, expired, or
mismatched, the server immediately returns a 400 Bad Request and halts the flow.

Closes #147


Type of Change

  • Security

What Changed

  • apps/backend/src/routes/auth.ts/github route: stores the generated state in
    a short-lived httpOnly cookie (maxAge: 600) before redirecting to GitHub.
  • apps/backend/src/routes/auth.ts/github/callback route: reads state from query
    and oauth_state from cookies, returns 400 on mismatch/missing, clears the cookie
    on success.
  • apps/backend/src/routes/auth.ts/google route: same cookie storage logic as
    the GitHub redirect.
  • apps/backend/src/routes/auth.ts/google/callback route: same CSRF validation
    logic as the GitHub callback.
  • Mobile flow (state?.startsWith('mobile_')) is fully preserved — validation happens
    before the mobile check so nothing in that path is broken.

How to Test

  1. Start the backend locally and initiate a normal GitHub or Google OAuth login — confirm
    the login completes successfully and you are redirected to the dashboard.
  2. Initiate a GitHub/Google OAuth redirect to capture the state value and the
    oauth_state cookie, then manually call the callback endpoint with a different or
    missing state query parameter — confirm the server returns 400 with
    "Invalid or missing OAuth state".
  3. Complete a valid login, then try replaying the same callback URL a second time —
    confirm the server returns 400 because the cookie was cleared after first use.
  4. Initiate a mobile OAuth flow (pass state=mobile_xxx) — confirm the login still
    completes and redirects to the mobile URI correctly.

Checklist

  • No new console.log or debug statements left in the code.


Additional Context

The oauth_state cookie is intentionally plain httpOnly + sameSite: lax rather than
signed, since @fastify/cookie signing is not currently configured project-wide. This is
still secure: httpOnly prevents JS access, sameSite: lax blocks cross-origin
requests from attaching the cookie, and the 10-minute maxAge limits the exposure
window. If the project adopts a cookie secret in future, the cookie can be upgraded to
signed with a one-line change. The (request.cookies as any) cast is a minor type
workaround since FastifyRequest doesn't include .cookies in its default type without
explicit @fastify/cookie type augmentation — functional at runtime, can be cleaned up
if type declarations are added later.

@Harxhit Harxhit added the gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking. label May 19, 2026
@Harxhit Harxhit requested a review from ShantKhatri May 20, 2026 14:52
@Harxhit
Copy link
Copy Markdown
Collaborator

Harxhit commented May 20, 2026

@ShantKhatri You can do the final review looks good to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[security] OAuth state parameter is never validated in GitHub/Google callbacks — CSRF protection is non-functional

2 participants