Describe the bug
I use ping as oidc provider. When I try to do a token exchange from springboot application, I got a "[invalid_request]" from ping.
When I drill down a little bit, I end up on the field value of the subject_token_type:
https://github.com/spring-projects/spring-security/blob/7030a62c76b0a0588ac5b99d605bbeabb8d4d783/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/TokenExchangeGrantRequest.java#L114
basically, I got a jwt access token, this class make the subject_token_type to "jwt" format and ping won't accept this value and wait for the access token type ( urn:ietf:params:oauth:token-type:access_token )
The RFC do not specify you must use a "jwt" type in the case of a jwt access token, and it specify that "access_token" type is OK even if your access token is jwt. ( https://datatracker.ietf.org/doc/html/rfc8693#name-token-type-identifiers ) so I guess the certification tool do not check that, and not supporting the 'jwt' type is fine at this level.
To Reproduce
Need to use a ping server to bootstrap the token exchange connection and use it.
Expected behavior
Is there a way to "force" the use of access_token type instead of jwt ?
Sample
-
Comment From: sjohnr
@GregoireW thanks for reaching out!
Is there a way to "force" the use of access_token type instead of jwt ?
Generally, we prefer questions be asked on stack overflow. However, given that this question could require discussion that might require opening an issue, I'll respond directly here.
Yes, it is possible to customize the OAuth 2.0 Access Token Request for the token-exchange
grant (RFC 8693). The Token Exchange Grant is discussed in the reference docs, specifically Customizing Request Parameters for Token Exchange.
One way to do this would be to override parameters with setParametersConverter
, like so:
@Bean
public OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> accessTokenResponseClient() {
RestClientTokenExchangeTokenResponseClient accessTokenResponseClient =
new RestClientTokenExchangeTokenResponseClient();
accessTokenResponseClient.setParametersConverter(grantRequest -> {
LinkedMultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames.SUBJECT_TOKEN_TYPE, "urn:ietf:params:oauth:token-type:access_token");
return parameters;
});
return accessTokenResponseClient;
}
Note that simply publishing this component as a bean is typically enough to plug it into the framework. Please review the OAuth2 overview page and authorization grants page in detail as all of this information is available there.
I'm going to close this issue as answered. However, if you have any additional thoughts or feedback on whether this customization meets your needs or the documentation around it, please add an additional comment and we can keep discussing. My hope is that this information is easy to find and understand and that the framework provides the flexibility you need, but we value any feedback you have on it.
Comment From: GregoireW
Ah! thank you!
I was looking on the default converter & co and face ".addAll" methods (DefaultTokenXxx class). Did not go down the rabbit hole to the documentation nor on the request.
Moving to the RestClientTokenExchangeTokenResponseClient did the trick.
I did follow a documentation that was explicitly referencing the DefaultTokenxxx class, so I was not aware there was a different implementation and a way to update parameters.
I guess when the default will not be the default anymore it will be easier to find the documentation.
As of today, the bean declaration is needed so there is no need to change anything. If there is a change to create this bean automatically from the registration then it would be nice to have a way to declare such converter either via a bean or via configuration, but again, as today we have to declare it, I guess it is not a big deal.
Thank you
Comment From: sjohnr
I did follow a documentation that was explicitly referencing the DefaultTokenxxx class, so I was not aware there was a different implementation and a way to update parameters.
Can you share a link to the documentation you were reading? Was it an older version of the docs?
I guess when the default will not be the default anymore it will be easier to find the documentation.
Also, please note that the javadoc and source code for DefaultTokenExchangeTokenResponseClient
refer to RestClientTokenExchangeTokenResponseClient
in the deprecation notice.
As of today, the bean declaration is needed so there is no need to change anything. If there is a change to create this bean automatically from the registration then it would be nice to have a way to declare such converter either via a bean or via configuration, but again, as today we have to declare it, I guess it is not a big deal.
Sorry, I'm not quite following yet. Regarding "it would be nice to have a way to declare such converter either via a bean or via configuration," can you clarify with an example what you think would be an improvement?
Comment From: GregoireW
Can you share a link to the documentation you were reading? Was it an older version of the docs?
I guess it was an early 6.3 version. The documentation point to a "addAll" method.
Regarding the configuration
Again, as it is today it is not a big deal.
Now, let say you have a configuration:
spring:
security:
oauth2:
client:
registration:
exchange:
provider: xxx
client-id: ${CLIENT_ID:1234}
client-secret: ${CLIENT_SECRET:1234}
authorization-grant-type: urn:ietf:params:oauth:grant-type:token-exchange
client-authentication-method: client_secret_basic
scope:
- openid
- profile
Hypothetically Spring Security one day will define that this declaration is enough to create a new RestClientTokenExchangeTokenResponseClient
In such case, I would expect we could define kind of TokenExchangeParameterConverter
that would automatically be added to the RestClientTokenExchangeTokenResponseClient
.
Another possibility would be to be able to add parameters directly in the properties like:
spring:
security:
oauth2:
client:
registration:
exchange:
provider: xxx
client-id: ${CLIENT_ID:1234}
client-secret: ${CLIENT_SECRET:1234}
authorization-grant-type: urn:ietf:params:oauth:grant-type:token-exchange
client-authentication-method: client_secret_basic
scope:
- openid
- profile
additional-parameters:
xxxx: yyyy
Again it is just an hypothetical
Comment From: sjohnr
Hypothetically Spring Security one day will define that this declaration is enough to create a new
RestClientTokenExchangeTokenResponseClient
Thanks, that helps me understand what you're thinking about.
Another possibility would be to be able to add parameters directly in the properties
This idea (or something like it) has come up more than once recently, and could be worth pursuing. It has also come up before (for example, gh-10452) but was rejecting since it was extremely limited to static properties only. If applied only to adding request parameters for token requests, it does not add enough flexibility. However, I think adding a way to somehow associate additional parameters with the ClientRegistration
could be beneficial for multiple use cases. I will discuss with the team and may open an issue after discussion.
Comment From: GregoireW
This idea (or something like it) has come up more than once recently, and could be worth pursuing. It has also come up before (for example, gh-10452) but was rejecting since it was extremely limited to static properties only. If applied only to adding request parameters for token requests, it does not add enough flexibility. However, I think adding a way to somehow associate additional parameters with the
ClientRegistration
could be beneficial for multiple use cases. I will discuss with the team and may open an issue after discussion.
The issue with this approach is "to add or not to add". My guess would be to create 2 parameters "add-properties", "set-properties" but ... what does it means ? for token exchange is pretty simple you have only 1 call to the token endpoint. For code flow, does it make sense to add parameter on authentication endpoint or token endpoint ?
So... not great... ok for my usecase it would be easier, but I totally understand it would not be implemented this way.