Skip to content

Ignore Content-Type parameters when resolving bearer tokens - #19499

Open
lArtiquel wants to merge 1 commit into
spring-projects:mainfrom
lArtiquel:bearer-token-form-encoded-content-type
Open

Ignore Content-Type parameters when resolving bearer tokens#19499
lArtiquel wants to merge 1 commit into
spring-projects:mainfrom
lArtiquel:bearer-token-form-encoded-content-type

Conversation

@lArtiquel

Copy link
Copy Markdown

Both bearer token resolvers decide whether a request is form-encoded by comparing the Content-Type for exact equality:

// DefaultBearerTokenResolver
!MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())

// ServerBearerTokenAuthenticationConverter
!MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())

A media type parameter defeats both comparisons. MediaType#equals includes parameters, so application/x-www-form-urlencoded;charset=UTF-8 is not equal to APPLICATION_FORM_URLENCODED. That header is legal (RFC 9110 §8.3) and is what several HTTP clients send by default for form bodies. The servlet resolver is additionally case-sensitive, though media types are not (RFC 9110 §8.3.1).

The result is a silent failure: with setAllowFormEncodedBodyParameter(true), the access_token body parameter is never read, and the request is rejected as unauthenticated with no indication that the token was present.

Reproduce (fails on main before this change):

DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
resolver.setAllowFormEncodedBodyParameter(true);

MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.setContentType("application/x-www-form-urlencoded;charset=UTF-8");
request.addParameter("access_token", "token");

assertThat(resolver.resolve(request)).isEqualTo("token"); // actual: null

Fix

Both now parse the Content-Type and compare with MimeType#equalsTypeAndSubtype, which is case-insensitive and ignores parameters. Media types that are not form-encoded are still rejected, including */*equalsTypeAndSubtype is used rather than isCompatibleWith precisely so that wildcards do not match. In the servlet resolver, an unparseable Content-Type is treated as not form-encoded rather than propagating InvalidMediaTypeException; the reactive converter's behaviour on a malformed header is unchanged.

Tests cover the charset parameter, upper-case media types, */*, and a malformed Content-Type. The reactive converter already normalizes case via HttpHeaders#getContentType, so its upper-case test passes before and after; it is included to pin the behaviour alongside the servlet one.

I did not find an existing issue for this. Happy to open one if you'd prefer the tracking record, and equally happy to target a maintenance branch instead — the servlet resolver has behaved this way since 5.1 and the reactive converter's form-body support since 6.5.

Related, not included: ServerOneTimeTokenAuthenticationConverter#isFormEncodedRequest compares the raw Content-Type header string the same way and has the same blind spot. I left it out to keep this change to one module and one feature, since fixing it there means routing through HttpHeaders#getContentType, which throws on a malformed header where the current code does not. Glad to follow up separately if you want it.

Verification

./gradlew :spring-security-oauth2-resource-server:test :spring-security-oauth2-resource-server:checkFormat
./gradlew :spring-security-config:test --tests "*OAuth2ResourceServer*"

Both pass.

DefaultBearerTokenResolver and ServerBearerTokenAuthenticationConverter
compared the request Content-Type against
application/x-www-form-urlencoded for exact equality. A request that
carries a media type parameter, such as

    Content-Type: application/x-www-form-urlencoded;charset=UTF-8

therefore did not qualify as form-encoded, and the access_token body
parameter was silently ignored even with
setAllowFormEncodedBodyParameter(true). The servlet resolver was
additionally case-sensitive, so an upper-case media type was rejected
as well.

Both now parse the Content-Type and compare only its type and subtype,
which is case-insensitive and ignores parameters. Wildcard and
malformed media types continue to be rejected.

Signed-off-by: Artem Tsvirko <tsvirkoartem@gmail.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants