Skip to content

Conversation

@seapagan
Copy link
Owner

@seapagan seapagan commented Jan 4, 2026

⚠️ Breaking change

  • Credentialed CORS requests are now disabled by default. Browser clients using cookies must switch to Bearer tokens or explicitly re-enable credentials with restricted origins.

What

  • Enforce typ="access" on access tokens and reject refresh/typ-less tokens in auth dependency.
  • Add unit/integration coverage for token type enforcement and protected routes.
  • Disable credentialed CORS for API usage, normalize origins, and warn on CORS_ORIGINS="*".
  • Document CORS guidance and mark critical items 1 and 2 as done in SECURITY-REVIEW.md.

Why

  • Closes critical security issues 1 and 2 from SECURITY-REVIEW.md:
    • Refresh token auth bypass (access token type enforcement).
    • Wildcard CORS with credentials (now disallowed; warning on *).

Testing

  • poe test
  • poe mypy
  • poe ruff

@coderabbitai
Copy link

coderabbitai bot commented Jan 4, 2026

📝 Walkthrough

Walkthrough

Adds explicit JWT token typing ("typ") for access vs refresh tokens and enforces it at runtime; disables CORS credentials by default and emits a startup warning when CORS_ORIGINS="*"; updates docs and adds tests for token typing, CORS behavior, and lifespan logging.

Changes

Cohort / File(s) Summary
JWT Token Type Validation
app/managers/auth.py
Add "typ":"access" to access token payload; require typ == "access" in get_jwt_user, rejecting other or missing typ with 401 and logging a warning.
CORS Security Hardening
app/main.py
Build cors_list from settings, set CORSMiddleware.allow_credentials=False, and emit a startup warning when cors_list contains "*".
Tests — JWT & Protected Routes
tests/unit/test_auth_manager.py, tests/unit/test_jwt_auth.py, tests/integration/test_protected_user_routes.py
Add assertions and tests ensuring access tokens include typ="access"; verify refresh tokens and tokens missing typ are rejected (401) on protected routes.
Tests — CORS & Lifespan
tests/unit/test_cors_config.py, tests/unit/test_lifespan.py
New tests assert allow_credentials=False, allow_origins contains "*", and startup logs a warning when wildcard origins are configured.
Docs & Examples
.env.example, SECURITY-REVIEW.md, docs/deployment/deployment.md, docs/usage/configuration/environment.md, docs/important.md
Update guidance: CORS_ORIGINS="*" acceptable for public APIs using Bearer tokens (will log a warning); recommend explicit origins for browser clients; document the CORS and JWT-type security changes and breaking-note about disabled CORS credentials.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant API as FastAPI
  participant AuthDep as get_jwt_user
  participant JWT as AuthManager
  participant DB as UserStore

  Client->>API: Request with Authorization: Bearer <token>
  API->>AuthDep: invoke auth dependency
  AuthDep->>JWT: decode & verify JWT
  JWT-->>AuthDep: return payload (may include typ)
  alt payload.typ == "access"
    AuthDep->>DB: fetch user by sub/claims
    DB-->>AuthDep: user record
    AuthDep-->>API: return authenticated user
    API-->>Client: 200 / route response
  else payload.typ missing or != "access"
    AuthDep->>AuthDep: log warning "INVALID_TOKEN"
    AuthDep-->>API: raise 401 INVALID_TOKEN
    API-->>Client: 401 Unauthorized
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰
I hopped through tokens, stamped each one right,
"access" wears a badge; others lose the light.
CORS folded its paws, credentials tucked away,
A startup whisper guards the wild display.
Nibble, patch, and hop — secure at night. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: enforcing token type separation (access vs refresh tokens) and hardening CORS defaults by disabling credentials and adding warnings.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link

@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: 0

🧹 Nitpick comments (1)
app/main.py (1)

65-72: Refactor the dependency on module-level cors_list.

The lifespan function references cors_list (line 65), but cors_list is defined later at line 173. While this works because lifespan executes after module initialization, it creates a non-obvious dependency on evaluation order and reduces code clarity.

🔎 Suggested refactoring approaches

Option 1: Define cors_list before lifespan

Move the cors_list definition to before the lifespan function:

+# set up CORS
+cors_list = [
+    origin.strip()
+    for origin in get_settings().cors_origins.split(",")
+    if origin.strip()
+]
+
 @asynccontextmanager
 async def lifespan(app: FastAPI) -> AsyncGenerator[Any, None]:

Option 2: Build cors_list inside lifespan

 @asynccontextmanager
 async def lifespan(app: FastAPI) -> AsyncGenerator[Any, None]:
     """Lifespan function Replaces the previous startup/shutdown functions."""
     # Initialize loguru logging within the server process.
     get_log_config()
 
+    cors_list = [
+        origin.strip()
+        for origin in get_settings().cors_origins.split(",")
+        if origin.strip()
+    ]
+
     if "*" in cors_list:

Option 1 is preferred as it keeps cors_list at module level for the middleware configuration while making the dependency explicit.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc1cd67 and 1ac918d.

📒 Files selected for processing (11)
  • .env.example
  • SECURITY-REVIEW.md
  • app/main.py
  • app/managers/auth.py
  • docs/deployment/deployment.md
  • docs/usage/configuration/environment.md
  • tests/integration/test_protected_user_routes.py
  • tests/unit/test_auth_manager.py
  • tests/unit/test_cors_config.py
  • tests/unit/test_jwt_auth.py
  • tests/unit/test_lifespan.py
🧰 Additional context used
🧬 Code graph analysis (4)
app/main.py (2)
app/logs.py (1)
  • warning (36-39)
app/config/settings.py (1)
  • get_settings (247-249)
tests/unit/test_lifespan.py (1)
app/main.py (1)
  • lifespan (55-138)
tests/integration/test_protected_user_routes.py (3)
app/config/settings.py (1)
  • get_settings (247-249)
app/managers/auth.py (1)
  • encode_refresh_token (80-106)
app/models/user.py (1)
  • User (10-36)
tests/unit/test_jwt_auth.py (2)
app/config/settings.py (1)
  • get_settings (247-249)
app/managers/auth.py (3)
  • refresh (170-236)
  • get_jwt_user (479-555)
  • ResponseMessages (29-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.14)
  • GitHub Check: test (3.11)
🔇 Additional comments (17)
app/managers/auth.py (2)

55-55: LGTM! Critical security improvement.

Adding typ="access" to access tokens enables type-based validation and prevents refresh tokens from being used as access tokens. This addresses critical security issue #1 from SECURITY-REVIEW.md.


496-505: LGTM! Proper token type enforcement.

The validation correctly rejects tokens with typ != "access", preventing authentication bypass via refresh tokens or other token types. The error handling is appropriate: logs a warning, increments auth failure metrics, and returns 401 with INVALID_TOKEN.

.env.example (1)

48-49: LGTM! Clear CORS guidance.

The updated comment accurately describes the new behavior: * is acceptable for public APIs using Bearer tokens (with a warning), while explicit origins should be used for browser clients. This aligns with the code changes in app/main.py.

docs/deployment/deployment.md (1)

19-20: LGTM! Clear deployment guidance.

The updated CORS documentation properly distinguishes between browser clients (requiring explicit origins) and public APIs using Bearer tokens (where * is acceptable but logged). This aligns with the security hardening objectives.

docs/usage/configuration/environment.md (1)

181-185: LGTM! Comprehensive CORS documentation.

The documentation clearly explains the two use cases: public APIs with Bearer tokens (where * is acceptable) vs. browser clients (requiring explicit origins). The note about the warning being logged helps developers understand the intended behavior.

app/main.py (2)

173-177: LGTM! Improved CORS origin parsing.

The list comprehension properly strips whitespace and filters empty entries, making the origin parsing more robust against malformed configuration.


182-182: LGTM! Critical security hardening.

Setting allow_credentials=False is correct for Bearer token authentication and prevents the security issue of wildcard CORS with credentials. This addresses critical security issue #2 from SECURITY-REVIEW.md.

Note: This is a breaking change for clients that relied on credentialed requests, but it's intentional and necessary for security.

tests/unit/test_cors_config.py (1)

11-24: LGTM! Test correctly validates CORS security configuration.

The test properly verifies that:

  • CORS middleware is present in the application
  • Credentials are disabled (allow_credentials=False)
  • Wildcard origin is configured

The use of next() without a default is appropriate here—if the middleware isn't found, the test should fail with StopIteration.

tests/unit/test_auth_manager.py (1)

45-45: LGTM! Assertions correctly validate token type claims.

The new assertions verify that:

  • Access tokens include typ="access" (line 45)
  • Refresh tokens include typ="refresh" (line 72)

This test coverage aligns with the security fix for token type enforcement described in the PR objectives.

Also applies to: 72-72

tests/unit/test_lifespan.py (1)

215-239: LGTM! Test correctly validates CORS wildcard warning.

The test properly verifies that:

  • A warning is logged when CORS_ORIGINS="*" is configured
  • Both the uvicorn logger (via caplog) and loguru logger emit the warning
  • The warning message contains the expected text

The test setup appropriately disables caching to isolate the CORS warning behavior.

SECURITY-REVIEW.md (1)

7-9: LGTM! Documentation accurately reflects the security fixes.

The notes correctly document:

  • Issue #1: Token type enforcement now implemented with typ="access" validation
  • Issue #2: CORS credentials disabled and startup warning for wildcard origins

These updates align with the PR objectives and the code changes across the other files.

Also applies to: 26-28

tests/unit/test_jwt_auth.py (3)

5-5: LGTM! New imports appropriately support JWT test scenarios.

The imports enable:

  • jwt: Direct token encoding for testing edge cases
  • get_settings: Access to the secret key for token creation

These are necessary for the new test cases that validate token type enforcement.

Also applies to: 10-10


64-81: LGTM! Test correctly validates refresh token rejection.

The test ensures that:

  • A valid refresh token is obtained from registration
  • When used as an access token (in the Authorization header), it's rejected
  • The response is HTTP 401 with INVALID_TOKEN message

This directly validates the token type separation security fix described in the PR objectives.


83-107: LGTM! Test correctly validates missing typ claim rejection.

The test ensures that:

  • A JWT without the typ claim is manually crafted
  • When presented as an access token, it's rejected by get_jwt_user
  • The response is HTTP 401 with INVALID_TOKEN message

This validates that tokens lacking the typ claim (legacy tokens or tokens from external sources) are properly rejected, completing the token type enforcement security fix.

tests/integration/test_protected_user_routes.py (3)

3-12: LGTM! Clean imports for token-type testing.

All new imports support the token-type enforcement tests and are used appropriately.


74-95: LGTM! Excellent coverage for refresh token rejection.

This test properly verifies that refresh tokens (with typ="refresh") are rejected on protected routes, addressing the critical security issue mentioned in the PR objectives.


97-126: LGTM! Thorough validation of typ claim requirement.

This test effectively verifies that tokens without the typ claim are rejected. The manual token construction using jwt.encode is the appropriate approach to create a valid JWT that intentionally omits the required typ field.

@codacy-production
Copy link

codacy-production bot commented Jan 4, 2026

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
+0.00% (target: -1.00%) 100.00%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (cc1cd67) 2209 2209 100.00%
Head commit (493b0bf) 2217 (+8) 2217 (+8) 100.00% (+0.00%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#804) 9 9 100.00%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

See your quality gate settings    Change summary preferences

@seapagan seapagan self-assigned this Jan 4, 2026
@seapagan seapagan merged commit 24eac05 into main Jan 4, 2026
18 checks passed
@seapagan seapagan deleted the security/critical-token-and-cors branch January 4, 2026 09:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants