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
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.