Skip to content

Conversation

@JSv4
Copy link
Collaborator

@JSv4 JSv4 commented Dec 29, 2025

The Auth0RemoteUserJSONWebTokenBackend.authenticate() method was catching all exceptions including JSONWebTokenExpired and returning None. This caused the GraphQL layer to return a generic "User is not authenticated" error instead of "Signature has expired", preventing the frontend from detecting token expiration and triggering automatic token refresh.

Changes:

  • Re-raise JSONWebTokenExpired in Auth0RemoteUserJSONWebTokenBackend so frontend receives "Signature has expired" message
  • Add auth_error dict to WebSocket middleware scope with code and message
  • Add WS_CLOSE_TOKEN_EXPIRED (4001) and WS_CLOSE_TOKEN_INVALID (4002) close codes for WebSocket connections
  • Add comprehensive tests for token expiration handling

Frontend's errorLink.ts already handles "Signature has expired" by clearing auth state and triggering page reload for silent Auth0 token refresh.

The Auth0RemoteUserJSONWebTokenBackend.authenticate() method was catching
all exceptions including JSONWebTokenExpired and returning None. This
caused the GraphQL layer to return a generic "User is not authenticated"
error instead of "Signature has expired", preventing the frontend from
detecting token expiration and triggering automatic token refresh.

Changes:
- Re-raise JSONWebTokenExpired in Auth0RemoteUserJSONWebTokenBackend so
  frontend receives "Signature has expired" message
- Add auth_error dict to WebSocket middleware scope with code and message
- Add WS_CLOSE_TOKEN_EXPIRED (4001) and WS_CLOSE_TOKEN_INVALID (4002)
  close codes for WebSocket connections
- Add comprehensive tests for token expiration handling

Frontend's errorLink.ts already handles "Signature has expired" by clearing
auth state and triggering page reload for silent Auth0 token refresh.
@JSv4 JSv4 linked an issue Dec 29, 2025 that may be closed by this pull request
@claude
Copy link

claude bot commented Dec 29, 2025

PR Review: Token Expiration Handling

Thank you for this well-crafted fix for issue #744. This PR properly addresses the token expiration signaling issue between backend and frontend. Here's my detailed review:


Strengths

  1. Root Cause Fix: The core issue is correctly identified and fixed in config/graphql_auth0_auth/backends.py:44-52. Re-raising JSONWebTokenExpired instead of swallowing it ensures the GraphQL layer can send the proper "Signature has expired" message to the frontend.

  2. Comprehensive WebSocket Enhancement: The addition of scope['auth_error'] with structured code and message fields provides a robust mechanism for WebSocket consumers to handle auth failures gracefully.

  3. Excellent Test Coverage: The new test file (opencontractserver/tests/test_token_expiration.py) is well-structured with:

    • Backend tests for exception re-raising behavior
    • WebSocket middleware tests
    • Close code consistency validation
    • Good use of @pytest.mark.serial for WebSocket tests
  4. Proper Constants: Using application-specific WebSocket close codes (4001, 4002) in the correct range (4000-4999) follows the WebSocket spec.

  5. Documentation: CHANGELOG.md provides excellent detail with file locations, line numbers, and clear explanation of the fix.


⚠️ Issues & Recommendations

CRITICAL: WebSocket Consumers Not Using auth_error

The middleware now sets scope['auth_error'], but none of the WebSocket consumers are checking or using this field. I reviewed config/websocket/consumers/document_conversation.py (lines 77-84) and it only checks scope['user'].is_authenticated:

if not self.scope["user"].is_authenticated:
    logger.warning(f"[Session {self.session_id}] User is not authenticated.")
    await self.close(code=4000)
    return

Problem: When a token expires, the middleware sets scope['auth_error']['code'] = 4001, but the consumer closes with generic code 4000. The frontend won't be able to distinguish between expired vs invalid tokens.

Recommendation: Update all consumers to check scope['auth_error'] and use the specific close codes:

if not self.scope["user"].is_authenticated:
    auth_error = self.scope.get("auth_error")
    if auth_error:
        logger.warning(f"[Session {self.session_id}] Auth failed: {auth_error['message']}")
        await self.close(code=auth_error['code'])
    else:
        logger.warning(f"[Session {self.session_id}] User is not authenticated.")
        await self.close(code=4000)
    return

This should be applied to:

  • config/websocket/consumers/document_conversation.py
  • config/websocket/consumers/corpus_conversation.py
  • config/websocket/consumers/standalone_document_conversation.py
  • config/websocket/consumers/unified_agent_conversation.py
  • Any other consumers

Minor: Test Coverage Gap

The WebSocket tests in opencontractserver/tests/test_token_expiration.py:144-150 only verify that the connection fails (self.assertFalse(connected)), but they don't verify:

  1. That scope['auth_error'] was actually set with the correct values
  2. That the consumer received the correct close code

Recommendation: Consider adding a test that verifies the scope contents or uses a minimal test consumer that explicitly checks and returns the auth_error.


Minor: Import Optimization

In config/websocket/middleware.py:2, Union is imported but no longer used after refactoring get_user_from_token to return User instead of Union[User, AnonymousUser].

Recommendation: Remove the unused import:

from typing import Any  # Remove Union

🔒 Security Considerations

Good: The error messages don't leak sensitive information. They provide just enough detail for the frontend to take action without exposing internals.

Good: The close codes are distinct (4001 vs 4002), allowing the frontend to implement different refresh strategies without exposing token internals.


🎯 Performance Considerations

✅ No performance concerns. The changes add minimal overhead (exception re-raising, dict assignment) in the auth path.


📋 Code Quality

Good: Follows Django and Channels best practices
Good: Comprehensive logging with appropriate levels (debug, warning, error)
Good: Type hints in WebSocket middleware signatures
Good: Clear docstrings explaining the new auth_error mechanism


Adherence to CLAUDE.md

  • ✅ No magic numbers (close codes are defined as named constants)
  • ✅ Proper test patterns (TransactionTestCase, pytest marks)
  • ✅ CHANGELOG.md updated with file locations and line numbers
  • ✅ Pre-commit hooks should pass (standard Python formatting)

📝 Summary

This is a well-implemented fix that addresses the root cause of issue #744. The main gap is that WebSocket consumers need to be updated to actually use the scope['auth_error'] information that the middleware now provides. Without this, the frontend won't receive the specific close codes needed to differentiate between expired and invalid tokens.

Recommended Actions Before Merge:

  1. ✅ Update all WebSocket consumers to check scope['auth_error'] and use specific close codes
  2. ⚠️ (Optional but recommended) Add test coverage for consumer close code behavior
  3. ⚠️ (Optional) Remove unused Union import

Once the consumers are updated, this will be a complete and robust solution. Great work on the comprehensive testing and documentation!

- Updated all WebSocket consumers to check scope['auth_error'] and use
  the specific close codes (4001 for expired, 4002 for invalid) instead
  of generic 4000
- Fixed consumers: document_conversation, corpus_conversation,
  standalone_document_conversation, unified_agent_conversation,
  thread_updates
- Removed unused Union import from middleware.py
- Fixed lazy import issue in utils.py for sync_remote_user
- Added Auth0 test settings defaults to enable test imports
- Cleaned up unused imports in test_token_expiration.py

This ensures the frontend can distinguish between expired tokens
(requiring refresh) and invalid tokens (requiring re-authentication).

Closes #744
@claude
Copy link

claude bot commented Dec 29, 2025

Code Review - PR #746: Token Expiration Handling

Summary

This PR addresses issue #744 by fixing token expiration signaling from backend to frontend. The changes enable proper distinction between expired and invalid tokens, allowing the frontend to trigger appropriate refresh/re-authentication flows.


✅ Strengths

1. Excellent Problem Analysis & Solution Design

  • The root cause analysis is spot-on: the Auth0 backend was swallowing JSONWebTokenExpired exceptions
  • The two-pronged approach (GraphQL + WebSocket) ensures comprehensive coverage
  • Well-documented in both code comments and CHANGELOG

2. Comprehensive Test Coverage (opencontractserver/tests/test_token_expiration.py)

  • Tests cover all three layers: Auth0 backend, WebSocket middleware, and close code consistency
  • Good use of mocking to isolate behavior
  • @pytest.mark.serial appropriately used for WebSocket tests
  • Tests verify both positive and negative cases (expired, invalid, valid tokens)

3. Consistent Error Handling Pattern

All 5 WebSocket consumers updated with identical pattern:

if not self.scope["user"].is_authenticated:
    auth_error = self.scope.get("auth_error")
    if auth_error:
        logger.warning(f"Auth failed: {auth_error['message']}")
        await self.close(code=auth_error["code"])
    else:
        logger.warning("User is not authenticated.")
        await self.close(code=4000)
    return

4. Proper Use of WebSocket Close Codes

  • 4001 for expired tokens (refreshable)
  • 4002 for invalid tokens (requires re-auth)
  • Correctly using 4000-4999 range reserved for application use
  • Tests verify codes are in valid range and distinct

5. Good Documentation

  • CHANGELOG entries are detailed with file paths and line numbers
  • Code comments explain the "why" behind re-raising JSONWebTokenExpired
  • Docstrings updated to reflect new auth_error behavior

6. Lazy Import Fix

The fix in config/graphql_auth0_auth/utils.py:124 prevents import errors when USE_AUTH0=False:

# Lazy import to avoid circular dependency when USE_AUTH0 is False
from opencontractserver.users.tasks import sync_remote_user

🔍 Issues & Concerns

1. CRITICAL: Potential Breaking Change in Auth Backend

File: config/graphql_auth0_auth/backends.py:44-52

Issue: Re-raising JSONWebTokenExpired changes the contract of authenticate(). Django authentication backends are expected to return None on failure, not raise exceptions.

Current Code:

except JSONWebTokenExpired:
    logger.warning(...)
    raise  # <-- This propagates up through Django auth middleware

Concerns:

  • Other parts of the system calling authenticate() may not expect exceptions
  • Could cause unhandled exception crashes in middleware chains
  • Django's AuthenticationMiddleware typically expects None for auth failure

Recommendation:

  • Check if this breaks any existing middleware or views that call authenticate()
  • Consider catching the exception at a higher level (GraphQL resolver) instead
  • Add integration tests that exercise the full auth middleware chain

2. Code Duplication Across 5 Consumers

Files: All consumer files have identical auth_error handling logic

Issue: The auth_error checking pattern is repeated 5 times verbatim. This violates DRY principle.

Recommendation:

# Create a shared helper in config/websocket/utils.py
async def check_auth_and_close_if_failed(consumer, session_id):
    """Check authentication and close with appropriate code if failed."""
    if not consumer.scope["user"].is_authenticated:
        auth_error = consumer.scope.get("auth_error")
        if auth_error:
            logger.warning(f"[Session {session_id}] Auth failed: {auth_error['message']}")
            await consumer.close(code=auth_error["code"])
        else:
            logger.warning(f"[Session {session_id}] Unauthenticated user")
            await consumer.close(code=4000)
        return True  # Auth failed
    return False  # Auth succeeded

# Then in consumers:
if await check_auth_and_close_if_failed(self, self.session_id):
    return

3. Inconsistent Logging Levels

Files: config/websocket/middleware.py, websocket_auth0_middleware.py

Issue: Mix of logger.debug(), logger.info(), and logger.warning() for similar events:

  • Line 81: logger.warning("No token provided") - should this be warning? It's expected for anonymous users
  • Line 88: logger.info("Successfully authenticated") - duplicated at line 40
  • Line 118: logger.info("Authentication complete") - already logged success at line 40

Recommendation:

  • Use logger.debug() for normal flow (no token, successful auth)
  • Use logger.warning() only for actual problems (expired/invalid tokens)
  • Remove duplicate log statements

4. Missing Error Code Constants in Consumers

Files: All consumer files

Issue: Consumers hard-code 4000 for generic auth failure instead of importing constants:

await self.close(code=4000)  # Magic number

Recommendation:

from config.websocket.middleware import WS_CLOSE_TOKEN_INVALID

await self.close(code=WS_CLOSE_TOKEN_INVALID)

Or define a WS_CLOSE_UNAUTHENTICATED = 4000 constant if this represents "no token provided" vs "invalid token".

5. Unused Import Removal Not Complete

File: config/websocket/middleware.py:2

Issue: CHANGELOG claims Union import was removed, but I don't see it in the original imports. Verify this change was actually needed.

6. Test Coverage Gaps

Missing Tests:

  1. Integration test: Full request cycle through Django middleware → Auth backend → GraphQL resolver with expired token
  2. Consumer behavior test: Verify consumers actually receive and use the auth_error from scope (current tests only check middleware)
  3. Backwards compatibility: Test that valid tokens still work through the entire stack
  4. Edge case: What happens if auth_error dict is malformed (missing 'code' or 'message')?

Recommendation:

# Add integration test in test_token_expiration.py
@pytest.mark.django_db
def test_expired_token_propagates_to_graphql_layer(self):
    """Verify expired token error message reaches GraphQL response."""
    # Create expired token
    # Make GraphQL request
    # Assert response contains "Signature has expired"

7. Potential Race Condition in Middleware

Files: Both middleware files

Issue: The auth_error is set on scope during middleware execution, but scope is shared across the entire connection lifecycle. If middleware runs again (reconnection?), the old auth_error might persist.

Current Code:

scope["auth_error"] = None  # Only set once at start

Recommendation: Verify that scope is fresh for each connection attempt, or explicitly clear previous auth_error state.


🔒 Security Considerations

✅ Good Security Practices:

  1. No sensitive data in error messages: Error messages don't leak token contents
  2. Proper exception distinction: Expired vs invalid tokens correctly differentiated
  3. Audit logging: All auth failures are logged with appropriate context

⚠️ Minor Concerns:

  1. Token logging: Line 31 in middleware.py logs token[:20] - consider reducing to token[:10] or removing entirely in production
  2. Error message verbosity: f"Invalid token: {e}" might leak token format details - consider generic message

📊 Performance Considerations

✅ No Performance Impact:

  • Exception re-raising has negligible overhead
  • scope.get("auth_error") is O(1) dict lookup
  • No additional database queries introduced

🎯 Best Practices Alignment

✅ Follows Project Standards:

  • Uses project logging patterns
  • Follows CHANGELOG format from CLAUDE.md
  • Uses @pytest.mark.serial for WebSocket tests
  • Proper use of database_sync_to_async

⚠️ Deviations:


🧪 Testing Recommendations

Before Merge:

  1. Run the new test suite: docker compose -f test.yml run django pytest opencontractserver/tests/test_token_expiration.py -n 4 --dist loadscope
  2. Run affected consumer tests: Ensure existing WebSocket tests still pass
  3. Manual testing:
    • Expire a token (set clock forward or wait for natural expiration)
    • Verify frontend receives 4001 close code and refreshes
    • Verify invalid token receives 4002 close code

📋 Recommendations Summary

Must Fix Before Merge:

  1. Verify Django auth middleware compatibility with re-raised exceptions (CRITICAL)
  2. Add integration test for full auth flow with expired token
  3. Test backwards compatibility with valid tokens

Should Fix Before Merge:

  1. Consolidate duplicate auth_error handling into shared utility
  2. Define WS_CLOSE_UNAUTHENTICATED constant instead of magic number 4000
  3. Add consumer-level test verifying auth_error is used correctly

Nice to Have:

  1. Reduce logging verbosity and fix inconsistent log levels
  2. Reduce token logging or remove in production
  3. Add edge case tests for malformed auth_error dict

✨ Overall Assessment

Rating: 7.5/10

This is a well-designed solution to a real problem with good test coverage and clear documentation. The core logic is sound and the changes are focused.

However, the critical concern about re-raising exceptions in the Django auth backend needs investigation before merge. Additionally, the code duplication across consumers should be addressed to maintain code quality.

Recommendation:

  • Approve with minor changes required
  • Address the critical auth backend concern
  • Consider consolidating the duplicated code
  • Add the recommended integration tests

Great work on the root cause analysis and comprehensive solution! The CHANGELOG documentation is particularly well done. 👏

Changes:
- Update tests to expect 4002 (WS_CLOSE_TOKEN_INVALID) for invalid tokens
  instead of 4000, reflecting the new specific error code behavior
- Consolidate duplicate auth_error handling across 5 consumers into shared
  utility function `check_auth_and_close_if_failed()` in auth_helpers.py
- Add `WS_CLOSE_UNAUTHENTICATED` constant (4000) and replace all magic
  numbers in consumers with the defined constant
- Fix inconsistent logging levels: use debug for normal flow, warning only
  for actual problems (expired/invalid tokens)
- Reduce token logging verbosity from 20 to 10 characters
- Add clarifying comment about scope lifecycle to address race condition
  concern (each WebSocket connection gets fresh scope)
- Add docstring to Auth0RemoteUserJSONWebTokenBackend explaining why it
  re-raises JSONWebTokenExpired (designed for graphql_jwt, not Django auth)
- Add tests for WS_CLOSE_UNAUTHENTICATED constant consistency
- Add new test for no-token public document access (test renamed from
  test_invalid_token_public_document_connects_as_anonymous)

Test files updated:
- test_standalone_document_consumer.py: Update expected close codes
- test_websocket_auth.py: Import and use WS_CLOSE_TOKEN_INVALID constant
- test_token_expiration.py: Add tests for unauthenticated close code
@claude
Copy link

claude bot commented Dec 29, 2025

Pull Request Review - Issue #744: Token Expiration Signaling

Summary

This PR effectively addresses issue #744 by ensuring that expired JWT tokens are properly signaled to the frontend via both GraphQL and WebSocket connections. The implementation is well-thought-out and follows good architectural patterns.

✅ Strengths

1. Excellent Architecture & Design

  • Clear separation of concerns: Auth error detection happens in middleware, consumers use a shared helper to act on it
  • DRY principle: The new check_auth_and_close_if_failed() utility eliminates code duplication across 5 consumers
  • Proper exception handling: Re-raising JSONWebTokenExpired in Auth0RemoteUserJSONWebTokenBackend is the correct approach for graphql_jwt integration
  • Well-documented close codes: Using application-specific codes (4001, 4002) in the 4000-4999 range is per WebSocket spec

2. Comprehensive Testing

  • 236 lines of new tests covering backend, middleware, and close code consistency
  • Tests verify the critical path: expired token → exception → auth_error in scope → correct close code
  • Close code consistency tests prevent drift between Auth0 and non-Auth0 middlewares
  • Uses proper mocking to isolate token expiration scenarios

3. Detailed Documentation

  • Excellent inline documentation explaining the "why" behind design decisions
  • CHANGELOG entries include file paths and line numbers (follows project conventions)
  • Clear docstrings on Auth0RemoteUserJSONWebTokenBackend explaining deviation from typical Django auth backend patterns

4. Error Signaling Design

The scope['auth_error'] pattern is clean:

scope['auth_error'] = {
    'code': WS_CLOSE_TOKEN_EXPIRED,  # 4001
    'message': 'Token has expired. Please refresh your session.'
}

This allows consumers to distinguish between:

  • No token provided (anonymous access)
  • Token expired (refresh needed)
  • Token invalid (re-authentication needed)

🔍 Code Quality Observations

Minor Issues

  1. Inconsistent Error Handling in Auth0 Middleware (config/websocket/middlewares/websocket_auth0_middleware.py:89-97)

    When get_user_by_token() returns None or a non-User object, the code sets auth_error but doesn't set scope['user'] = AnonymousUser() since it's already set. However, this case seems unlikely given that get_user_by_token() should raise an exception if the user isn't found. Consider adding a comment explaining this edge case or removing the defensive check if it's unreachable.

  2. Import Cleanup (config/graphql_auth0_auth/utils.py:124)

    Good fix moving the import inside the function! However, the comment says "Lazy import to avoid circular dependency when USE_AUTH0=False" - it might be worth verifying this is the actual issue rather than just an import-time error when the module isn't configured. The real reason appears to be avoiding import errors when Auth0 is disabled.

  3. Test Coverage Gap

    The WebSocketTokenExpirationTestCase tests check that connections are rejected (assertFalse(connected)), but they don't verify the specific close codes (4001 vs 4002) are actually sent. Consider checking close_code in the test assertions:

    self.assertEqual(close_code, WS_CLOSE_TOKEN_EXPIRED)
  4. Logging Consistency

    The middleware files use a mix of logger.debug() and logger.info() for successful auth. The PR changes most to debug, which is appropriate since successful auth is normal operation. Ensure this is consistent across all files.

🔒 Security Considerations

✅ Good Security Practices

  1. No information leakage: Error messages distinguish between "expired" and "invalid" only for debugging - both tell the client to re-authenticate
  2. Proper exception propagation: JSONWebTokenExpired bubbles up to GraphQL layer for proper handling
  3. Backward compatibility: Other JWT errors still return None (not breaking existing error handling)

⚠️ Potential Concern

Close Code Semantics (Minor)

The PR introduces WS_CLOSE_TOKEN_EXPIRED (4001) to signal "please refresh your token". However, there's a security consideration: if a malicious client receives 4001, they know the token format is valid but expired, which could aid in token guessing attacks.

Recommendation: This is likely acceptable since:

  • JWT expiration is already visible in the token itself (it's base64-encoded, not encrypted)
  • The benefit of proper UX (automatic token refresh) outweighs minimal information leakage
  • Document this consideration if not already discussed

🧪 Testing Recommendations

1. Integration Test for Full Flow

Consider adding an integration test that verifies the complete flow:

  1. Create valid JWT token
  2. Manually expire it (mock time or create with exp in past)
  3. Connect via WebSocket
  4. Verify close code is 4001
  5. Verify frontend receives appropriate error message

2. Test Consumer Behavior

While check_auth_and_close_if_failed() is well-tested, verify one consumer end-to-end:

async def test_document_consumer_closes_with_expired_token(self):
    # Setup expired token
    # Connect to DocumentConversationConsumer
    # Verify close_code == WS_CLOSE_TOKEN_EXPIRED

3. Test settings/test.py Defaults

The new Auth0 settings in config/settings/test.py should be validated - add a test that imports auth modules with USE_AUTH0=False to ensure no import errors.

📊 Performance Considerations

✅ No Performance Impact

  • Auth checks happen once per WebSocket connection (not per message)
  • database_sync_to_async wrapper on get_user_from_token() is appropriate
  • No additional database queries introduced

🎯 Best Practices Adherence

Follows OpenContracts Guidelines ✅

  1. No dead code: Old inline auth checks removed after utility function introduced
  2. DRY principle: check_auth_and_close_if_failed() consolidates 5 duplicate implementations
  3. Single Responsibility: Middleware handles detection, helper handles response, consumers handle business logic
  4. No magic numbers: Close codes are named constants with documentation
  5. Comprehensive CHANGELOG: Includes file paths, line numbers, and impact analysis

Suggestions for Improvement

  1. Constants file: Consider moving WebSocket close codes to opencontractserver/constants/ since they're used across multiple files (currently duplicated in two middleware files)

  2. Consumer consistency: All consumers now use WS_CLOSE_UNAUTHENTICATED (4000) for permission failures that aren't token-related. This is good, but verify the frontend handles 4000, 4001, and 4002 distinctly.

🐛 Potential Bugs

1. Race Condition in Token Refresh (Theoretical)

If the frontend's errorLink.ts triggers a page reload on "Signature has expired", there's a window where multiple GraphQL requests could all detect expiration and trigger multiple reloads. This is likely handled by the frontend's Auth0 silent refresh mechanism, but worth verifying.

2. WebSocket Reconnection Loop (Edge Case)

If a token expires while a WebSocket connection is active:

  1. Next message triggers auth check
  2. Connection closes with 4001
  3. Frontend refreshes token
  4. Reconnects
  5. If token refresh fails, infinite loop?

Recommendation: Verify frontend has reconnection backoff and max retry limits.

📝 Documentation Improvements

  1. Add to docs/architecture/: Consider documenting the WebSocket authentication flow in a dedicated doc under docs/architecture/websocket_auth.md

  2. Frontend integration: Document the expected frontend behavior for each close code (4000, 4001, 4002) in the authentication pattern docs

  3. Migration guide: If this changes behavior (e.g., tokens that previously failed silently now trigger refreshes), document for operators

✨ Final Verdict

APPROVED with minor suggestions for follow-up.

This is a high-quality PR that solves a real UX problem (silent token expiration) with a clean, well-tested implementation. The architectural decisions are sound, the code is maintainable, and the testing is comprehensive.

Recommended Actions Before Merge:

  1. ✅ Verify all tests pass (especially parallel pytest runs)
  2. ✅ Confirm frontend errorLink.ts properly handles the new "Signature has expired" message
  3. ⚠️ Consider adding integration test for full token expiration flow (can be follow-up)
  4. ⚠️ Consider moving close codes to constants file (can be follow-up)
  5. ⚠️ Verify frontend has WebSocket reconnection limits (can be follow-up)

Score: 9/10

Excellent work! The only reason it's not a 10 is the minor testing gap (not verifying close codes in assertions) and the potential for consolidating constants. These are minor and don't block merging.


Reviewed by: Claude Code
Review Date: 2025-12-29

…odes

- Fix duplicate username 'admin' conflicts in parallel test runs by using
  unique usernames (agent_result_superuser, corpus_exec_superuser,
  badge_idor_superuser)
- Update WebSocket tests to expect close code 4002 for invalid tokens
  (aligned with recent auth_error changes)
- Convert PydanticAI tests from run_sync() to async/await to avoid
  "event loop is closed" errors
- Fix extract task callback test to patch function directly instead of
  .si method (works correctly in eager mode)
@claude
Copy link

claude bot commented Dec 29, 2025

Code Review: Token Expiration Signaling Fix (Issue #744)

Summary

This PR successfully addresses issue #744 by ensuring that JWT token expiration is properly signaled to the frontend, enabling automatic token refresh. The implementation is well-architected with comprehensive test coverage and excellent documentation.


Strengths

1. Excellent Architecture & DRY Principles

  • New check_auth_and_close_if_failed() utility (config/websocket/utils/auth_helpers.py) - Eliminates code duplication across 5 WebSocket consumers
  • Centralized close code constants - WS_CLOSE_TOKEN_EXPIRED (4001), WS_CLOSE_TOKEN_INVALID (4002), WS_CLOSE_UNAUTHENTICATED (4000)
  • Consistent scope['auth_error'] pattern - Both JWT and Auth0 middlewares use the same error structure

2. Comprehensive Test Coverage

The new test_token_expiration.py file provides excellent coverage:

  • Backend authentication behavior (Auth0RemoteUserJSONWebTokenBackend)
  • WebSocket middleware error handling
  • Close code consistency validation
  • Edge cases (no token, invalid token, expired token)

3. Outstanding Documentation

  • CHANGELOG.md entries are exceptionally detailed with file paths and line numbers
  • Inline code comments explain the "why" behind design decisions (e.g., why JSONWebTokenExpired is re-raised)
  • Docstrings are comprehensive (see Auth0RemoteUserJSONWebTokenBackend class docstring)

4. Proper Error Handling

  • JSONWebTokenExpired is correctly re-raised in Auth0RemoteUserJSONWebTokenBackend.authenticate() (line 44-52)
  • Other JWT errors still return None for backward compatibility
  • Middlewares properly distinguish between expired vs invalid tokens

5. Security Best Practices

  • No sensitive information leaked in error messages
  • Proper use of WebSocket close codes (4000-4999 range for application use)
  • Maintains anonymous access support for public documents

🔍 Issues & Recommendations

Critical: Import Error Risk

Location: config/graphql_auth0_auth/utils.py:124

Issue: While the lazy import fix is good, there's still a potential issue:

from opencontractserver.users.tasks import sync_remote_user

If USE_AUTH0=False, this import could still fail if the tasks.py module depends on Auth0-specific dependencies.

Recommendation:

try:
    from opencontractserver.users.tasks import sync_remote_user
    sync_remote_user.delay(user.username)
except ImportError:
    logger.warning("Auth0 sync task unavailable (USE_AUTH0=False?)")

Minor: Log Level Inconsistency

Location: config/websocket/middleware.py:84-87

Issue: Changed from logger.info() to logger.debug() for "No token provided" message, but this may make troubleshooting harder in production.

Recommendation: Consider using logger.info() for connection attempts and logger.debug() for token details. This maintains visibility into connection patterns without spamming logs.

Minor: Unused Import Cleanup

Location: config/websocket/middleware.py:2

Issue: The CHANGELOG mentions removing Union import, but verify no other unused imports remain.

Recommendation: Run pre-commit run --all-files to ensure import cleanup is complete.

Documentation: Missing Frontend Integration Guide

Issue: While the backend changes are well-documented, there's no mention of how the frontend's errorLink.ts handles these new close codes.

Recommendation: Add a section to the CHANGELOG or a comment in the middleware explaining:

  • Which frontend file handles the close codes
  • How 4001 triggers token refresh vs 4002 triggers re-auth
  • Link to frontend documentation if it exists

Testing: WebSocket Consumer Tests

Location: opencontractserver/tests/test_token_expiration.py:120-175

Issue: The WebSocket tests verify connection rejection but don't assert the exact close code received:

self.assertFalse(connected)  # Should also check close_code == WS_CLOSE_TOKEN_EXPIRED

Recommendation: Enhance assertions:

connected, close_code = await communicator.connect()
self.assertFalse(connected)
self.assertEqual(close_code, WS_CLOSE_TOKEN_EXPIRED)

🛡️ Security Assessment

No security concerns identified:

  • Error messages don't leak sensitive information
  • Token validation logic is sound
  • No IDOR or authentication bypass risks
  • Proper use of close codes prevents timing attacks

Performance Considerations

Minimal performance impact:

  • check_auth_and_close_if_failed() is a lightweight async function
  • No additional database queries introduced
  • Middleware error handling is efficient (no try/except overhead for success path)

⚠️ Minor concern: Lazy import in configure_user() adds slight overhead, but this is negligible and only affects user creation (not hot path).


📋 Pre-Merge Checklist

Before merging, verify:

  • Run pre-commit run --all-files to ensure code quality
  • Run docker compose -f test.yml run django pytest opencontractserver/tests/test_token_expiration.py -n 4 --dist loadscope to verify tests pass
  • Verify frontend properly handles new close codes (4001, 4002)
  • Test Auth0 flow in staging environment
  • Confirm CHANGELOG entries match actual changes

🎯 Overall Assessment

LGTM with minor recommendations. This is high-quality work that follows the project's conventions and solves the problem elegantly.

Score: 9/10

  • Excellent architecture and code organization
  • Comprehensive test coverage
  • Outstanding documentation
  • Minor improvements recommended but not blocking

Recommended Action: Approve with optional follow-up for the testing enhancement and import error handling.

@codecov
Copy link

codecov bot commented Dec 29, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@JSv4 JSv4 merged commit df083fe into main Dec 30, 2025
8 checks passed
@JSv4 JSv4 deleted the claude/investigate-issue-744-ygZ7R branch December 30, 2025 18:11
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.

Signal to Frontend When Token is Stale

3 participants