Skip to content

feat(auth): add session refresh and validate endpoints - #6235

Merged
Lang-Akshay merged 4 commits into
mainfrom
feat/session-refresh-validate
Aug 21, 2026
Merged

feat(auth): add session refresh and validate endpoints#6235
Lang-Akshay merged 4 commits into
mainfrom
feat/session-refresh-validate

Conversation

@madhu-mohan-jaishankar

@madhu-mohan-jaishankar madhu-mohan-jaishankar commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

🔗 Related Issue

Closes #6011


📝 Summary

Adds session-lifecycle endpoints so UI clients can implement silent refresh and idle detection now that TOKEN_EXPIRY defaults to 20 minutes:

  • POST /auth/refresh — issues a new session JWT for a valid session. Preserves the old token's scope narrowing, rotates the CSRF token (CSRF tokens are HMAC-bound to the JWT jti, which changes on refresh), and re-sets the jwt_token cookie when the request authenticated via cookie. Refuses API tokens and any token without token_use="session" with 403 (fail-closed) — long-lived API tokens have their own expiry/revocation lifecycle. Refresh events are audit-logged via AuditTrailService.
  • GET /auth/validate — reports valid, expires_at / expires_in, the user profile, session_source (local | sso | api_token, derived from the token_use / auth_provider / source claims), and a config block of session-lifecycle values (normalized to seconds) so clients don't hardcode them.

Both endpoints live in mcpgateway/routers/auth.py and are exposed at /v1/auth/* (canonical) plus the unversioned legacy shim, same as the sibling /auth/login / /auth/logout. The issue's /api/auth/* paths don't exist in the codebase; the implemented paths match the actual v1/legacy mounts and the pre-existing manual test case (AUTH-004) and CSRF config references.

Supporting changes:

  • SESSION_MAX_LIFETIME (default 480 min, 0 disables): absolute session-age cap enforced at refresh via a session_start claim carried across refreshes — silent refresh cannot extend a session forever. No SESSION_IDLE_TIMEOUT was added: server-side idle enforcement already exists as TOKEN_IDLE_TIMEOUT (revocation-backed, in mcpgateway/auth.py); /auth/validate surfaces it to clients instead of duplicating the knob.
  • SESSION_WARNING_TIME, SESSION_REFRESH_BUFFER, SESSION_ACTIVITY_TRACKING: client-behavior hints surfaced via /auth/validate.
  • Rate limiting via a new SESSION_REFRESH tier in RateLimitMiddleware (pattern covers both /auth/refresh and /v1/auth/refresh), configured by SESSION_REFRESH_RATE_LIMIT (default 10/min). Reuses the existing Redis-backed, multi-dimensional (IP / user / team) infrastructure rather than adding a bespoke limiter.
  • /auth/refresh removed from default CSRF_EXEMPT_PATHS: the entry predated the endpoint; a cookie-authenticated, CSRF-exempt refresh would let a cross-site request silently keep a victim's session alive. Bearer requests skip CSRF by design, so API clients are unaffected.
  • create_access_token() accepts an optional extra_claims dict (reserved claims cannot be overridden) for the session_start carry-over.
  • Refresh is single-use rotation: the predecessor token is revoked (blocklisted with reason=token_refresh and its original expiry) before the new token is minted, with compare-and-set semantics so concurrent refreshes cannot both succeed; refresh fails closed (401) if the revocation cannot be persisted.

📏 Reviewability

  • This PR has one clear purpose
  • The linked issue is not labeled triage
  • Unrelated bugs or improvements are tracked in separate issues/PRs
  • Tests are included with the code they validate
  • If AI-assisted, I understand and can explain the generated changes

🏷️ Type of Change

  • Feature / Enhancement

🧪 Verification

Check Command Status
Lint (changed files) ruff check, isort --check-only, pylint (9.96), bandit (0 issues), interrogate (100%)
Unit tests (affected) pytest tests/unit/mcpgateway/routers/ tests/unit/mcpgateway/middleware/test_rate_limit_middleware.py tests/unit/mcpgateway/middleware/test_csrf_middleware.py tests/unit/mcpgateway/test_config.py — 1586 passed, 5 skipped (pre-existing)
mypy (changed files) mypy mcpgateway/routers/auth.py — 4 errors, identical to pre-change baseline (pre-existing debt, none introduced)
Full make lint / make test / make coverage pending CI

New test coverage (13 tests in test_auth.py, 1 in test_rate_limit_middleware.py), including the deny paths: API-token refresh refused (403), missing-token_use token refused (403), no session token (401), max-lifetime exceeded (401), session_start carried over on refresh, SSO / external-IdP / api_token session_source classification, cookie-vs-bearer cookie behavior, and SESSION_REFRESH tier matching on both mounts.


✅ Checklist

  • Code formatted (make black isort pre-commit)
  • Tests added/updated for changes
  • Documentation updated (if applicable)
  • No secrets or credentials committed

📓 Notes (optional)

  • Design decisions not specified by the issue: SSO-established sessions may refresh locally in this phase (the delegated SSO refresh flow is a later phase of [EPIC][UI-REWRITE][SECURITY]: Session timeout, idle detection, and silent refresh for the React UI #5804); unauthenticated /auth/validate returns 401 (consistent with the auth dependency) rather than {valid: false}; enforced durations follow the repo's minutes convention (TOKEN_EXPIRY-style) while client hints are in seconds — /auth/validate normalizes everything to seconds.
  • tests/unit/mcpgateway/middleware/test_rate_limit_middleware.py includes isort normalization of pre-existing import-heading debt (comment-only hunks) — the file was non-compliant before this change; touching it triggered the standard formatter cleanup.
  • Deployments that set CSRF_EXEMPT_PATHS explicitly in .env should drop /auth/refresh from their list to pick up the secure default.

@a-effort

Copy link
Copy Markdown
Collaborator

LGTM! ✅

@Lang-Akshay Lang-Akshay left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocking Changes

# Area File Line Blocking reason Required change
1 Security mcpgateway/routers/auth.py 385-396, 479-507 High / CWE-347: refresh and validation decode the request token with signature verification disabled. The security handoff found that get_current_user can involve a plugin/custom auth path, so the handler may trust forged token_use, session_start, scopes, and expiry claims. Use the exact payload returned by a verified JWT path, or reject refresh/validate for non-gateway-JWT auth methods. For cookie auth, verify the selected cookie token and bind its subject/JTI to the authenticated user before making session decisions.
2 Issue / Quality mcpgateway/config.py, .env.example config.py:385-403, .env.example:1090-1105 Linked issue #6011 explicitly requires SESSION_IDLE_TIMEOUT with a 900-second default. The PR instead exposes the pre-existing TOKEN_IDLE_TIMEOUT (60-minute default) and does not add the requested setting. Add and surface SESSION_IDLE_TIMEOUT with the issue's required default/units, or obtain an explicit issue-level decision changing that acceptance criterion.
3 Issue / Verification PR metadata N/A Issue #6011 requires make verify; the PR description marks full verification as pending CI, so this required acceptance criterion is unresolved in the reviewed state. Run and pass the required verification in CI before merge, and attach the result to the PR.

@madhu-mohan-jaishankar

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review @Lang-Akshay — the security catch in particular was a real gap. Here's where each finding stands:

1. Unverified token decode (CWE-347) — Fixed in a052f0b. Both endpoints now route the extracted token through verify_jwt_token_cached() (full signature/expiry/audience verification) before any session decision, and additionally check the jti against the revocation blocklist and bind the token's sub to the authenticated user. A request that authenticated through a non-gateway-JWT path (plugin/basic/proxy) gets 401 from both endpoints, since there is no gateway session to refresh or validate. New deny-path tests cover forged signature, revoked jti, and subject mismatch on refresh, plus forged/missing token on validate. The normal bearer path does no extra crypto work — verify_jwt_token_cached is a per-request cache hit for the token the auth dependency already verified.

2. SESSION_IDLE_TIMEOUT — This one was a deliberate deviation rather than an oversight, and worth discussing. Server-side idle enforcement already exists as TOKEN_IDLE_TIMEOUT (revocation-backed, in mcpgateway/auth.py), and GET /auth/validate surfaces it to clients as config.idle_timeout in seconds — which covers the linked issue's underlying goal (clients shouldn't hardcode the value). Adding a second setting named SESSION_IDLE_TIMEOUT would give operators two knobs that appear to control the same behavior while only one is enforced. The deviation is documented in the PR description under "Supporting changes". If there's a strong preference for the literal acceptance criterion I'm happy to revisit, but two sources of truth for one enforcement point seemed like the worse trade.

3. Verificationmake verify passes locally (it's the packaging gate: twine / check-manifest / pyroma). The CI failures on the previous push are also fixed in a052f0b: the pre-commit failure was a ruff format line-wrap in config.py, and the four pytest failures were tests in test_auth_logout.py / test_csrf_fixes.py whose mock patch targets pointed at import paths that moved to module level — they now patch mcpgateway.routers.auth.* directly. Full lint/test/coverage results will be on this push's CI run.

@Lang-Akshay Lang-Akshay left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocking Changes

# Area File Line Blocking reason Required change
1 Quality / Issue mcpgateway/routers/auth.py 478-492, 511-531 Both endpoints depend on get_current_user(), while the configured bearer dependency reads only the auth header. Cookie extraction in _extract_raw_token() is reached only after dependency authentication; a cookie-only request therefore fails before refresh/validate logic runs. This contradicts the PR's cookie-session requirement and makes the from_cookie cookie-reset branch unreachable in the normal route path. Make the route dependency authenticate the supported session cookie (preferably via the existing centralized verifier and user resolver), or use an auth dependency that accepts the cookie and preserves the same subject/revocation checks. Add an end-to-end cookie-only smoke path.

@madhu-mohan-jaishankar

madhu-mohan-jaishankar commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch again @Lang-Akshay — confirmed and fixed in 52d4052.

@madhu-mohan-jaishankar
madhu-mohan-jaishankar force-pushed the feat/session-refresh-validate branch 2 times, most recently from 127a3b4 to 26a4875 Compare August 19, 2026 18:43

@Lang-Akshay Lang-Akshay left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocking Changes

# Area File Line Blocking reason Required change
1 Security mcpgateway/routers/auth.py 576–579 Refresh generates new_jti but only records old_jti for audit; it never revokes the predecessor. The old bearer/cookie token remains valid and can be replayed to mint more tokens, defeating rotation and logout/revocation expectations (High, CWE-613). Atomically revoke/blocklist old_jti using its original expiry and a refresh reason before returning the new token. Make rotation single-use/compare-and-set so concurrent refreshes cannot both succeed; fail closed if predecessor revocation cannot be persisted.

@madhu-mohan-jaishankar
madhu-mohan-jaishankar force-pushed the feat/session-refresh-validate branch from 938669d to 5e806b2 Compare August 21, 2026 11:38
Lang-Akshay
Lang-Akshay previously approved these changes Aug 21, 2026

@Lang-Akshay Lang-Akshay left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM ✅

Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>
Signed-off-by: Madhu Mohan Jaishankar <madhu.mohan.jaishankar@ibm.com>

@Lang-Akshay Lang-Akshay left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM ✅

@Lang-Akshay
Lang-Akshay added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit c6cfe8b Aug 21, 2026
36 checks passed
@Lang-Akshay
Lang-Akshay deleted the feat/session-refresh-validate branch August 21, 2026 13:16
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.

[FEATURE][API]: Session refresh & validate endpoints (POST /api/auth/refresh, GET /api/auth/validate)

3 participants