oauth: skip OAuth validation when serviceAuths already authenticated request#58
Closed
yjp20 wants to merge 2 commits into
Closed
oauth: skip OAuth validation when serviceAuths already authenticated request#58yjp20 wants to merge 2 commits into
yjp20 wants to merge 2 commits into
Conversation
…request
When a server is configured with both the global OAuth provider and per-server
serviceAuths, the middleware chain is ordered (last-wraps-first via
ChainMiddleware) so ServiceAuth runs before OAuthValidate:
mcpRecover -> ServiceAuth -> OAuthValidate -> ... -> handler
A request with `Authorization: Basic <creds matching serviceAuths>` would
succeed in NewServiceAuthMiddleware (which sets servicecontext on the
context and calls next.ServeHTTP), then immediately be rejected by
NewValidateTokenMiddleware with "Invalid authorization header format"
because the Basic header isn't a Bearer token. Effectively, configuring
serviceAuths.basic on a server with global OAuth made Basic auth
unauthenticatable.
Fix: have NewValidateTokenMiddleware skip OAuth validation when service
auth has already authenticated the request (servicecontext.GetAuthInfo
returns ok). Symmetrically, OAuth-authenticated requests still pass
through ServiceAuth via the existing oauth.GetUserFromContext check at
the top of NewServiceAuthMiddleware.
Adds an integration test (oauth_with_service_auth_test.go) covering both
the bearer-token and basic-auth paths against a server with global OAuth
configured.
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces a bypass in the OAuth validation middleware for requests already authenticated via service-specific credentials and includes integration tests for this behavior. However, the current middleware implementation may cause service authentication to block standard OAuth tokens if it precedes the validator in the chain. It is recommended to adjust the middleware logic and add a test case to validate that standard OAuth functionality is preserved in this configuration.
Verifies that a valid OAuth access token (minted via the full authorization-code flow against the fake Google IDP) still authenticates successfully when the same server has serviceAuths configured. Symmetric to the bearer/basic service-auth cases — ensures ServiceAuth doesn't inadvertently block legitimate OAuth requests.
Contributor
|
Thanks young-jin! If you don't mind I will instead open another PR to fix the general issue of auth composition. That's what I see as the root cause here |
dgellow
added a commit
that referenced
this pull request
Apr 29, 2026
Closes #58. When a server is configured with both global OAuth and per-server `serviceAuths`, the two authenticators previously fought each other: whichever ran first rejected anything it didn't recognize, even when the other one would have accepted it. Configuring service tokens alongside OAuth meant either OAuth users or service callers worked, never both, depending on chain order. The PR that closed the original report fixed one direction; this branch fixes both. Beyond that, service auth requests had no real downstream identity. They surfaced in logs as `user=""`, slipped past per-user session and connection limits, and weren't tracked in storage. To audit or rate-limit them you'd have had nothing to key on. Each `serviceAuths` entry now carries a `name` (required for bearer, optional for basic — defaults to the username) that resolves to a per-server identity. That identity flows through every place that already used the OAuth user's email: logs, session keys, limit pools, audit trails. A bearer token shared across many CI runners shares one identity by design; separate entries get separate pools. A few security items got picked up along the way. Basic auth now runs bcrypt against a dummy hash on no-match, so response timing no longer reveals which usernames exist. Empty bearer tokens and empty basic passwords are rejected at config load — closes the case where a misconfigured `{"$env":"VAR"}` would silently authenticate `Authorization: Bearer ` (no token). The identity domain used for service auth is reserved (RFC 9476's `.alt`); the OAuth flow refuses any IDP-claimed email in that domain, so a real user can't impersonate a service identity. Existing configs with bearer entries need a one-line addition (`"name": "..."`) — there's no sensible default for bearer since the array of tokens has no admin-visible label.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When an MCP server is configured with both the global OAuth provider (
proxy.auth) and per-serverserviceAuths, the middleware chain ininternal/mcpfront.go:404-419is built like this:ChainMiddlewarewraps last-wraps-first, so the actual execution order is:A request with
Authorization: Basic <creds matching serviceAuths>flows like this:NewServiceAuthMiddlewarematches the credentials, setsservicecontext.WithAuthInfo, callsnext.ServeHTTP(...).NewValidateTokenMiddlewareruns next. It looks at theAuthorizationheader, seesBasicinstead ofBearer, and returns 401 withInvalid authorization header formatand an RFC 9728WWW-Authenticate: Bearerchallenge — without consulting the service-auth context that ServiceAuth just set.Net effect: configuring
serviceAuths.basicon a server with global OAuth made Basic auth unauthenticatable, even when the credentials are correct. Same issue applies toserviceAuths.bearertokens that aren't valid OAuth access tokens.integration/basic_auth_test.godoesn't catch this because it passesnilfor the auth config, soauthServer == niland OAuth middleware is never installed.Fix
In
NewValidateTokenMiddleware, short-circuit when service auth has already authenticated the request:Symmetrically, OAuth-authenticated requests already pass through
NewServiceAuthMiddlewarecleanly via the existingoauth.GetUserFromContextcheck at the top of that middleware.