Skip to content

Extract auth helpers from app.main#353

Open
TUPM96 wants to merge 4 commits into
ramimbo:mainfrom
TUPM96:codex/bounty-320-auth-boundary
Open

Extract auth helpers from app.main#353
TUPM96 wants to merge 4 commits into
ramimbo:mainfrom
TUPM96:codex/bounty-320-auth-boundary

Conversation

@TUPM96
Copy link
Copy Markdown
Contributor

@TUPM96 TUPM96 commented May 26, 2026

Refs #320

Summary

  • Move OAuth route registration, signed cookie verification, CSRF token helpers, session cookie names, and auth request helpers into a focused app.auth module.
  • Keep app.main compatibility aliases for existing tests/callers while replacing inline auth logic with module calls.
  • Harden OAuth next-path validation against percent-encoded network-path, backslash, and header-injection ambiguity.
  • Add focused auth tests for signed value round-tripping, encoded next-path rejection, and the login route state fallback.

Complexity reduced

  • app.main no longer owns GitHub OAuth exchange, cookie signing, session cookie lifetime constants, or auth route wiring; those are isolated in app.auth for smaller review surfaces around admin/API/page routes.

Tests

  • python -m pytest -q
  • python -m ruff check .

Summary by CodeRabbit

  • New Features

    • GitHub OAuth login with cookie-based user and admin sessions
    • Logout support
    • CSRF protection and signed session/state tokens
    • Sanitized post-login redirect handling to prevent unsafe redirects
  • Tests

    • Added tests for auth helpers, signed-token validation, and redirect safety
  • Refactor

    • Authentication logic moved to a dedicated auth module for cleaner organization

Review Change Stack

Copilot AI review requested due to automatic review settings May 26, 2026 01:32
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

@TUPM96, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 2 minutes and 45 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 26dd8a45-06c4-45f9-a1ed-fe68ff92f02f

📥 Commits

Reviewing files that changed from the base of the PR and between 3404d0c and 4427ccd.

📒 Files selected for processing (2)
  • app/auth.py
  • tests/test_auth.py
📝 Walkthrough

Walkthrough

Extracts auth logic into app/auth.py: HMAC-signed values, CSRF tokens, safe redirect sanitizer, GitHub OAuth flow, session cookie handling, and route registration. app/main.py delegates auth helpers and route registration; tests exercise signing and redirect safety; minor ledger import cleanup.

Changes

Auth Module Extraction and Refactoring

Layer / File(s) Summary
Auth module security foundations
app/auth.py
Constants define cookie names and session lifetimes. safe_next_path() sanitizes redirect targets to block unsafe characters and suspicious prefixes. signed_value() and verified_value() implement HMAC-SHA256 signing with timestamp and age validation. csrf_token() and verify_csrf_token() build CSRF protection on top of signed values.
Auth module identity and OAuth flow
app/auth.py
admin_login_from_request() and github_login_from_request() extract identities from signed cookies with membership validation. require_admin_token_from_request() enforces access control via header token. _github_oauth_login_from_callback() validates OAuth state (age and signature), exchanges authorization code for GitHub access token, fetches user login, sanitizes the post-login redirect target, and raises HTTP errors on invalid state or failed lookups. _set_github_session_cookies() writes HttpOnly/secure/lax cookies for user and admin sessions. register_auth_routes() registers handlers for /auth/github/login, /auth/github/callback, /admin/login, /admin/callback, and /auth/logout.
Main.py refactoring and auth delegation
app/main.py
Import cleanup removes local crypto and URL encoding utilities. app.auth is imported and key helpers are re-exported via module-level aliases. Local implementations of _oauth_configured(), _safe_next_path(), _signed_value(), _verified_value(), _csrf_token(), and _verify_csrf_token() are removed. create_app() calls auth_helpers.register_auth_routes() to register endpoints. Request helper functions become thin wrappers delegating to auth_helpers. Inline route handlers for all auth endpoints are removed from this file.
Test coverage for auth module
tests/test_auth.py
Unit tests verify signed_value() and verified_value() round-trip with correct secrets and reject wrong secrets. Parameterized tests validate safe_next_path() normalizes encoded and ambiguous redirect inputs. Integration test requests /auth/github/login with an encoded malicious next parameter and verifies the signed state cookie contains a safe normalized next_path.
Ledger import & execute result cleanup
app/ledger/service.py
Removes unused typing imports (cast, CursorResult) and assigns session.execute(...) results directly while preserving rowcount checks in pay_bounty and close_bounty.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the main changes, complexity reductions, and testing, but is missing required template sections like Evidence and specific test evidence checkmarks. Complete the PR description template by adding detailed evidence of the issue being fixed and checking off all test evidence items to confirm validation steps were performed.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: extracting authentication helpers from app.main into a dedicated module.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR extracts GitHub OAuth + session/cookie helpers into a dedicated app/auth.py module and adds tests to harden redirect handling (next parameter) against encoded open-redirect and header-injection patterns.

Changes:

  • Added app/auth.py with OAuth routes registration, cookie/session helpers, CSRF helpers, and a stricter safe_next_path.
  • Refactored app/main.py to delegate auth routes and auth request parsing to app.auth.
  • Added unit/integration-style tests for signing/verification and safe_next_path behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
tests/test_auth.py Adds tests validating cookie signing and hardened redirect sanitization.
app/main.py Removes inline auth helpers/routes and wires the app to app.auth helpers instead.
app/auth.py Introduces centralized auth logic including redirect sanitization and OAuth route registration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/auth.py
Comment on lines +130 to +155
async with httpx.AsyncClient(timeout=10) as client:
token_response = await client.post(
"https://github.com/login/oauth/access_token",
headers={"Accept": "application/json"},
data={
"client_id": settings.github_oauth_client_id,
"client_secret": settings.github_oauth_client_secret,
"code": code,
"redirect_uri": f"{settings.public_base_url}/auth/github/callback",
},
)
token_response.raise_for_status()
access_token = token_response.json().get("access_token")
if not access_token:
raise HTTPException(status_code=401, detail="GitHub OAuth token exchange failed")
user_response = await client.get(
"https://api.github.com/user",
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {access_token}",
},
)
user_response.raise_for_status()
login = str(user_response.json().get("login", "")).lower()
if not login:
raise HTTPException(status_code=401, detail="GitHub OAuth user lookup failed")
Comment thread app/auth.py
return RedirectResponse("/auth/github/login?next=/admin", status_code=302)

@app.get("/admin/callback")
async def admin_callback(request: Request) -> RedirectResponse:
Comment thread tests/test_auth.py
("/me", "/me"),
("/bounties?status=open", "/bounties?status=open"),
("/%2f%2fevil.example/me", "/me"),
("/%5cevil.example/me", "/me"),
Comment thread app/auth.py
Comment on lines +32 to +46
def safe_next_path(next_path: str | None) -> str:
decoded_next_path = unquote(next_path) if next_path else ""
if (
not next_path
or not next_path.startswith("/")
or next_path.startswith("//")
or len(next_path) > 2048
or "\\" in next_path
or decoded_next_path.startswith("//")
or "\\" in decoded_next_path
or any(ord(char) < 32 or 127 <= ord(char) < 160 for char in next_path)
or any(ord(char) < 32 or 127 <= ord(char) < 160 for char in decoded_next_path)
):
return "/me"
return next_path
Copy link
Copy Markdown
Contributor

@TateLyman TateLyman left a comment

Choose a reason for hiding this comment

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

Blocking issue: current head fails the required type check because the branch removes the CursorResult[Any] casts/import from app/ledger/service.py while pay_bounty() and close_bounty() still read claimed.rowcount after session.execute(update(...)). SQLAlchemy typing infers Result[Any], so mypy rejects the attribute.\n\nEvidence:\n- Hosted Quality, readiness, docs, and image checks is failing in type check.\n- Local repro: uv run --extra dev python -m mypy app/auth.py app/ledger/service.py app/main.py -> app/ledger/service.py:670: error: "Result[Any]" has no attribute "rowcount" and app/ledger/service.py:741: error: "Result[Any]" has no attribute "rowcount".\n- Focused auth/security/wallet validation otherwise passes: uv run --extra dev python -m pytest tests/test_auth.py tests/test_security.py tests/test_wallet_api.py -q -> 90 passed.\n\nThe auth extraction may still be reviewable after that is fixed, but the ledger typing cleanup is unrelated to the auth boundary and should either be reverted or typed explicitly as a cursor result before merge.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/auth.py`:
- Around line 130-156: Wrap the GitHub token exchange and user lookup HTTP calls
in try/except to catch httpx.RequestError and httpx.HTTPStatusError so
network/timeouts and non-2xx responses are handled and translated into
appropriate HTTPException responses; specifically, surround the
httpx.AsyncClient POST to "https://github.com/login/oauth/access_token" (where
token_response.raise_for_status() and access_token are used) and the GET to
"https://api.github.com/user" (where user_response.raise_for_status() and login
are used) with exception handling that returns a clear HTTPException (e.g., 401
for auth failures or 502/503 for upstream/network errors) and include brief
error context in the detail message.

In `@tests/test_auth.py`:
- Around line 12-16: The current test covers secret mismatch but not age-based
expiry; add a new test (e.g., test_verified_value_rejects_expired_tokens) that
uses signed_value("github:alice", "test-cookie-secret"), asserts
verified_value(token, "test-cookie-secret", 60) returns "github:alice", then
asserts verified_value(token, "test-cookie-secret", max_age=0) is None to
validate timestamp/age rejection; alternatively, if you prefer real-time expiry,
use time.sleep to exceed a very short max_age, but prefer the max_age=0 approach
for a deterministic unit test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e10c6ebe-9c0a-493f-8dc0-4a062f3d9525

📥 Commits

Reviewing files that changed from the base of the PR and between ac99d79 and 50a8ecb.

📒 Files selected for processing (3)
  • app/auth.py
  • app/main.py
  • tests/test_auth.py

Comment thread app/auth.py
Comment thread tests/test_auth.py
@TUPM96
Copy link
Copy Markdown
Contributor Author

TUPM96 commented May 26, 2026

Follow-up pushed in 4427ccd to address the auth review items on the current branch.

What changed:

  • Wrapped the GitHub OAuth token exchange and user lookup HTTP calls so GitHub non-2xx responses become clear 401 auth failures and network/request errors become 502 upstream failures instead of leaking raw httpx exceptions.
  • Added a deterministic signed-cookie expiry regression by monkeypatching time around verified_value().

Validation:

  • uv run --extra dev python -m pytest tests/test_auth.py tests/test_security.py tests/test_wallet_api.py -q -> 91 passed.
  • uv run --extra dev python -m pytest -q -> 343 passed.
  • uv run --extra dev ruff check app/auth.py app/main.py tests/test_auth.py tests/test_security.py tests/test_wallet_api.py -> passed.
  • uv run --extra dev ruff format --check app/auth.py app/main.py tests/test_auth.py tests/test_security.py tests/test_wallet_api.py -> passed.
  • uv run --extra dev python -m mypy app -> passed.
  • uv run --extra dev python scripts/docs_smoke.py -> docs smoke ok.
  • git diff --check origin/main...HEAD -> clean.

Copy link
Copy Markdown
Contributor

@TateLyman TateLyman left a comment

Choose a reason for hiding this comment

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

Rechecked current head 4427ccd after the auth follow-up fixes. The earlier app/ledger/service.py rowcount type-check blocker is gone, and the branch now also wraps GitHub OAuth upstream failures with explicit HTTPException responses plus expiry coverage.

Validation:

  • uv run --extra dev python -m pytest tests/test_auth.py tests/test_security.py tests/test_wallet_api.py -q -> 91 passed.
  • uv run --extra dev python -m mypy app/auth.py app/ledger/service.py app/main.py -> success.
  • uv run --extra dev ruff check app/auth.py app/ledger/service.py app/main.py tests/test_auth.py -> passed.
  • uv run --extra dev ruff format --check app/auth.py app/ledger/service.py app/main.py tests/test_auth.py -> passed.
  • git diff --check origin/main...HEAD -> clean.
  • Hosted quality check is green and GitHub reports the branch mergeable.

No remaining blocker found from my earlier review.

@ramimbo ramimbo added the mrwk:needs-info More information needed label May 26, 2026
@ramimbo
Copy link
Copy Markdown
Owner

ramimbo commented May 26, 2026

Maintainer note after #320 filled: this PR still references closed bounty #320. If you want it considered for the current code-health round, rebase against main, update the bounty reference to #377, and keep the scope distinct from already accepted #328, #337, #364, #375, and #376. This is not accepted or paid as-is.

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

Labels

mrwk:needs-info More information needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants