1. Overview
Sometimes OAuth2 APIs can diverge a little from the standard, in which case we need to do some customizations to the standard OAuth2 requests.
Spring Security 5.1 provides support for customizing OAuth2 authorization and token requests.
In this tutorial, we’ll see how to customize request parameters and response handling.
2. Custom Authorization Request
First, we’ll customize the OAuth2 authorization request. We can modify standard parameters and add extra parameters to the authorization request as we need.
To do so, we need to implement our own OAuth2AuthorizationRequestResolver:
public class CustomAuthorizationRequestResolver
implements OAuth2AuthorizationRequestResolver {
private OAuth2AuthorizationRequestResolver defaultResolver;
public CustomAuthorizationRequestResolver(
ClientRegistrationRepository repo, String authorizationRequestBaseUri) {
defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri);
}
// ...
}
Note that we used the DefaultOAuth2AuthorizationRequestResolver to provide base functionality.
We’ll also override the resolve() methods to add our customization logic:
public class CustomAuthorizationRequestResolver
implements OAuth2AuthorizationRequestResolver {
//...
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest req = defaultResolver.resolve(request);
if(req != null) {
req = customizeAuthorizationRequest(req);
}
return req;
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest req = defaultResolver.resolve(request, clientRegistrationId);
if(req != null) {
req = customizeAuthorizationRequest(req);
}
return req;
}
private OAuth2AuthorizationRequest customizeAuthorizationRequest(
OAuth2AuthorizationRequest req) {
// ...
}
}
We’ll add our customizations later on using our method customizeAuthorizationRequest() method as we’ll discuss in the next sections.
After implementing our custom OAuth2AuthorizationRequestResolver, we need to add it to our security configuration:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.oauth2Login()
.authorizationEndpoint()
.authorizationRequestResolver(
new CustomAuthorizationRequestResolver(
clientRegistrationRepository(), "/oauth2/authorize-client"))
//...
}
}
Here we used oauth2Login().authorizationEndpoint().authorizationRequestResolver() to inject our custom OAuth2AuthorizationRequestResolver.
3. Customizing Authorization Request Standard Parameters
Now, let’s discuss the actual customization. We can modify OAuth2AuthorizationRequest as much as we want.
For starters, we can modify a standard parameter for each authorization request.
We can, for example, generate our own “state” parameter:
private OAuth2AuthorizationRequest customizeAuthorizationRequest(
OAuth2AuthorizationRequest req) {
return OAuth2AuthorizationRequest
.from(req).state("xyz").build();
}
4. Authorization Request Extra Parameters
We can also add extra parameters to our OAuth2AuthorizationRequest using the additionalParameters() method of the OAuth2AuthorizationRequest and passing in a Map:
private OAuth2AuthorizationRequest customizeAuthorizationRequest(
OAuth2AuthorizationRequest req) {
Map<String,Object> extraParams = new HashMap<String,Object>();
extraParams.putAll(req.getAdditionalParameters());
extraParams.put("test", "extra");
return OAuth2AuthorizationRequest
.from(req)
.additionalParameters(extraParams)
.build();
}
We also have to make sure that we include the old additionalParameters before we add our new ones.
Let’s see a more practical example by customizing the authorization request used with the Okta Authorization Server.
4.1. Custom Okta Authorize Request
Okta has extra optional parameters for authorization request to provide the user with more functionality. For example, idp which indicates the identity provider.
The identity provider is Okta by default, but we can customize it using idp parameter:
private OAuth2AuthorizationRequest customizeOktaReq(OAuth2AuthorizationRequest req) {
Map<String,Object> extraParams = new HashMap<String,Object>();
extraParams.putAll(req.getAdditionalParameters());
extraParams.put("idp", "https://idprovider.com");
return OAuth2AuthorizationRequest
.from(req)
.additionalParameters(extraParams)
.build();
}
5. Custom Token Request
Now, we’ll see how to customize the OAuth2 token request.
We can customize the token request by customizing OAuth2AccessTokenResponseClient.
The default implementation for OAuth2AccessTokenResponseClient is DefaultAuthorizationCodeTokenResponseClient.
We can customize the token request itself by providing a custom RequestEntityConverter and we can even customize the token response handling by customizing DefaultAuthorizationCodeTokenResponseClient RestOperations:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.tokenEndpoint()
.accessTokenResponseClient(accessTokenResponseClient())
//...
}
@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient(){
DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new DefaultAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setRequestEntityConverter(new CustomRequestEntityConverter());
OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter =
new OAuth2AccessTokenResponseHttpMessageConverter();
tokenResponseHttpMessageConverter.setAccessTokenResponseConverter(new CustomTokenResponseConverter());
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
accessTokenResponseClient.setRestOperations(restTemplate);
return accessTokenResponseClient;
}
}
We can inject our OAuth2AccessTokenResponseClient using tokenEndpoint().accessTokenResponseClient().
To customize token request parameters, we’ll implement CustomRequestEntityConverter. Similarly, to customize handling token response, we’ll implement CustomTokenResponseConverter.
We’ll discuss both CustomRequestEntityConverter and CustomTokenResponseConverter in the following sections.
6. Token Request Extra Parameters
Now, we’ll see how to add extra parameters to our token request by building a custom Converter:
public class CustomRequestEntityConverter implements
Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {
private OAuth2AuthorizationCodeGrantRequestEntityConverter defaultConverter;
public CustomRequestEntityConverter() {
defaultConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
}
@Override
public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest req) {
RequestEntity<?> entity = defaultConverter.convert(req);
MultiValueMap<String, String> params = (MultiValueMap<String,String>) entity.getBody();
params.add("test2", "extra2");
return new RequestEntity<>(params, entity.getHeaders(),
entity.getMethod(), entity.getUrl());
}
}
Our Converter transforms OAuth2AuthorizationCodeGrantRequest to a RequestEntity.
We used default converter OAuth2AuthorizationCodeGrantRequestEntityConverter to provide base functionality, and added extra parameters to the RequestEntity body.
7. Custom Token Response Handling
Now, we’ll customize handling the token response.
We can use the default token response converter OAuth2AccessTokenResponseHttpMessageConverter as a starting point.
We’ll implement CustomTokenResponseConverter to handle the “scope” parameter differently:
public class CustomTokenResponseConverter implements
Converter<Map<String, Object>, OAuth2AccessTokenResponse> {
private static final Set<String> TOKEN_RESPONSE_PARAMETER_NAMES = Stream.of(
OAuth2ParameterNames.ACCESS_TOKEN,
OAuth2ParameterNames.TOKEN_TYPE,
OAuth2ParameterNames.EXPIRES_IN,
OAuth2ParameterNames.REFRESH_TOKEN,
OAuth2ParameterNames.SCOPE).collect(Collectors.toSet());
@Override
public OAuth2AccessTokenResponse convert(Map<String, Object> tokenResponseParameters) {
Object accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);
Set<String> scopes = Collections.emptySet();
if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) {
Object scope = tokenResponseParameters.get(OAuth2ParameterNames.SCOPE);
scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope.toString(), ","))
.collect(Collectors.toSet());
}
//...
return OAuth2AccessTokenResponse.withToken(accessToken.toString())
.tokenType(accessTokenType)
.expiresIn(expiresIn)
.scopes(scopes)
.refreshToken(refreshToken.toString())
.additionalParameters(additionalParameters)
.build();
}
}
The token response converter transforms Map to OAuth2AccessTokenResponse.
In this example, we parsed the “scope” parameter as a comma-delimited instead of a space-delimited String.
Let’s go through another practical example by customizing the token response using LinkedIn as an authorization server.
7.1. LinkedIn Token Response Handling
Finally, let’s see how to handle the LinkedIn token response. This contains only access_token and expires_in, but we also need token_type.
We can simply implement our own token response converter and set token_type manually:
public class LinkedinTokenResponseConverter
implements Converter<Map<String, String>, OAuth2AccessTokenResponse> {
@Override
public OAuth2AccessTokenResponse convert(Map<String, String> tokenResponseParameters) {
String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);
long expiresIn = Long.valueOf(tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN));
OAuth2AccessToken.TokenType accessTokenType = OAuth2AccessToken.TokenType.BEARER;
return OAuth2AccessTokenResponse.withToken(accessToken)
.tokenType(accessTokenType)
.expiresIn(expiresIn)
.build();
}
}
8. Conclusion
In this article, we learned how to customize OAuth2 authorization and token requests by adding or modifying request parameters.
The full source code for the examples is available over on GitHub.