Skip to content

Origin/2378 Add expires_at on dcr client#2565

Merged
crivetimihai merged 2 commits intomainfrom
origin/2378-add-expires-at-on-dcr-client
Jan 29, 2026
Merged

Origin/2378 Add expires_at on dcr client#2565
crivetimihai merged 2 commits intomainfrom
origin/2378-add-expires-at-on-dcr-client

Conversation

@suciu-daniel
Copy link
Copy Markdown
Collaborator

@suciu-daniel suciu-daniel commented Jan 29, 2026

🐛 Bug-fix PR

Before opening this PR please:

  1. make lint - passes ruff, mypy, pylint
  2. make test - all unit + integration tests green
  3. make coverage - ≥ 90 %
  4. make docker docker-run-ssl or make podman podman-run-ssl
  5. Update relevant documentation.
  6. Tested with sqlite and postgres + redis.
  7. Manual regression no longer fails. Ensure the UI and /version work correctly.

📌 Summary

In DCRService.register_client, the registered client had expires_at set to None, with a TODO that client_secret_expires_at should be use to populate this field

🔁 Reproduction Steps

  1. Register a mcpgateway with OAuth configured to use DCR
  2. Complete the authorization
  3. Check the registered clients expiration date, which is set to None.

🐞 Root Cause

The issue was the expires_at was set to None on all cases, with a TODO on what it should be next to it.

💡 Fix Description

I used client_secret_expires_at which is a RFC7591 key for DCR, to set our expires_at key.

🧪 Verification

Check Command Status
Lint suite make lint
Unit tests make test
Coverage ≥ 90 % make coverage
Manual regression no longer fails steps / screenshots

📐 MCP Compliance (if relevant)

  • Matches current MCP spec
  • No breaking change to MCP clients

✅ Checklist

  • Code formatted (make black isort pre-commit)
  • No secrets/credentials committed

Solves [BUG][SONAR][LOW]: Missing expires_at calculation in DCR client registration

@suciu-daniel suciu-daniel self-assigned this Jan 29, 2026
@suciu-daniel suciu-daniel changed the title Origin/2378 add expires at on dcr client Origin/2378 add expires_at on dcr client Jan 29, 2026
@suciu-daniel suciu-daniel changed the title Origin/2378 add expires_at on dcr client Origin/2378 Add expires_at on dcr client Jan 29, 2026
@crivetimihai
Copy link
Copy Markdown
Member

Rebase & Review Summary

Changes made during rebase:

  • Rebased onto main to remove merge commit (now single clean commit)
  • Updated commit message to follow conventional commits format:
    fix(oauth): populate expires_at from client_secret_expires_at in DCR registration
    

Code Review: ✅ Approved

RFC 7591 Compliance: The implementation correctly uses client_secret_expires_at from the DCR response:

  • Per RFC 7591 Section 3.2.1, this is an optional field containing a Unix timestamp
  • Value of 0 means "never expires" - correctly handled by the > 0 check
  • The original TODO was misleading (suggested summing two values, but client_secret_expires_at is already an absolute timestamp)

Security: No concerns - this is informational metadata only

Performance: No impact - single dictionary lookup + conditional datetime conversion

Consistency: Uses the same datetime.fromtimestamp(..., tz=timezone.utc) pattern as elsewhere in the codebase

Tests: All 176 OAuth-related unit tests pass

@crivetimihai
Copy link
Copy Markdown
Member

Final Review & Fixes Summary

Rebased and significantly enhanced the original implementation with multiple rounds of review.

Key Improvements Made:

1. Type Safety & Coercion

  • Added int() coercion for string timestamps from non-strict AS implementations
  • Added proper exception handling for ValueError, TypeError, OSError, OverflowError

2. Millisecond Timestamp Detection

  • Detects millisecond timestamps using digit count (>= 13 digits)
  • Automatically converts to seconds instead of failing silently
  • Uses logger.debug to avoid log noise

3. Parse Failure Handling

  • Changed _parse_expires_at_timestamp() to return tuple[datetime | None, bool]
  • On parse failure: preserves existing expires_at instead of clearing to None
  • On explicit 0: sets to None (RFC 7591 "never expires")
  • On valid timestamp: sets to parsed datetime

4. Update Path Enhancement

  • update_client_registration now updates expires_at independent of secret rotation
  • An AS can refresh expiry without rotating the secret

5. Comprehensive Test Coverage (11 new tests)

  • Valid timestamp parsing
  • Zero value (never expires)
  • Missing field
  • String coercion
  • Invalid value handling
  • Millisecond conversion
  • Secret rotation scenarios
  • Expiry-only updates
  • Preserve on invalid/negative values

Files Changed

  • mcpgateway/services/dcr_service.py: +64 lines (helper method + integration)
  • tests/unit/mcpgateway/services/test_dcr_service.py: +540 lines (11 new tests)

Test Results

✅ 41 DCR tests pass
✅ 183 OAuth tests pass (full suite)

@crivetimihai
Copy link
Copy Markdown
Member

Complete Change Summary

Original PR

Added expires_at population from client_secret_expires_at in DCR registration.

Enhancements Made During Review

1. Type Safety & Error Handling

  • Added int() coercion for string timestamps from non-strict AS implementations
  • Comprehensive exception handling: ValueError, TypeError, OSError, OverflowError
  • Negative value rejection with warning log

2. Millisecond Timestamp Detection

  • Detects millisecond timestamps using digit count (>= 13 digits)
  • Automatically converts ms → seconds instead of failing
  • Changed from logger.info to logger.debug to reduce noise

3. Tuple Return Pattern for Safe Updates
Created _parse_expires_at_timestamp() returning tuple[datetime | None, bool]:

(datetime, True)  → Valid timestamp, update expires_at
(None, True)      → Explicit 0 ("never expires"), set to None  
(None, False)     → Parse failed, preserve existing expires_at

4. Update Path Enhancement

  • update_client_registration now updates expires_at independent of secret rotation
  • Only updates when should_update=True, preserving existing value on parse failure
  • Handles AS implementations that refresh expiry without rotating secret

5. Test Coverage (11 new tests)

Test Purpose
test_register_client_sets_expires_at_from_client_secret_expires_at Valid timestamp
test_register_client_expires_at_none_when_zero RFC 7591 "never expires"
test_register_client_expires_at_none_when_missing Missing field
test_register_client_handles_string_client_secret_expires_at String coercion
test_register_client_handles_invalid_client_secret_expires_at Invalid value
test_register_client_converts_millisecond_timestamp ms → s conversion
test_update_client_registration_updates_expires_at Update with rotation
test_update_client_registration_sets_expires_at_none_when_zero Update to never expires
test_update_client_registration_updates_expires_at_without_secret_rotation Expiry-only update
test_update_client_registration_preserves_expires_at_on_invalid_value Preserve on invalid
test_update_client_registration_preserves_expires_at_on_negative_value Preserve on negative

Files Changed

mcpgateway/services/dcr_service.py                 |  64 ++-
tests/unit/mcpgateway/services/test_dcr_service.py | 540 +++++++++++++++++++++
2 files changed, 603 insertions(+), 1 deletion(-)

Test Results

  • ✅ 41 DCR service tests pass
  • ✅ 183 OAuth tests pass (full suite)
  • ✅ All existing tests unaffected

@crivetimihai crivetimihai merged commit 256ac24 into main Jan 29, 2026
53 checks passed
@crivetimihai crivetimihai deleted the origin/2378-add-expires-at-on-dcr-client branch January 29, 2026 18:42
@crivetimihai crivetimihai added this to the Release 1.0.0-RC1 milestone Jan 31, 2026
hughhennelly pushed a commit to hughhennelly/mcp-context-forge that referenced this pull request Feb 8, 2026
Signed-off-by: hughhennnelly <hughhennelly06@gmail.com>
kcostell06 pushed a commit to kcostell06/mcp-context-forge that referenced this pull request Feb 24, 2026
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.

2 participants