Skip to content

Extract auth session helpers from app main#338

Closed
LubuSeb wants to merge 1 commit into
ramimbo:mainfrom
LubuSeb:codex/mergework-auth-boundary-320
Closed

Extract auth session helpers from app main#338
LubuSeb wants to merge 1 commit into
ramimbo:mainfrom
LubuSeb:codex/mergework-auth-boundary-320

Conversation

@LubuSeb
Copy link
Copy Markdown
Contributor

@LubuSeb LubuSeb commented May 26, 2026

Refs #320

Summary

  • Extract signed-cookie, OAuth state, GitHub/admin login, CSRF, and session-cookie helpers from app/main.py into focused app/auth.py.
  • Keep route wiring, OAuth HTTP exchange, page rendering, account validation, bounty attempts, payouts, wallets, webhooks, MCP, and ledger behavior in their existing boundaries.
  • Add direct auth regression tests for safe redirect targets, signed user/admin cookies, admin-token separation, OAuth state verification, and CSRF token verification.

Why this is distinct

Validation

  • .\.venv\Scripts\python.exe -m pytest -q -> 323 passed.
  • .\.venv\Scripts\python.exe -m ruff check app tests -> passed.
  • .\.venv\Scripts\python.exe -m ruff format --check app tests -> passed.
  • .\.venv\Scripts\python.exe -m mypy app -> success.
  • .\.venv\Scripts\python.exe scripts\docs_smoke.py -> docs smoke ok.
  • git diff --check origin/main...HEAD -> clean.

Review gate

No secrets, wallet private keys, payout credentials, deployment values, private vulnerability details, or MRWK price claims are included.

Summary by CodeRabbit

Release Notes

  • New Features

    • CSRF token protection now required for admin bounty operations.
    • Enhanced OAuth state verification with improved redirect validation.
  • Improvements

    • Admin authorization now supports both session cookies and bearer tokens for greater flexibility.
    • Strengthened session cookie security and management.
  • Tests

    • Added comprehensive test coverage for authentication and authorization workflows.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

A new app/auth.py module consolidates authentication logic previously scattered in app/main.py. The module provides signed-token verification, path sanitization, CSRF primitives, and an AuthContext class that manages cookie/header extraction, login enforcement, OAuth state validation, and session cookies. app/main.py was refactored to instantiate and delegate all auth concerns to AuthContext, and test files were updated to import the now-public helpers instead of private variants.

Changes

Authentication module extraction and integration

Layer / File(s) Summary
Auth helper primitives
app/auth.py
Constants, oauth_configured(), safe_next_path(), HMAC-based signed_value() / verified_value() with timestamp verification, and csrf_token() / verify_csrf_token() primitives.
AuthContext class and session management
app/auth.py
AuthContext dataclass wraps Settings and exposes cookie/header extraction (github_login_from_request, admin_login_from_request), requirement enforcement (require_github_login, require_admin, require_admin_token), OAuth state verification, and cookie writers (set_oauth_state_cookie, set_login_cookies, clear_session_cookies).
Auth module test suite
tests/test_auth.py
Tests for path sanitization, signed cookie parsing, admin token vs cookie separation (401 on invalid usage), OAuth state validation (defaults external URLs to "/me", rejects mismatches), and CSRF boundary verification.
Main.py refactoring and endpoint integration
app/main.py
Removed local auth helpers, instantiated AuthContext(settings) in create_app, rewired API dependencies to use auth.require_* methods, reworked OAuth login/callback with auth.verify_oauth_state(), and updated session endpoints (/auth/logout, /admin, /admin/logout) and CSRF token generation to use AuthContext methods.
Test file updates for auth helpers
tests/test_bounty_attempts.py, tests/test_security.py, tests/test_wallet_api.py
Updated imports to use public safe_next_path, signed_value, verified_value from app.auth instead of private _* variants from app.main, and adjusted test fixtures to sign cookies and validate redirects via the new public API.

Sequence Diagram

sequenceDiagram
  participant Client
  participant AuthGithubLogin as /auth/github/login
  participant AuthGithubCallback as /auth/github/callback
  participant AuthContext
  participant Response
  Client->>AuthGithubLogin: POST with next path
  AuthGithubLogin->>AuthContext: oauth_configured()
  AuthGithubLogin->>AuthContext: signed_value(random state)
  AuthGithubLogin->>AuthContext: set_oauth_state_cookie(state)
  AuthGithubLogin->>Response: Redirect to GitHub OAuth
  Client->>AuthGithubCallback: GET code + state from GitHub
  AuthGithubCallback->>AuthContext: verify_oauth_state(cookie, state)
  AuthContext->>AuthContext: Verify signature and expiry
  AuthContext->>Response: Return sanitized next_path
  AuthGithubCallback->>AuthContext: set_login_cookies(login)
  AuthGithubCallback->>Response: Redirect to next_path
Loading

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: extracting auth/session helpers from app/main.py to a new focused app/auth.py module, which is the core objective.
Description check ✅ Passed The description covers the key changes, includes a reference to issue #320, provides comprehensive validation evidence, and explains the PR's context and distinct scope.
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

@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 167-184: In set_login_cookies, normalize the provided login to
lowercase before checking against settings.admin_logins so case differences
don't prevent setting the ADMIN_COOKIE_NAME; use the lowercased value for the
comparison (but continue to sign/store the original or decide consistently) and
ensure the check against settings.admin_logins uses the same normalization as
wherever admin logins are stored.

In `@tests/test_auth.py`:
- Around line 82-87: Add a negative assertion to
test_csrf_uses_same_signed_cookie_boundary to ensure action mismatch is
rejected: after creating auth = AuthContext(_settings()) and token =
auth.csrf_token("admin-bounty", "alice"), call auth.verify_csrf_token(token,
action="other-action", login="alice") and assert that it returns False; this
mirrors the existing login-mismatch check and verifies the action boundary of
csrf_token/verify_csrf_token.
🪄 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: d4d299b2-7b67-4ad4-b7bf-308b6314c80a

📥 Commits

Reviewing files that changed from the base of the PR and between f4fa763 and 079c38b.

📒 Files selected for processing (6)
  • app/auth.py
  • app/main.py
  • tests/test_auth.py
  • tests/test_bounty_attempts.py
  • tests/test_security.py
  • tests/test_wallet_api.py

Comment thread app/auth.py
Comment on lines +167 to +184
def set_login_cookies(self, response: Response, login: str) -> None:
response.set_cookie(
USER_COOKIE_NAME,
self.signed_value(login),
httponly=True,
secure=True,
samesite="lax",
max_age=USER_COOKIE_MAX_AGE_SECONDS,
)
if login in self.settings.admin_logins:
response.set_cookie(
ADMIN_COOKIE_NAME,
self.signed_value(login),
httponly=True,
secure=True,
samesite="lax",
max_age=ADMIN_COOKIE_MAX_AGE_SECONDS,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Case mismatch may prevent admin cookie from being set.

Line 176 compares login directly against admin_logins, but GitHub usernames may arrive in original case (e.g., "Alice") while admin_logins is expected to contain lowercase values. If the OAuth callback passes the login without lowercasing, admin users won't receive the admin cookie.

Normalize login to lowercase before the comparison:

🐛 Proposed fix
     def set_login_cookies(self, response: Response, login: str) -> None:
         response.set_cookie(
             USER_COOKIE_NAME,
             self.signed_value(login),
             httponly=True,
             secure=True,
             samesite="lax",
             max_age=USER_COOKIE_MAX_AGE_SECONDS,
         )
-        if login in self.settings.admin_logins:
+        if login.lower() in self.settings.admin_logins:
             response.set_cookie(
                 ADMIN_COOKIE_NAME,
                 self.signed_value(login),
                 httponly=True,
                 secure=True,
                 samesite="lax",
                 max_age=ADMIN_COOKIE_MAX_AGE_SECONDS,
             )
🤖 Prompt for 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.

In `@app/auth.py` around lines 167 - 184, In set_login_cookies, normalize the
provided login to lowercase before checking against settings.admin_logins so
case differences don't prevent setting the ADMIN_COOKIE_NAME; use the lowercased
value for the comparison (but continue to sign/store the original or decide
consistently) and ensure the check against settings.admin_logins uses the same
normalization as wherever admin logins are stored.

Comment thread tests/test_auth.py
Comment on lines +82 to +87
def test_csrf_uses_same_signed_cookie_boundary() -> None:
auth = AuthContext(_settings())
token = auth.csrf_token("admin-bounty", "alice")

assert auth.verify_csrf_token(token, action="admin-bounty", login="alice")
assert not auth.verify_csrf_token(token, action="admin-bounty", login="bob")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider adding negative test case for action mismatch.

The test verifies login mismatch rejection but doesn't test action mismatch. Adding a test like assert not auth.verify_csrf_token(token, action="other-action", login="alice") would strengthen CSRF boundary validation coverage.

💡 Suggested addition
     assert auth.verify_csrf_token(token, action="admin-bounty", login="alice")
     assert not auth.verify_csrf_token(token, action="admin-bounty", login="bob")
+    assert not auth.verify_csrf_token(token, action="other-action", login="alice")
🤖 Prompt for 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.

In `@tests/test_auth.py` around lines 82 - 87, Add a negative assertion to
test_csrf_uses_same_signed_cookie_boundary to ensure action mismatch is
rejected: after creating auth = AuthContext(_settings()) and token =
auth.csrf_token("admin-bounty", "alice"), call auth.verify_csrf_token(token,
action="other-action", login="alice") and assert that it returns False; this
mirrors the existing login-mismatch check and verifies the action boundary of
csrf_token/verify_csrf_token.

@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 update: holding this with mrwk:needs-info. Please address the auth review items before merge: normalize the admin-login comparison consistently in set_login_cookies, and add a CSRF action-mismatch regression. No PR bounty payout is recorded while it is held.

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.

Reviewed the auth/session extraction for #320.

What I checked:

  • app/auth.py: signed cookie helpers, OAuth state verification, safe next-path handling, GitHub/admin session lookup, admin-token-only dependency, login cookie setting, and session clearing.
  • app/main.py: route dependencies now call AuthContext methods without changing the existing boundaries for bounty attempts, wallet linking/claims, admin webhook/payout APIs, OAuth callbacks, and admin form CSRF.
  • tests/test_auth.py, tests/test_security.py, tests/test_wallet_api.py, and tests/test_bounty_attempts.py: regression coverage moved from private app.main helpers to public auth helpers and keeps cookie auth separate from admin-token API auth.

Validation run locally:

  • uv run --extra dev python -m pytest tests/test_auth.py tests/test_security.py::test_admin_bounty_form_requires_csrf_for_cookie_auth tests/test_security.py::test_admin_bounty_api_requires_admin_token_not_cookie_auth tests/test_wallet_api.py::test_github_login_stores_safe_default_for_backslash_next -q -> 8 passed.
  • uv run --extra dev python -m pytest tests/test_security.py tests/test_wallet_api.py tests/test_bounty_attempts.py -q -> 86 passed.
  • uv run --extra dev ruff check app tests -> passed.
  • uv run --extra dev ruff format --check app tests -> passed.
  • uv run --extra dev mypy app -> passed.
  • uv run --extra dev python scripts/docs_smoke.py -> docs smoke ok.
  • git diff --check origin/main...HEAD -> clean.

I did not find a behavioral blocker. GitHub currently reports the PR merge state as CLEAN, and the hosted quality check is green.

Copy link
Copy Markdown
Contributor

@artylobos artylobos left a comment

Choose a reason for hiding this comment

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

Reviewed the auth extraction on head 079c38b. I would keep this held until two small auth-boundary items are addressed:

  • app/auth.py:176: set_login_cookies() checks if login in self.settings.admin_logins, while the read paths normalize logins (github_login_from_request() lowercases the user cookie, and admin_login_from_request() lowercases before admin membership). With admin_logins=("alice",) and an OAuth login value of "ALICE", the user cookie is set but the mr�

@artylobos
Copy link
Copy Markdown
Contributor

Reviewed on head 079c38b.

Two auth-boundary items remain:

  1. app/auth.py:167-184 - set_login_cookies() checks login directly against settings.admin_logins, while the read paths normalize logins to lowercase. With admin_logins=("alice",) and an OAuth login of "ALICE", the user cookie is set but the admin cookie is skipped. Normalize before comparison so the write path matches the read path.
  2. tests/test_auth.py:82-87 - the CSRF regression covers the wrong login, but not the wrong action. The current implementation rejects action mismatch, but the test should lock that boundary in place.

Validation:

  • uv run --extra dev python -m pytest tests/test_auth.py tests/test_security.py tests/test_wallet_api.py -q -> 88 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/auth.py app/main.py -> success
  • direct probe: set_login_cookies("ALICE") does not set mrwk_admin; verify_csrf_token(..., action="other-action") returns False.

@TateLyman
Copy link
Copy Markdown
Contributor

Follow-up on my earlier review: I agree the admin-cookie case-normalization note is valid. My validation covered the lower-case login path, but set_login_cookies() compares the raw OAuth login to settings.admin_logins while the read path lowercases. A GitHub login returned as ALICE with admin_logins=("alice",) would set the user cookie but skip the admin cookie. I would treat that as a real small blocker before merge.

The CSRF action-mismatch suggestion is useful regression coverage too, but lower severity than the admin-cookie write/read normalization mismatch.

@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.

@ramimbo
Copy link
Copy Markdown
Owner

ramimbo commented May 26, 2026

Closing this stale bounty submission. It still references a closed/filled bounty round and is dirty or unstable against current main after the route/test extraction batches. A future attempt needs a fresh PR against current main, a currently open bounty reference, and scope distinct from work already accepted.

@ramimbo ramimbo added the mrwk:rejected Submission rejected label May 26, 2026
@ramimbo ramimbo closed this May 26, 2026
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 mrwk:rejected Submission rejected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants