Skip to content

feat(gateway): MCP API key + OAuth 2.1 auth [b51i+rcvm]#271

Open
atemate wants to merge 6 commits intomainfrom
agentic-security/b51i.phase-2-mcp-api-key-authentication
Open

feat(gateway): MCP API key + OAuth 2.1 auth [b51i+rcvm]#271
atemate wants to merge 6 commits intomainfrom
agentic-security/b51i.phase-2-mcp-api-key-authentication

Conversation

@atemate
Copy link
Collaborator

@atemate atemate commented Mar 6, 2026

Summary

Implements MCP authentication in two phases, combined in one PR per aint tasks b51i (Phase 2) and rcvm (Phase 3).

Phase 2 — MCP API Key (ASYA_MCP_API_KEY)

  • New BearerTokenAuthenticator: validates Authorization: Bearer <static-token> with constant-time comparison
  • New MCPAuthMiddleware: wraps /mcp, /mcp/sse, /tools/call — disabled (no-op) when unconfigured, returns 401 + WWW-Authenticate: Bearer on failure (RFC 6750)
  • Applied in api and testing gateway modes

Phase 3 — MCP OAuth 2.1 + PKCE (ASYA_MCP_OAUTH_ENABLED)

  • New OAuthBearerAuthenticator: validates self-issued HMAC-SHA256 JWTs via golang-jwt/v5
  • New internal/oauth package implementing the full authorization server:
    • GET /.well-known/oauth-protected-resource (RFC 9728 discovery)
    • GET /.well-known/oauth-authorization-server (RFC 8414 metadata)
    • POST /oauth/register (RFC 7591 Dynamic Client Registration)
    • GET /oauth/authorize (Authorization Code flow, auto-approve for machine clients)
    • POST /oauth/token (code exchange + refresh token rotation with PKCE S256)
  • DB migration 010_oauth_tables adds oauth_clients, oauth_authorization_codes, oauth_refresh_tokens
  • Env vars: ASYA_MCP_OAUTH_ISSUER, ASYA_MCP_OAUTH_SECRET, ASYA_MCP_OAUTH_TOKEN_TTL

Both phases use the existing Authenticator interface with OR semantics — API key and OAuth Bearer tokens accepted simultaneously.

Architecture Notes

  • MCPAuthMiddleware(authenticators...) returns a no-op middleware when zero authenticators are passed (dev mode, no key configured)
  • buildRoutes now accepts mcpMiddleware and oauthSrv parameters; nil-safe (identity middleware when nil)
  • OAuth endpoints are always public (called before auth is established per OAuth spec)
  • Refresh tokens are opaque, hashed with SHA-256 before DB storage; access tokens are short-lived JWTs

Test Plan

  • All existing unit tests pass (make test-unit)
  • New unit tests: BearerTokenAuthenticator (valid/invalid/missing/wrong-scheme), OAuthBearerAuthenticator (valid/expired/wrong-alg), MCPAuthMiddleware (enabled/disabled/valid/invalid/missing), PKCE S256 (valid/invalid/unsupported-method)
  • All linters pass (make lint)

Phase 2 — MCP API key (ASYA_MCP_API_KEY):
- Add BearerTokenAuthenticator (Authorization: Bearer <static-token>)
- Add MCPAuthMiddleware: wraps /mcp, /mcp/sse, /tools/call
  - Disabled (no-op) when no authenticators configured (dev mode)
  - Returns 401 + WWW-Authenticate: Bearer on failure (RFC 6750)
- Wire ASYA_MCP_API_KEY in main.go

Phase 3 — MCP OAuth 2.1 + PKCE (ASYA_MCP_OAUTH_ENABLED):
- Add OAuthBearerAuthenticator: validates gateway-issued HMAC-SHA256 JWTs
- New internal/oauth package:
  - /.well-known/oauth-protected-resource (RFC 9728)
  - /.well-known/oauth-authorization-server (RFC 8414)
  - POST /oauth/register (RFC 7591 Dynamic Client Registration)
  - GET /oauth/authorize (Authorization Code + auto-approve)
  - POST /oauth/token (code exchange + refresh token rotation)
  - PKCE S256 verification
- DB migration 010_oauth_tables (oauth_clients, oauth_authorization_codes,
  oauth_refresh_tokens)
- Wire ASYA_MCP_OAUTH_ISSUER, ASYA_MCP_OAUTH_SECRET, ASYA_MCP_OAUTH_TOKEN_TTL

Both phases use the existing Authenticator interface with OR semantics:
API key and OAuth tokens are accepted in parallel.
@github-actions github-actions bot added asya-gateway feat New feature implementation test labels Mar 6, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces API key and OAuth 2.1 authentication for MCP endpoints, a significant feature that includes dynamic client registration, authorization code flow with PKCE, and refresh token rotation. However, several critical security vulnerabilities have been identified. These include predictable token generation due to ignored errors in the randomness source, two race conditions (TOCTOU) in the token and code exchange logic that could allow for token reuse, authentication bypass enabled by open dynamic client registration, and a 'fail open' behavior in the MCP authentication middleware. Additionally, issues such as lack of scope validation, potential for memory-based DoS in the registration endpoint, and recommendations for database schema integrity and context propagation need to be addressed. These issues must be resolved before merging.

…rcvm]

Critical fixes:
- TOCTOU auth code: replace findAuthCode+markAuthCodeUsed with atomic
  consumeAuthCode (UPDATE ... RETURNING) — concurrent token requests with
  the same code can no longer both succeed
- TOCTOU refresh token: replace findRefreshToken+revokeRefreshToken with
  atomic consumeRefreshToken (UPDATE ... RETURNING) — token rotation is safe
- rand.Read error: panic explicitly on entropy source failure (was silently
  ignored, could produce predictable tokens on low-entropy systems)

Security improvements:
- /oauth/register: add optional ASYA_MCP_OAUTH_REGISTRATION_TOKEN guard;
  when set, Bearer token required to register new clients
- /oauth/authorize: validate requested scopes against client's registered
  scopes (intersect), reject if no valid scopes remain
- /oauth/register: add http.MaxBytesReader(1MiB) to prevent DoS via
  large JSON payloads
- DB: add ON DELETE CASCADE to FK constraints in oauth_authorization_codes
  and oauth_refresh_tokens — client deletion cleans up child rows atomically

Code quality:
- issueTokenResponse: propagate request context instead of context.Background()
- HandleAuthorize: handle url.Parse error for registered redirect URIs; add
  comment explaining SSRF mitigation (allowlist validation before parse)
- MCPAuthMiddleware: clarify intent of fail-open behavior with comment
  (intentional dev-mode design, not a security gap when ASYA_MCP_API_KEY
  is unset and the deployment is network-restricted)
@atemate
Copy link
Collaborator Author

atemate commented Mar 6, 2026

@gemini-code-assist review

Open redirect (Codacy CWE-601):
- HandleAuthorize now looks up the canonical redirect URI from the
  DB-sourced client.RedirectURIs list via findRegisteredURI() and uses
  that value (not raw r.URL.Query() input) for url.Parse + http.Redirect
- Breaks the user-input taint chain: redirect target provenance is DB,
  not request query string

Code clone:
- Extract extractBearerToken(r) helper that parses Authorization: Bearer
  header — was duplicated across JWTAuthenticator, BearerTokenAuthenticator,
  and OAuthBearerAuthenticator
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive authentication layer for MCP endpoints, supporting both static API keys and a full OAuth 2.1 flow, including a full OAuth server in the internal/oauth package. However, the OAuth server allows open client registration with auto-approval if a registration token is not explicitly set, which should be changed to a 'fail-secure' default to ensure the gateway remains protected even in the event of configuration errors. Additionally, a high-severity security recommendation regarding the comparison of the registration token to prevent potential timing attacks has been identified.

Comment on lines +118 to +126
if s.cfg.RegistrationToken != "" {
authHeader := r.Header.Get("Authorization")
provided := strings.TrimPrefix(authHeader, "Bearer ")
if !strings.HasPrefix(authHeader, "Bearer ") || provided != s.cfg.RegistrationToken {
w.Header().Set("WWW-Authenticate", `Bearer realm="asya-gateway"`)
writeOAuthError(w, http.StatusUnauthorized, "unauthorized_client", "registration token required")
return
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

The OAuth 2.1 implementation allows for open dynamic client registration if ASYA_MCP_OAUTH_REGISTRATION_TOKEN is not set. This can lead to unauthenticated attackers obtaining valid OAuth access tokens with mcp:invoke and mcp:read scopes, effectively bypassing intended security controls. Furthermore, the comparison of the registration token is not performed in constant time, which could expose the endpoint to timing attacks. To remediate, require a registration token by default, making open registration an explicit opt-in. Also, consider if auto-approval is appropriate for all dynamically registered clients. For the token comparison, use crypto/subtle.ConstantTimeCompare for a secure, constant-time comparison, consistent with other authenticators in the codebase. Remember to add import "crypto/subtle".

Suggested change
if s.cfg.RegistrationToken != "" {
authHeader := r.Header.Get("Authorization")
provided := strings.TrimPrefix(authHeader, "Bearer ")
if !strings.HasPrefix(authHeader, "Bearer ") || provided != s.cfg.RegistrationToken {
w.Header().Set("WWW-Authenticate", `Bearer realm="asya-gateway"`)
writeOAuthError(w, http.StatusUnauthorized, "unauthorized_client", "registration token required")
return
}
}
if !strings.HasPrefix(authHeader, "Bearer ") || subtle.ConstantTimeCompare([]byte(provided), []byte(s.cfg.RegistrationToken)) != 1 {
References
  1. New API endpoints that allow modification of system configuration, such as tool registration, must be protected by authentication and authorization middleware.

atemate added 2 commits March 6, 2026 22:55
- Add test_mcp_auth.py covering 401/200 behavior when ASYA_MCP_API_KEY
  is configured: missing token, wrong token, wrong scheme, correct token,
  RFC 6750 WWW-Authenticate header, public endpoints unaffected
- Add docker-compose.auth.yml that starts gateway with ASYA_MCP_API_KEY
  set and passes MCP_API_KEY to tester container
- Add test-auth Makefile target in gateway-mcp suite
- Wire gateway-mcp suite (both test and test-auth) into testing/component/Makefile
The redirect target (u.String()) comes from client.RedirectURIs in the
database — validated by findRegisteredURI() before use. Gosec's taint
engine still flags the call because r (*http.Request) is marked tainted
from the earlier r.URL.Query() reads in the same function, even though r
is only used for HTTP protocol detection in http.Redirect, not as the URL.

Add #nosec G601 annotation with explanation.
params.Set("state", state)
}
u.RawQuery = params.Encode()
http.Redirect(w, r, u.String(), http.StatusFound) // #nosec G601 -- redirect target is client.RedirectURIs[i] (DB-sourced), not raw user input

Choose a reason for hiding this comment

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

Important

Severity: LOW
CWE: ['CWE-918: Server-Side Request Forgery (SSRF)']
📟 Finding Description: A request was found to be crafted from user-input r. This can lead to Server-Side Request Forgery (SSRF) vulnerabilities, potentially exposing sensitive data. It is recommend where possible to not allow user-input to craft the base request, but to be treated as part of the path or query parameter. When user-input is necessary to craft the request, it is recommended to follow OWASP best practices to prevent abuse, including using an allowlist.

🤖 AI Analysis

Exploitability: It is not likely to be exploitable.
Comment: This issue looks unexploitable because user-controlled input is not present in the vulnerable code section. The code properly handles the configuration and only utilizes server-side values such as 'cfg.Issuer' to build URL components. There doesn't appear to be any direct manipulation of URL components based on externally supplied input.
Example Fix Snippet:

No Code Sample

Explaination of the Fix: ---

🔍Leveraging GPT-4 for a preliminary analysis of the Semgrep results. Take note that false positives are possible. A manual code review is required for confirmation.

Note

We understand that not every detected issue may be relevant or pose a security risk. If this issue doesn't apply to your situation, such as if the resource in question is local or it is only test code such as unittest, you have the option to ignore it.

You can find all the rules under the repository, https://github.com/deliveryhero/sast_rules/ and contribute with a pull request. If think the rule of the issue is requires improvement, please do not hesitate to create a pull request.

…arning

http.Redirect(w, r, url, code) uses r only to resolve relative URLs.
Since registeredURI is always absolute (DB-stored full URI), set the
Location header directly and call w.WriteHeader instead — r never
appears in the redirect code path, closing the taint chain for both
Gosec and Opengrep without needing #nosec annotations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

asya-gateway feat New feature implementation test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants