Describe the bug Both the SpringOpaqueTokenIntrospector and NimbusOpaqueTokenIntrospector use the clientId and clientSecret to authenticate the calls to the authorization server.

This is done via basic authentication added using a BasicAuthenticationInterceptor. This does not perform any URL encoding.

This issue was addressed in https://github.com/spring-projects/spring-security/issues/9610 for the token granting client, but persists for the introspection client.

The workaround at the moment is to manually encode the secret when instantiating the introspector.

To Reproduce 1. Set up a Spring Authorization Server with a client with a secret such as badSecret% 1. Configure a SpringOpaqueTokenIntrospector or NimbusOpaqueTokenIntrospector to use that client 2. Attempt to use the introspector with the Spring Authorization Server. 3. See the server respond with a 400 invalid_request error and see the following cause in the logs:

Caused by: java.lang.IllegalArgumentException: URLDecoder: Incomplete trailing escape (%) pattern
        at java.base/java.net.URLDecoder.decode(URLDecoder.java:230) ~[?:?]
        at java.base/java.net.URLDecoder.decode(URLDecoder.java:147) ~[?:?]
        at org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretBasicAuthenticationConverter.convert(ClientSecretBasicAuthenticationConverter.java:85) ~[spring-security-oauth2-authorization-server-1.3.2.jar!/:1.3.2]
        ... 103 more

Expected behavior The token introspector should URL encode the secret.

Comment From: jzheaux

Thanks for this report, @joelossher. I agree that this should be taken care of.

My primary concern is that there are applications like yours that are self-encoding already (like you are). To start encoding by default at this point would break those applications.

Because there is a constructor that accepts a RestOperations instance, there is already a workaround, should the fix cause any issues; we just don't want to break folks in a point release.

For that reason, I'll schedule this fix for 6.5.

Comment From: jzheaux

Are you able to provide a PR to add the encoding and a test?

Comment From: ngocnhan-tran1996

@jzheaux

SpringOpaqueTokenIntrospector use BasicAuthenticationInterceptor for encoding clientId and clientSecret. So clientSecret will be encoded before calling BasicAuthenticationInterceptor. Please correct me if I am wrong

Comment From: jzheaux

@ngocnhan-tran1996, my understanding is that the OAuth 2.0 spec indicates that the client id and secret should be URL encoded before performing the Basic Auth encoding of username and password together (pseudocode follows):

header = base64(urlencode(username) + ':' + urlencode(password))

So this ticket would make it so that the values are URL encoded before formulating the Authorization: Basic header.

Does that clarify, or am I misunderstanding your question?

Comment From: ngocnhan-tran1996

@jzheaux

Let me make more clearly Currently, SpringOpaqueTokenIntrospector use BasicAuthenticationInterceptor for encoding clientId and clientSecret

https://github.com/spring-projects/spring-security/blob/9d2ca3da6a285f31ebd2da5f019127e1527e5042/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java#L88

And deep into this, it use HttpHeaders#encodeBasicAuth (belongs to Spring Framework) like your pseudocode

String credentialsString = username + ":" + password;
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(charset));
return new String(encodedBytes, charset);

So if we want to encode password in side SpringOpaqueTokenIntrospector, we will do something like

restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(urlencode(clientId), urlencode(clientSecret)));

This part confuses me, we need to encode from Spring Framework side or Spring Security side if somewhere in Spring Security also use HttpHeaders#encodeBasicAuth

Comment From: jzheaux

The encoding that Spring Framework provides is Base64 encoding. The OAuth spec indicates that the values should be URL encoded and then also thereafter Base64 encoded.

If you look carefully at the Framework code, you'll see that it is doing:

header = base64(username + ':' + password)

which is different from

header = base64(urlencode(username) + ':' + urlencode(password))

Comment From: ngocnhan-tran1996

Thank for your explantion, can I work on this?

Comment From: jzheaux

Sure! Note that it is targeted for 6.5, so the PR wouldn't merge for a couple of months.