Skip to content

fix: embed PKCE code_verifier in signed OAuth state parameter#72

Merged
fatherlinux merged 1 commit into
mainfrom
fix/oauth-pkce-signed-state
May 3, 2026
Merged

fix: embed PKCE code_verifier in signed OAuth state parameter#72
fatherlinux merged 1 commit into
mainfrom
fix/oauth-pkce-signed-state

Conversation

@fatherlinux
Copy link
Copy Markdown
Member

Summary

  • Eliminates recurring "Missing code verifier" OAuth error by moving PKCE code_verifier out of cookie-based sessions and into the OAuth state parameter, signed with itsdangerous.URLSafeTimedSerializer
  • Google echoes state back in the redirect URL — guaranteed to survive regardless of cookie behavior, privacy extensions, or browser restarts
  • Extracted _validate_oauth_callback() helper to keep ruff complexity under limits
  • Updated OAuth callback tests: signed state validation, tampered state rejection, missing code handling

Test plan

  • pytest tests/ -v — 40 passed
  • ruff check — clean
  • ruff format --check — clean
  • Deploy to lotor and test OAuth flow end-to-end
  • Test in incognito window to verify no cookie dependency

🤖 Generated with Claude Code

The "Missing code verifier" error kept recurring because storing the
PKCE code_verifier in cookies (session) is inherently fragile — cookies
can be dropped by browser restarts, privacy extensions, or SameSite
enforcement. Instead, bundle the code_verifier into the OAuth state
parameter using itsdangerous.URLSafeTimedSerializer. Google echoes
state back in the redirect URL, guaranteeing delivery regardless of
cookie behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@fatherlinux fatherlinux merged commit 261ee15 into main May 3, 2026
2 of 3 checks passed
@fatherlinux fatherlinux deleted the fix/oauth-pkce-signed-state branch May 3, 2026 14:30
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces session-based OAuth state management with a signed-state approach using itsdangerous.URLSafeTimedSerializer. This change embeds the PKCE code_verifier and CSRF nonce directly into the OAuth state parameter to improve reliability for users with restricted cookie settings. However, the review identifies a critical security regression: by removing the session-bound check, the implementation is now vulnerable to login CSRF and account linking attacks. Additionally, the code exposes sensitive stack traces to users and contains duplicated logic for state signing that should be refactored into a helper function.

Comment thread app.py
Comment on lines +382 to +384
state_data, code, error = _validate_oauth_callback()
if error:
return error
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The new signed-state approach removes the session-based CSRF protection for the OAuth callback. While the state is signed, it is no longer bound to the specific user session that initiated the request. An attacker could initiate a flow, obtain a valid signed_state, and trick a victim into completing the flow, potentially leading to account linking or login CSRF. To maintain security while still embedding the code_verifier in the state, you should include a unique nonce in the payload and verify it against a value stored in the user's session (e.g., session['oauth_state']) during the callback. If the goal is to support users with blocked cookies, be aware that this change significantly weakens the security of the authentication flow.

Comment thread app.py
except Exception as e:
import traceback

return f"<pre>Error: {e}\n\n{traceback.format_exc()}</pre>", HTTPStatus.INTERNAL_SERVER_ERROR
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Exposing full stack traces to the user is a security risk (Information Exposure). It can reveal internal file paths, library versions, and code logic that an attacker could exploit. Consider logging the traceback on the server and returning a generic error message to the client.

Comment thread app.py
Comment on lines +421 to +425
signed_state = URLSafeTimedSerializer(app.config["SECRET_KEY"]).dumps(reauth_payload)
parsed = urlparse(authorization_url)
params = parse_qs(parsed.query)
params["state"] = [signed_state]
authorization_url = urlunparse(parsed._replace(query=urlencode(params, doseq=True)))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for signing the state and updating the authorization URL is duplicated here from the auth_google function. Additionally, the URLSafeTimedSerializer is instantiated multiple times across the file. Consider refactoring this into a helper function to improve maintainability and reduce the risk of inconsistent implementations.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant