3

I am trying to use LinkedIn authentication with my spring boot application, i am getting the following error

[invalid_token_response] 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;charset=utf-8]; 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

Error Message On Screen

this is my application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          linkedin:
            clientId: CLIENTID
            clientSecret: SECRET
            client-authentication-method: post
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/linkedin
            scope: r_liteprofile, r_emailaddress,w_member_social

            client-name: Linkedin

        provider:
          linkedin:          
            authorization-uri: https://www.linkedin.com/oauth/v2/authorization
            token-uri: https://www.linkedin.com/oauth/v2/accessToken
            user-info-uri: https://api.linkedin.com/v1/people/~?format=json
            user-name-attribute: id

any ideas how to solve this or how to do authentication in spring boot with linkedin

Jagadesh
  • 2,104
  • 1
  • 16
  • 28
Tariq Husein
  • 186
  • 1
  • 9
  • Notice the network tab on your browser to check what exactly is being returned as the response. Without the linked in application its hard to test it and identify the issue. Confirm the parameters being passed are correct, e.g. scope parameters should be bifurcated by space as described in the documentation here. https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin/context – Sariq Shaikh Apr 16 '20 at 17:52
  • the request is done by the spring security, the params being passed are all correct – Tariq Husein Apr 17 '20 at 04:16
  • hello, were u able to solve the issue? I am also facing the same – vigamage May 31 '20 at 13:56
  • 1
    @vigamage not really, I couldn't figure it out, but instead i started using scribejava, it is way easier
    https://github.com/scribejava/scribejava/tree/master/scribejava-apis
    – Tariq Husein Jun 01 '20 at 03:15

1 Answers1

1

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.

justthink
  • 439
  • 3
  • 6
  • I stopped trying to integrate SpringSecurity with LinkedIn OAuth2. I found no other SDK so I gave up on SpringSecurity and implemented the OAuth2 myself. – justthink Jul 30 '21 at 07:32