Repository navigation
Conversation
There was a problem hiding this comment.
TokenScopingMiddleware only lets a permission-scoped API token through if the path has an entry in its (method, pattern, permission) table. /oauth/status has one. /oauth/token does not, so a token scoped to gateways.read gets a 403 before the handler runs.
|
Both review findings are addressed in cc29983.
Test changes: the handler tests now pass a dict user and keyword arguments, because Not done: the CHANGELOG entry and the full pre-merge gate ( |
cc29983 to
cfd2387
Compare
msureshkumar88
left a comment
There was a problem hiding this comment.
The route is backend-only, off by default, and needs no migration. The Closes #6966 link is present. The token-scoping fix in cfd238736 resolves the earlier finding. Caller identity comes from the authenticated user, the query is keyed by that identity, and refresh_token is never returned. Those parts are sound.
Three items block merge. I list them first, then the remaining findings.
Blocking
blocking | mcpgateway/routers/oauth_router.py:1701 + mcpgateway/middleware/token_scoping.py:165 | The route returns a live upstream bearer token but requires only gateways.read. The viewer and platform_viewer roles hold gateways.read (bootstrap_db.py:476-515), and so does any API token scoped to it. A read-only role can lift the raw provider token and use it outside ContextForge, past RBAC, plugins and audit (CWE-200, CWE-522, CWE-863). | Add a dedicated permission (for example gateways.token_read). Map the route to it. Grant it to developer and team_admin only. Add deny tests for a viewer and for a gateways.read-only API token.
blocking | mcpgateway/routers/oauth_router.py:1769-1775 | The handler maps every None from get_user_token() to 404 "complete the OAuth flow". The DB backend returns None for no row, expired without refresh token, failed refresh, and any swallowed exception (db_backend.py:196-244). A DB or IdP outage tells the user to re-consent. A token still valid but inside threshold_seconds also returns None when the refresh fails. | Return a result with a state. 404 only for no row. 401/409 with a stable code for reauthorization needed. 502/503 for backend or IdP errors. Return the current token when refresh fails and the token has not expired.
blocking | tests/live_gateway/ | AGENTS.md requires a black-box test against a running gateway. All 12 new tests call the handler with mocks. None exercises the real RBAC decorator, TokenScopingMiddleware, SecurityHeadersMiddleware, or a stored token row. | Add a live test: flag off, no token, seeded token returns 200 without refresh_token, other user's token never returned, viewer and scoped token denied.
Functionally impacting
functionally-impacting | oauth_router.py:1752-1754 | No grant-type guard. client_credentials, password and token-exchange gateways hold no per-user token row. The caller gets 404 with a hint to run /oauth/authorize, which cannot work. | Return 400 unless grant_type is authorization_code. Add a test per grant type.
functionally-impacting | oauth_router.py:1708-1713 | threshold_seconds up to 3600 forces a refresh on every call when the provider token lifetime is 3600 seconds or less. There is no rate limit and no refresh lock. Concurrent refreshes can invalidate a rotating refresh token (CWE-770, CWE-362). | Lower the maximum. Add a per-user rate limit. Serialize refresh per gateway and user.
functionally-impacting | oauth_router.py:1776 | get_token_info() is a second backend call (a second Vault GET with retry on the Vault backend). It is unguarded. A raise gives a bare 500 after the token was refreshed. "or {}" hides a failure and returns expires_at null. | Return token and metadata from one storage call. Log and map failures.
functionally-impacting | oauth_router.py:1778-1779 | Token issuance appears only in an INFO log. | Call AuditTrailService.log_action() with its own session, a correlation_id, the user and the gateway. Never log the token.
functionally-impacting | CHANGELOG.md, docs/docs/manage/oauth.md, charts/mcp-stack/values.yaml | No CHANGELOG entry, no endpoint docs, no Helm value for OAUTH_TOKEN_RETRIEVAL_ENABLED. | Add all three. State the permission, the error contract and the exfiltration risk.
Suggestions and minor
suggestion | oauth_router.py:1778 | SecurityHeadersMiddleware already sets "no-store, private", Pragma and Expires on protected paths (security_headers.py:503-510). The handler header and the Response parameter are redundant. | Remove both. Assert the header in the live test.
suggestion | oauth_router.py:1762-1764 | Dead branch. _enforce_gateway_access() already raises 401 for an unknown email (oauth_router.py:589-591). | Delete the check and its test.
suggestion | oauth_router.py:1747-1750 | The 503 detail names the env var and confirms the route exists (CWE-209). | Return 404, or register the route only when the flag is true.
suggestion | oauth_router.py:1701 | The return type is a bare dict. | Add a Pydantic response model so OpenAPI documents it and a schema test enforces "no refresh_token".
suggestion | tests/unit/mcpgateway/routers/test_oauth_router.py | test_threshold_seconds_query_param_is_bounded reads route.dependant internals. | Call the route with TestClient at threshold_seconds=3601 and expect 422.
minor | oauth_router.py:1779 | The f-string log builds the message at every level. | Use lazy %s arguments.
minor | commits | The bandit-suppression commit stands alone. No commit references #6966. | Squash it into the first commit.
Process
Issue #6966 still carries the triage label. The maintainer asked whether OBO token exchange (RFC 8693) already covers this need. The thread has an answer but no decision. Get explicit acceptance on the issue before merge.
The full review, with CWE mapping and test-gap list, is in my local notes. I ran the 12 new tests locally and they pass. I did not run the full suite.
Please do inline comment |
msureshkumar88
left a comment
There was a problem hiding this comment.
The route is backend-only, off by default, and needs no migration. The Closes #6966 link is present. The token-scoping fix in cfd238736 resolves the earlier finding. Caller identity comes from the authenticated user, the query is keyed by that identity, and refresh_token is never returned. Those parts are sound.
Three items block merge. Each has an inline comment, except the live test, which has no file in the diff:
oauth_router.py:1702: the route needs a dedicated permission, notgateways.read.oauth_router.py:1769: the handler maps everyNonefromget_user_token()to 404.- Missing live test:
AGENTS.mdrequires a black-box test against a running gateway. All 12 new tests call the handler with mocks. None exercises the real RBAC decorator,TokenScopingMiddleware,SecurityHeadersMiddleware, or a stored token row. Add a test undertests/live_gateway/: flag off, no token, seeded token returns 200 withoutrefresh_token, another user's token never returned, viewer and scoped token denied.
These items have no line in the diff:
- functionally-impacting |
CHANGELOG.md,docs/docs/manage/oauth.md,charts/mcp-stack/values.yaml| No CHANGELOG entry, no endpoint docs, no Helm value forOAUTH_TOKEN_RETRIEVAL_ENABLED. Add all three. State the permission, the error contract and the exfiltration risk. - minor | commits | The bandit-suppression commit stands alone. No commit references #6966. Squash it into the first commit.
- process | Issue #6966 still carries the
triagelabel. The maintainer asked whether OBO token exchange (RFC 8693) already covers this need. The thread has an answer but no decision. Get explicit acceptance on the issue before merge.
I ran the 12 new tests locally and they pass. I did not run the full suite.
cfd2387 to
d0cd504
Compare
9db7006 to
98a2383
Compare
msureshkumar88
left a comment
There was a problem hiding this comment.
Thanks for the rework. Several earlier findings are now resolved:
- The handler maps failures to explicit states: 404 for no row, 409 for reauthorization, 502 for provider errors, 503 for storage errors. A still-valid token is returned when refresh fails. Both backends follow this.
- Token and metadata come from one
get_user_token_details()call. - The grant-type guard rejects non-
authorization_codegateways. - The caller-controlled
threshold_secondsquery parameter is gone. An operator setting replaces it. - A live gateway test, a CHANGELOG entry and endpoint docs are present. Issue #6966 no longer carries
triage.
Two blocking items remain. Each has an inline comment.
oauth_router.py:1700:tokens.createdoes not restrict the route. Theviewerandplatform_viewerroles hold it.test_oauth_manager.py:1678: the PR makes 5 existing unit tests fail. They pass onmain.
Verification on 98a2383e3:
pytest tests/live_gateway/test_oauth_token_retrieval.py: 9 passed, 1 failed (test_viewer_and_wrong_team_are_denied[viewer]returns 200 withaccess-for-viewer).pyteston the changed unit test files: 5 failed inTestIssuerPinningOnAudienceLearning, all others passed.
Findings without a diff line:
functionally-impacting | charts/mcp-stack/values.yaml | No Helm value for OAUTH_TOKEN_RETRIEVAL_ENABLED or OAUTH_TOKEN_REFRESH_THRESHOLD_SECONDS. | Add both with the config.py defaults.
minor | PR description | The Notes section still describes the removed threshold_seconds query parameter and the gateways.read check. | Update it to the operator setting and the final permission.
minor | commits | The bandit-suppression commit stands alone. No commit references #6966. | Squash before merge.
Suggestions from the previous review that are still open: oauth_router.py:1745 (503 detail names the env var), the bare dict return type (add a response model), and the lazy %s log arguments at oauth_router.py:1784.
2e2a295 to
e011a83
Compare
eff6125 to
47be13f
Compare
msureshkumar88
left a comment
There was a problem hiding this comment.
Thanks for the rework. Every item from my previous review is resolved on 47be13fd5:
- The route now requires the new
gateways.token_readpermission, here and intoken_scoping.py. Onlydeveloperandteam_adminreceive it, so viewers can no longer retrieve the token. - The
test_oauth_manager.pyhunk is reverted, andTestIssuerPinningOnAudienceLearningpasses again. - Each token issuance writes an audit entry through
log_action(). The token value never reaches the log. - The database backend serializes refresh per gateway and user, and re-reads the row before refreshing.
report.mdis removed. The Helm values, the PR Notes, the typo and the lazy log arguments are fixed.
Verification: the unit tests for the changed areas pass. A broader RBAC, OAuth and bootstrap run gives 3744 passed and 12 skipped.
One item remains before merge:
blocking | .secrets.baseline | "Run pre-commit hooks" fails because detect-secrets rewrites the baseline. The baseline carries chart entries that main does not have, and the exclude regex repeats ^.secrets.baseline$. | Restore the file from main, run make detect-secrets-scan, and commit the result.
Optional follow-ups: the 503 detail names the env var, the email check after _enforce_gateway_access() is unreachable, the handler Cache-Control header duplicates the middleware, and the route returns a bare dict. Squash on merge to tidy the commit history.
… endpoint Lets a caller fetch their own current OAuth access_token for a gateway, auto-refreshing it when within a configurable threshold (default 10 minutes, max 1 hour) of expiring. The refresh_token is never returned. Disabled by default via OAUTH_TOKEN_RETRIEVAL_ENABLED; gated by the existing team/ownership gateway-access check. Success responses are marked Cache-Control: no-store and issuance is logged (never the token itself) for forensic traceability. Signed-off-by: peter-safwat-ibm <peterzaccha@outlook.com> Signed-off-by: cafalchio <mcafalchio@gmail.com>
Signed-off-by: peter-safwat-ibm <peterzaccha@outlook.com>
Add @require_permission(GATEWAYS_READ) to the handler. Map GET /oauth/token to gateways.read in TokenScopingMiddleware. Add deny-path tests for both layers. Signed-off-by: Jonathan Springer <jps@s390x.com>
Signed-off-by: cafalchio <mcafalchio@gmail.com>
Signed-off-by: cafalchio <mcafalchio@gmail.com>
Signed-off-by: cafalchio <mcafalchio@gmail.com>
c3f076f to
89ab2b5
Compare
✨ Feature / Enhancement PR
🔗 Epic / Issue
Link to the epic or parent issue:
Closes #6966
🚀 Summary (1-2 sentences)
Adds
GET /oauth/token/{gateway_id}, which returns the caller's own current OAuthaccess_tokenfor a gateway (auto-refreshed if within a configurable threshold of expiring) without ever returning therefresh_token.📏 Reviewability
triage🧪 Checks
List exact commands, screenshots, videos, logs, validation steps, or explain why evidence is not feasible.
uv run ruff checkpasses on changed filesuv run banditpasses onmcpgateway/routers/oauth_router.pyuv run pytest tests/unit/mcpgateway/routers/test_oauth_router.pypasses (222 tests)make lint/make test(full suite) not run in this environment — see individual commands above📓 Notes (optional)
Design sketch or extra context.
OAUTH_TOKEN_RETRIEVAL_ENABLED(env, defaultfalse).gateways.token_readand the existing_enforce_gateway_accessvisibility check. The endpoint returns only the caller's stored token.OAUTH_TOKEN_REFRESH_THRESHOLD_SECONDSfrom0to3600seconds (default600). Callers cannot set the threshold.{gateway_id, access_token, token_type, expires_at, scopes}—refresh_tokenis never included.Cache-Control: no-store; issuance is logged (gateway id + user email) but the token value itself is never logged.