Summary

Hi there, I've been dealing with this issue for the past few days that I believe I've finally resolved. Essentially I could not connect a spring backend to an external (non-spring) OAuth2 server for token introspection. I followed the stacktrace and adjusted two lines that seemed to be problematic. The problem as I've found had to do with bad casting from String to Collection.

Current Behavior

First off, here is the method that was giving me some issues when I was integrating all of this. My scope and aud are strings just to avoid confusion.

//package org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter

public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
        HashMap parameters = new HashMap();
        LinkedHashSet scope = new LinkedHashSet((Collection)(map.containsKey("scope")?(Collection)map.get("scope"):Collections.emptySet()));
        Authentication user = this.userTokenConverter.extractAuthentication(map);
        String clientId = (String)map.get("client_id");
        parameters.put("client_id", clientId);
        if(this.includeGrantType && map.containsKey("grant_type")) {
            parameters.put("grant_type", (String)map.get("grant_type"));
        }

        LinkedHashSet resourceIds = new LinkedHashSet((Collection)(map.containsKey("aud")?(Collection)map.get("aud"):Collections.emptySet()));
        List authorities = null;
        if(user == null && map.containsKey("authorities")) {
            String[] request = (String[])((Collection)map.get("authorities")).toArray(new String[0]);
            authorities = AuthorityUtils.createAuthorityList(request);
        }

        OAuth2Request request1 = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, (String)null, (Set)null, (Map)null);
        return new OAuth2Authentication(request1, user);
    }

The following two lines were giving me trouble as they were converting straight from a String to a Collection. Instead, I first converted String -> String[] -> List -> Collection (See below). That seemed to fix it and now I no longer get an exception.

Problem Lines

LinkedHashSet scope = new LinkedHashSet((Collection)(map.containsKey("scope")?(Collection)map.get("scope"):Collections.emptySet()));
LinkedHashSet resourceIds = new LinkedHashSet((Collection)(map.containsKey("aud")?(Collection)map.get("aud"):Collections.emptySet()));
        List authorities = null;

The exception output

{
  "timestamp": 1482472600723,
  "status": 500,
  "error": "Internal Server Error",
  "exception": "java.lang.ClassCastException",
  "message": "java.lang.String cannot be cast to java.util.Collection",
  "path": "/somepath"
}

My Changes

I created a class that inherits from AccessTokenConverter and overrode this method with the following:

public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
        Map<String, String> parameters = new HashMap<>();
        String scope = (String) map.get(SCOPE);
        Collection<String> scopeCollection = map.containsKey(SCOPE) ? (Collection<String>) Arrays.asList(scope.split(" ")) : Collections.emptySet();
        Set<String> scopeSet = new LinkedHashSet<String>(scopeCollection);
        Authentication user = userTokenConverter.extractAuthentication(map);
        String clientId = (String) map.get(CLIENT_ID);
        parameters.put(CLIENT_ID, clientId);
        if (includeGrantType && map.containsKey(GRANT_TYPE)) {
            parameters.put(GRANT_TYPE, (String) map.get(GRANT_TYPE));
        }

        String aud = (String) map.get(AUD);
        Collection<String> audCollection = map.containsKey(AUD) ? (Collection<String>) Arrays.asList(aud.split(" ")) : Collections.<String>emptySet();
        Set<String> resourceIds = new LinkedHashSet<String>(audCollection);
        Collection<? extends GrantedAuthority> authorities = null;
        if (user == null && map.containsKey(AUTHORITIES)) {
            String[] roles = ((Collection<String>) map.get(AUTHORITIES)).toArray(new String[0]);
            authorities = AuthorityUtils.createAuthorityList(roles);
        }
        OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scopeSet, resourceIds, null, null,
                null);
        return new OAuth2Authentication(request, user);
    }

Expected Behavior

Bad token:

{
  "error": "invalid_token",
  "error_description": "$TOKEN"
}

Good output:

{
  "output": "Some Output"
}

Version

From Gradle, here are my spring dependencies:

    compile("org.springframework.boot:spring-boot-starter:1.3.3.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-data-jpa:1.3.3.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-web:1.3.3.RELEASE")
    compile("org.springframework.security.oauth:spring-security-oauth2:2.0.7.RELEASE")

Other Code

My OAuth2Configuration:

public class OAuth2ServerConfiguration {
    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

        private TokenExtractor tokenExtractor = new BearerTokenExtractor();

        @Autowired
        @SuppressWarnings("SpringJavaAutowiringInspection")
        private AuthorizationConfig authorizationConfig;

        @Override
        public void configure(final ResourceServerSecurityConfigurer resources) {
            String id = authorizationConfig.getClientId(),
                    secret = authorizationConfig.getSecret(),
                    endpoint = authorizationConfig.getEndpoint();

            System.out.println(id);
            System.out.println(secret);
            System.out.println(endpoint);

            HydraTokenService remoteTokenServices = new MyTokenService(id, secret, endpoint, new MyAccessTokenConverter());
            resources.resourceId(authorizationConfig.getClientId());
            resources.tokenServices(remoteTokenServices);
        }

        @Override
        public void configure(final HttpSecurity http) throws Exception {
            http.addFilterAfter(new OncePerRequestFilter() {
                @Override
                protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
                    if (tokenExtractor.extract(request) == null) {
                        SecurityContextHolder.clearContext();
                    }
                    filterChain.doFilter(request, response);
                }
            }, AbstractPreAuthenticatedProcessingFilter.class);
            http.csrf().disable();
            http.authorizeRequests().anyRequest().authenticated();
        }
    }
}

Custom RemoteTokenServices

public class MyTokenService extends RemoteTokenServices {

    protected final Log logger = LogFactory.getLog(getClass());

    private RestOperations restTemplate;

    private String checkTokenEndpointUrl;

    private String clientId;

    private String clientSecret;

    private String tokenName = "token";

    private AccessTokenConverter tokenConverter;

    public MyTokenService(String clientId, String clientSecret, String tokenEndpoint, AccessTokenConverter tokenConverter) {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.checkTokenEndpointUrl = tokenEndpoint;
        this.tokenConverter = tokenConverter;


        restTemplate = new RestTemplate();
        ((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            // Ignore 400
            public void handleError(ClientHttpResponse response) throws IOException {
                if (response.getRawStatusCode() != 400) {
                    super.handleError(response);
                }
            }
        });
    }

    @Override
    public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {

        MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
        formData.add(tokenName, accessToken);
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
        Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);

        // I changed this line as well but I don't believe it was causing any issues.
        Boolean isActive = (Boolean)map.get("active");
        if (!isActive){
            logger.debug("check_token returned error: " + map.get("error"));
            throw new InvalidTokenException(accessToken);
        }

        return tokenConverter.extractAuthentication(map);
    }

    private String getAuthorizationHeader(String clientId, String clientSecret) {
        String creds = String.format("%s:%s", clientId, clientSecret);

        System.out.println(String.format("Credentials: %s", creds));

        try {
            return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
        }
        catch (UnsupportedEncodingException e) {
            throw new IllegalStateException("Could not convert String");
        }
    }

    private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
        if (headers.getContentType() == null) {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        }
        @SuppressWarnings("rawtypes")
        Map map = restTemplate.exchange(path, HttpMethod.POST,
                new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
        @SuppressWarnings("unchecked")
        Map<String, Object> result = map;
        return result;
    }
}

Please let me know if there's anything more I should provide. Thanks!

Comment From: tuzko

<dependency>
            <!-- Eureka service registration -->
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.fasterxml.jackson.dataformat</groupId>
                    <artifactId>jackson-dataformat-xml</artifactId>
                </exclusion>
            </exclusions>
</dependency>

this solved string to collection, arraylist to string etc

Comment From: eleftherias

Thanks for reaching out and apologies for the late response. This is an issue with the legacy spring-security-oauth project which is now deprecated. If a similar issue occurs using Spring Security please create another issue and we will take a look.