I had the same error.
An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: Error while extracting response for type [class org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse] and content type [application/json]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: An error occurred reading the OAuth 2.0 Access Token Response: tokenType cannot be null; nested exception is java.lang.IllegalArgumentException: tokenType cannot be null
Here you see linkedin OAuth2 provider is complaining tokenType cannot be null
when you are requesting the token.
This happens when Google, and certain other 3rd party identity providers, are more strict about the token type name that is sent in the headers to the user info endpoint. The default is “Bearer” which suits most providers and matches the spec, but if you need to change it you can set security.oauth2.resource.token-type.
Reference
In order to have this configuration, we will need to bring in spring-security-oauth2-autoconfigure
.
Solution 1
Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RC2</version>
</dependency>
Configuration
In the .properties
configurations, you can add
security.oauth2.resource.token-type=Bearer
But the solution above did not solve the problem, so I check out this next solution.
Solution 2
Reference
Reference
This is a known bug for Spring Security 5. LinkedIn is not a very popular choice as an OAuth2 authorization provider. It is not surprising that Spring Security is beautifully compatible with Google and other providers but it does not comply with LinkedIn OAuth2 requirements for clients.
5.1. Successful Response
The authorization server issues an access token and optional refresh token, and constructs the response by adding the following parameters to the entity-body of the HTTP response with a 200 (OK) status code:
token_type REQUIRED. The type of the token issued as described in Section 7.1. Value is case insensitive.
token_type
is a required parameter but Spring Security does not pass it into the request.
In the SecurityConfig
, you can specify how you want to handle the token response by setting .tokenEndpoint().accessTokenResponseClient(authorizationCodeTokenResponseClient())
The full demo code is available at https://github.com/jzheaux/messaging-app/blob/master/client-app/src/main/java/sample/config/SecurityConfig.java#L61
SecurityConfig
.authorizationEndpoint()
.baseUri(...)
.authorizationRequestRepository(...)
.and()
.tokenEndpoint()
.accessTokenResponseClient(authorizationCodeTokenResponseClient())
.and()
.redirectionEndpoint()
.baseUri(...)
authorizationCodeTokenResponseClient()
private method
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeTokenResponseClient() {
OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter =
new OAuth2AccessTokenResponseHttpMessageConverter();
tokenResponseHttpMessageConverter.setTokenResponseConverter(new CustomAccessTokenResponseConverter()); //https://github.com/jzheaux/messaging-app/blob/392a1eb724b7447928c750fb2e47c22ed26d144e/client-app/src/main/java/sample/web/CustomAccessTokenResponseConverter.java#L35
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
tokenResponseClient.setRestOperations(restTemplate);
return tokenResponseClient;
}
Custom Convertor
package com.fermedu.resume.config;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CustomAccessTokenResponseConverter implements Converter<Map<String, String>, 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, String> tokenResponseParameters) {
String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);
OAuth2AccessToken.TokenType accessTokenType = OAuth2AccessToken.TokenType.BEARER;
long expiresIn = 0;
if (tokenResponseParameters.containsKey(OAuth2ParameterNames.EXPIRES_IN)) {
try {
expiresIn = Long.valueOf(tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN));
} catch (NumberFormatException ex) { }
}
Set<String> scopes = Collections.emptySet();
if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) {
String scope = tokenResponseParameters.get(OAuth2ParameterNames.SCOPE);
scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope, " ")).collect(Collectors.toSet());
}
Map<String, Object> additionalParameters = new LinkedHashMap<>();
tokenResponseParameters.entrySet().stream()
.filter(e -> !TOKEN_RESPONSE_PARAMETER_NAMES.contains(e.getKey()))
.forEach(e -> additionalParameters.put(e.getKey(), e.getValue()));
return OAuth2AccessTokenResponse.withToken(accessToken)
.tokenType(accessTokenType)
.expiresIn(expiresIn)
.scopes(scopes)
.additionalParameters(additionalParameters)
.build();
}
}
This solution 2 works for me.
https://github.com/scribejava/scribejava/tree/master/scribejava-apis – Tariq Husein Jun 01 '20 at 03:15