Skip to content

fix: stop OAuth2Extractor from letting a non-Bearer header shadow access_token#512

Open
ultramcu wants to merge 1 commit into
golang-jwt:mainfrom
ultramcu:fix/oauth2-non-bearer-fallthrough
Open

fix: stop OAuth2Extractor from letting a non-Bearer header shadow access_token#512
ultramcu wants to merge 1 commit into
golang-jwt:mainfrom
ultramcu:fix/oauth2-non-bearer-fallthrough

Conversation

@ultramcu
Copy link
Copy Markdown

Problem

OAuth2Extractor is documented as "looks in 'Authorization' header then 'access_token' argument for a token". In practice, a present but non-Bearer Authorization header silently shadows a valid access_token:

Authorization: Basic dXNlcjpwYXNzd29yZA==   +   ?access_token=<valid JWT>
  -> OAuth2Extractor returns "Basic dXNlcjpwYXNzd29yZA==" (the access_token is never read)

The cause is the composition:

var OAuth2Extractor = &MultiExtractor{
    AuthorizationHeaderExtractor, // returns ANY non-empty Authorization value
    ArgumentExtractor{"access_token"},
}

AuthorizationHeaderExtractor's stripBearerPrefixFromTokenString returns the header value unchanged when there is no "Bearer " prefix, and MultiExtractor stops at the first non-empty token, so the access_token fallback only runs when the Authorization header is entirely absent. A request that carries both HTTP Basic auth (e.g. a browser re-sending cached credentials, or a proxy) and a valid ?access_token=<jwt> therefore fails to authenticate, surfacing a confusing token is malformed: token contains an invalid number of segments error.

Fix

Use BearerExtractor{} as the first sub-extractor. It already returns ErrNoTokenInRequest for a non-Bearer header, so MultiExtractor correctly falls through to access_token:

var OAuth2Extractor = &MultiExtractor{
    BearerExtractor{},
    ArgumentExtractor{"access_token"},
}

Behavior change

A token placed directly in the Authorization header without the Bearer scheme prefix is no longer extracted by OAuth2Extractor (it previously was, as a side effect of the lenient strip). This matches RFC 6750, which mandates the Bearer scheme for OAuth2 access tokens. A real Bearer <token> header is unaffected, and AuthorizationHeaderExtractor itself is unchanged for anyone using it directly.

Tests

Adds TestOAuth2Extractor covering: Bearer header, Bearer precedence over access_token, access_token fallback with no header, and the regression cases — a non-Bearer header (Basic ..., Token ...) falling through to access_token, and a non-Bearer header with no access_token returning ErrNoTokenInRequest. The fallthrough cases fail before this change and pass after.

go test -race ./..., go vet ./... and gofmt are clean; no existing test changed.

…ess_token

OAuth2Extractor used AuthorizationHeaderExtractor, which returns any non-empty
Authorization header value (its "Bearer " prefix strip is a no-op for other
schemes). Because MultiExtractor stops at the first non-empty token, a present
but non-Bearer header (e.g. "Basic ...") was returned as the token and the
documented 'access_token' fallback only ran when the Authorization header was
entirely absent. A request carrying both HTTP Basic auth and a valid
?access_token=<jwt> therefore failed to authenticate, with a confusing
"invalid number of segments" error.

Use BearerExtractor as the first sub-extractor: it returns ErrNoTokenInRequest
for a non-Bearer header, so MultiExtractor correctly falls through to
access_token. Adds TestOAuth2Extractor covering precedence and fallthrough.
@ultramcu ultramcu requested review from mfridman and oxisto as code owners May 24, 2026 10:03
Comment thread request/oauth2.go
// that it does not shadow a valid 'access_token' argument.
var OAuth2Extractor = &MultiExtractor{
AuthorizationHeaderExtractor,
BearerExtractor{},
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.

where is BearerExtractor defined?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

BearerExtractor is the existing exported type in this package — request/extractor.go:84 (type BearerExtractor struct{}, with an ExtractToken method). I reused it here precisely because it returns ErrNoTokenInRequest when the Authorization header isn't Bearer …, so MultiExtractor falls through to the access_token form value instead of being shadowed by a non-Bearer header (e.g. Basic …). The previous code used stripBearerPrefixFromTokenString, which returns the header unchanged for a non-bearer value, which is what stopped the fallthrough.

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