Expected Behavior
I am using Spring security 5.3.3.RELEASE and I have a use case where am using a B2B (service to service communication) via client credentials. My OAuth2 server gives me access_token and (optionally if configured a JWT token in response with additional attribute id_token) and in few cases I want to use access_token and some cases I might use JWT token received from id_token attribute.
Currently OAuth2AuthorizedClientManager.authorize() returns OAuth2AuthorizedClient which only expose methods to return access_token and refresh_token but it doesn't allow any other attributes to be returned.
For my B2B client applications (using openfeign client interceptor) I am using AuthorizedClientServiceOAuth2AuthorizedClientManager which internally uses ClientCredentialsOAuth2AuthorizedClientProvider for client_credentials and interestingly DefaultClientCredentialsTokenResponseClient retrieves all attributes including all standard attributes with extra non standard attributes captured in a Map<String, Object> but while returning OAuth2AuthorizedClient it sets only access token and not the additional attributes.
I understand this as far as standard it's correctly implemented but having this additional Map exposed also won't break any of the extensibility as well as allows additional attributes to be accessed by API for some custom oauth service providers.
Alternatively to support my usecase i.e.to access both access_token and jwt token with current approach I can write 2 separate AuthorizedClientServiceOAuth2AuthorizedClientManager and ensure getAccessToken() returns jwt token by processing via DefaultClientCredentialsTokenResponseClient with a custom OAuth2AccessTokenResponseHttpMessageConverter which can ultimately return id_token my JWT token to treat it as a access token. But this will be kind of faking it and hacking true intention of these APIs, also this would be too much of custom code for me.
I was wondering the APIs can be enhanced to return additional Map via OAuth2AuthorizedClient it would allow applications like mine to leverage some of the custom response attributes effectively without much of customization.
Comment From: jgrandja
@AjitDas
I have a use case where am using a B2B (service to service communication) via client credentials. My OAuth2 server gives me access_token and (optionally if configured a JWT token in response with additional attribute
id_token)
So the Token Response includes the id_token from a client_credentials grant flow? Is my understanding correct?
Comment From: AjitDas
That's correct Joe. It will be something like this captured below it will contain both opaque token as well as jwt token and based on some configuration I want to send opaque token vs jwt token to different outbound calls.
Take for example I have micro service1 which call 2 other micro services, svc1 uses client_credentials and get both the tokens in response but it send opaque token to svc2 where as send jwt token to svc3. This is because we have multiple development groups in our org who owns and manages these micro services and each group have their own release cycles.
microsvs-1 --> microsvs-2 microsvc-1 --> microsvs-3
GET https://my-authorization.svc/token?client_id=****&client_secret=***&grant_type=client_credentials&scope=RETURN_ALL_CLIENT_SCOPES
{
"access_token": "f257d******", // my opaque token
"token_type": "BEARER",
"id_token": "eyJraW*****", // my jwt token
"scope": "scope_1, scope_2",
"expires_in": "3600"
}
Comment From: jgrandja
Is the id_token parameter the same as the ID Token defined in OpenID Connect 1.0? If yes, then this is confusing since client_credentials flow is not implemented in OpenID Connect 1.0 given that client_credentials does NOT represent "on behalf of a user".
Comment From: AjitDas
@jgrandja yes I know this could be confusing a little. Our auth server returns access_token and additional field id_token as jwt just to avoid doing a network hop to auth server when a microservice receives a jwt token instead of access token. I am aware this is not a standard client_credentials response but a custom response attribute since our internal microservice calls are within network and we want to avoid network hop in every passthough services just to validate the token. We will get all the info from jwt token what a validate call to auth sever would have returned.
I know it can be said if jwt is what need why don't just return access_token with jwt token instead of opaque token without this additional id_token. But as I mentioned earlier we are in transition phase where different services uses different token, ultimately everyone will use jwt token and our access_token can return jwt token, but till then we need to have both the tokens returned in response.
Having said that am fully aware what spring provides already is a standard solution as mentioned above in my previous comments. I was looking a way to access the additional attributes that are already accessed via DefaultClientCredentialsTokenResponseClient as Map, but since this is not exposed OAuth2AuthorizedClientManager as an accessor I have to do some custom processing or have 2 different AuthorizedClientServiceOAuth2AuthorizedClientManager one with default and another with custom reading and setting returned object with id_token value. I will highly appreciate If you have any suggestion to do it anyway which could be easier and efficient.
Thank you for time !
Comment From: jgrandja
@AjitDas You can still leverage AuthorizedClientServiceOAuth2AuthorizedClientManager but you will need to supply a custom OAuth2AuthorizedClientProvider. Below is a custom configuration that will work for you:
@Configuration
public class WebClientConfig {
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.provider(clientCredentialsAuthorizedClientProvider())
.build();
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
private OAuth2AuthorizedClientProvider clientCredentialsAuthorizedClientProvider() {
final OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
return context -> {
ClientRegistration clientRegistration = context.getClientRegistration();
if (!AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType())) {
return null;
}
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest =
new OAuth2ClientCredentialsGrantRequest(clientRegistration);
OAuth2AccessTokenResponse tokenResponse;
try {
tokenResponse = accessTokenResponseClient.getTokenResponse(clientCredentialsGrantRequest);
} catch (OAuth2AuthorizationException ex) {
throw new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex);
}
return new CustomOAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken(), tokenResponse.getAdditionalParameters());
};
}
public class CustomOAuth2AuthorizedClient extends OAuth2AuthorizedClient {
private Map<String, Object> additionalParameters;
public CustomOAuth2AuthorizedClient(ClientRegistration clientRegistration, String principalName,
OAuth2AccessToken accessToken, Map<String, Object> additionalParameters) {
super(clientRegistration, principalName, accessToken);
this.additionalParameters = additionalParameters;
}
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
}
}
NOTE: The lambda returned from clientCredentialsAuthorizedClientProvider() is a simplified version of ClientCredentialsOAuth2AuthorizedClientProvider. You might want to account for token expiry as well.
I'm going to close this issue as the custom configuration provided will work for your use case.
Comment From: NotFound403
OAuth2 Client Mode
when the token creating :
OAuth2AuthorizationCodeAuthenticationToken authenticationResult = new OAuth2AuthorizationCodeAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(), accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken(), accessTokenResponse.getAdditionalParameters());
when the token saving:
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
authenticationResult.getClientRegistration(), principalName, authenticationResult.getAccessToken(),
authenticationResult.getRefreshToken());
additionalParameters is not exposed ,and its unused actually, but sometimes its needed, i have to do more config to expose it .