8

I was using Spring Boot 1.4.0 with Spring OAuth2. When I requested a token, the server response was:

  {
  "access_token": "93f8693a-22d2-4139-a4ea-d787f2630f04",
  "token_type": "bearer",
  "refresh_token": "2800ea24-bb4a-4a01-ba87-2d114c1a2235",
  "expires_in": 899,
  "scope": "read write"
  }

When I updated my project to Spring Boot 1.4.1, the server response became

  {
  "error": "invalid_client",
  "error_description": "Bad client credentials"
  }

What was changed from version 1.4.0 to 1.4.1 ? And what should I do to make my request work again?

EDIT

WebSecurityConfiguration:

 @Configuration
 @EnableWebSecurity
 public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter{

 /** The client details service. */
    @Autowired
    private ClientDetailsService clientDetailsService;

    /** The password encoder. */
    @Autowired
    private PasswordEncoder passwordEncoder;

    /** The custom authentication provider. */
    @Autowired
    private CustomAuthenticationProvider customAuthenticationProvider;

    /** The o auth 2 token store service. */
    @Autowired
    private OAuth2TokenStoreService oAuth2TokenStoreService;

    /**
     * User details service.
     *
     * @return the user details service
     */
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetailsService userDetailsService = new ClientDetailsUserDetailsService(clientDetailsService);
        return userDetailsService;
    }

    /**
     * Register authentication.
     *
     * @param auth the auth
     */
    @Autowired
    protected void registerAuthentication(final AuthenticationManagerBuilder auth) {
        try {
            auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder);
        } catch (Exception e) {
            LOGGER.error("Não foi possível registrar o AuthenticationManagerBuilder.", e);
        }
    }

    /**
     * Authentication manager bean.
     *
     * @return the authentication manager
     * @throws Exception the exception
     */
    @Override
    @Bean(name = "authenticationManagerBean")
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * Authentication manager.
     *
     * @return the authentication manager
     * @throws Exception the exception
     */
    @Override
    @Bean(name = "authenticationManager")
    protected AuthenticationManager authenticationManager() throws Exception {
        UserAuthenticationManager userAuthenticationManager = new UserAuthenticationManager();
        userAuthenticationManager.setCustomAuthenticationProvider(customAuthenticationProvider);
        return userAuthenticationManager;
    }

    /**
     * User approval handler.
     *
     * @param tokenStore the token store
     * @return the token store user approval handler
     */
    @Bean
    @Autowired
    public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore) {
        TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
        handler.setTokenStore(oAuth2TokenStoreService);
        handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
        handler.setClientDetailsService(clientDetailsService);
        return handler;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO Auto-generated method stub
        super.configure(auth);
    }





}

The OAuth2Config

@Configuration
@EnableAuthorizationServer
@Order(LOWEST_PRECEDENCE - 100)
public class OAuth2Config extends AuthorizationServerConfigurerAdapter  {

/** The token store. */
@Autowired
private TokenStore tokenStore;

/** The user approval handler. */
@Autowired
private UserApprovalHandler userApprovalHandler;

/** The authentication manager. */
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;

/**
 * Para ativar o Authorization Basic remova a seguinte linha: security allowFormAuthenticationForClients()
 * 
 * @see http://stackoverflow.com/questions/26881296/spring-security-oauth2-full-authentication-is-required-to-access-this-resource
 * 
 */
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    security.allowFormAuthenticationForClients();
}

/* (non-Javadoc)
 * @see org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter#configure(org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer)
 */
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler).authenticationManager(authenticationManager);
}

Resource Server

/**
 * The Class ResourceServer.
 */
@Configuration
@EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {

private static final String CLIENTE_AUTHENTICATED_READ = "#oauth2.clientHasRole('ROLE_TRUSTED_CLIENT') and #oauth2.isClient() and #oauth2.hasScope('read')";

private static final String CLIENTE_AUTHENTICATED_WRITE = "#oauth2.clientHasRole('ROLE_TRUSTED_CLIENT') and #oauth2.isClient() and #oauth2.hasScope('write')";

private static final String CONTADOR_AUTHENTICATED_READ = "#oauth2.clientHasRole('ROLE_CLIENT') and #oauth2.isUser() and #oauth2.hasScope('read')";

private static final String CONTADOR_AUTHENTICATED_WRITE = "#oauth2.clientHasRole('ROLE_CLIENT') and #oauth2.isUser() and #oauth2.hasScope('write')";

private static final String CONTADOR_OR_CLIENTE_AUTHENTICATED_READ = "(#oauth2.clientHasRole('ROLE_TRUSTED_CLIENT') and #oauth2.isClient() and #oauth2.hasScope('read')) or (#oauth2.clientHasRole('ROLE_CLIENT') and #oauth2.isUser() and #oauth2.hasScope('read'))";

private static final String CONTADOR_OR_CLIENTE_AUTHENTICATED_WRITE = "(#oauth2.clientHasRole('ROLE_TRUSTED_CLIENT') and #oauth2.isClient() and #oauth2.hasScope('write')) or (#oauth2.clientHasRole('ROLE_CLIENT') and #oauth2.isUser() and #oauth2.hasScope('write'))";

private static final String URL_CONTADOR = "/v1/files/^[\\d\\w]{24}$/contadores/self";

private static final String URL_CLIENTE = "/v1/files/^[\\d\\w]{24}$/contadores/[0-9]{1,}";

/** The client details service. */
@Autowired
private ClientDetailsService clientDetailsService;

/** The o auth 2 token store service. */
@Autowired
private OAuth2TokenStoreService oAuth2TokenStoreService;

/**
 * http.authorizeRequests().antMatchers("/v1/emails").fullyAuthenticated();
 * https://github.com/ShuttleService/shuttle/blob/7a0001cfbed4fbf851f1b27cf1b952b2a37c1bb8/src/main/java/com/real/apps/shuttle/security/SecurityConfig.java
 * 
 * @see org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter#configure(org.springframework.security.config.annotation.web.builders.HttpSecurity)
 * 
 */
@Override
public void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();
    http.sessionManagement().sessionCreationPolicy(STATELESS).and().
        authorizeRequests()
        //===========================COMUNS (SEM AUTORIZAÇÃO) ===============//
        .antMatchers(POST, "/oauth/token").anonymous()
        .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()

        //===========================FILE CONTROLLER=========================//
        //===========================CONTADOR================================//
        .antMatchers(POST,     "/v1/files/randomId/contadores/self").access(CONTADOR_AUTHENTICATED_WRITE)
        .regexMatchers(PUT,    URL_CONTADOR).access(CONTADOR_AUTHENTICATED_WRITE)
        .regexMatchers(GET,    URL_CONTADOR).access(CONTADOR_AUTHENTICATED_READ)
        .regexMatchers(DELETE, URL_CONTADOR).access(CONTADOR_AUTHENTICATED_WRITE)

        //===========================CLIENTE=================================//
        .regexMatchers(POST,   "/v1/files/randomId/contadores/[0-9]{1,}").access(CLIENTE_AUTHENTICATED_WRITE)
        .regexMatchers(PUT,    URL_CLIENTE).access(CLIENTE_AUTHENTICATED_WRITE)
        .regexMatchers(GET,    URL_CLIENTE).access(CLIENTE_AUTHENTICATED_READ)
        .regexMatchers(DELETE, URL_CLIENTE).access(CLIENTE_AUTHENTICATED_WRITE)

        //===========================METADATA CONTROLLER=====================//
        .antMatchers(GET,      "/v1/metadatas/").access(CONTADOR_OR_CLIENTE_AUTHENTICATED_READ)
        .regexMatchers(GET,    "/v1/metadatas/^[\\d\\w]{24}$").access(CONTADOR_OR_CLIENTE_AUTHENTICATED_READ)
        .regexMatchers(GET,    "/v1/metadatas/self/folders/^[\\d\\w]{24}$").access(CONTADOR_OR_CLIENTE_AUTHENTICATED_READ)

        //===========================FOLDER CONTROLLER=======================//
        .regexMatchers(PUT,    "/v1/folders/^[\\d\\w]{24}$/contadores/self/lock").access(CONTADOR_AUTHENTICATED_WRITE)
        .regexMatchers(PUT,    "/v1/folders/^[\\d\\w]{24}$/contadores/self/unlock").access(CONTADOR_AUTHENTICATED_WRITE)
        .regexMatchers(GET,    "/v1/folders/^[\\d\\w]{24}$").access(CONTADOR_AUTHENTICATED_READ)

        //===========================ESPAÇO CONTROLLER=======================//
        .antMatchers(GET,      "/v1/espacos/contadores/self").access(CONTADOR_AUTHENTICATED_READ)

        //===========================OBRIGACAO CONTROLLER====================//
        .antMatchers(GET,      "/v1/obrigacoes").access(CONTADOR_AUTHENTICATED_READ)
        .antMatchers(POST,     "/v1/obrigacoes").access(CONTADOR_OR_CLIENTE_AUTHENTICATED_WRITE)

        //===========================PROTOCOLO CONTROLLER===================//
        .regexMatchers(GET,      "/v1/protocolos/^[\\d\\w]{24}$").access(CONTADOR_AUTHENTICATED_READ)
        .and().authorizeRequests().antMatchers("/v1/**").authenticated();





}

/* (non-Javadoc)
 * @see org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter#configure(org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer)
 */
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    resources.tokenServices(customTokenServices());
    resources.resourceId("arquivos-upload-api").stateless(false);
}

/**
 * Custom token services.
 *
 * @return the resource server token services
 */
@Primary
@Bean(name = "defaultAuthorizationServerTokenServices")
public ResourceServerTokenServices customTokenServices() {
    final CustomTokenServices defaultTokenServices = new CustomTokenServices();
    defaultTokenServices.setTokenStore(oAuth2TokenStoreService);
    defaultTokenServices.setSupportRefreshToken(true);
    defaultTokenServices.setReuseRefreshToken(false);
    defaultTokenServices.setClientDetailsService(clientDetailsService);
    return defaultTokenServices;
}
}

EDIT 2

There an class called ProviderManager. When I request a token, the method below is called:

    public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    Authentication result = null;
    boolean debug = logger.isDebugEnabled();

    for (AuthenticationProvider provider : getProviders()) {
        if (!provider.supports(toTest)) {
            continue;
        }

        if (debug) {
            logger.debug("Authentication attempt using "
                    + provider.getClass().getName());
        }

        try {
            result = provider.authenticate(authentication);

            if (result != null) {
                copyDetails(authentication, result);
                break;
            }
        }
        catch (AccountStatusException e) {
            prepareException(e, authentication);
            // SEC-546: Avoid polling additional providers if auth failure is due to
            // invalid account status
            throw e;
        }
        catch (InternalAuthenticationServiceException e) {
            prepareException(e, authentication);
            throw e;
        }
        catch (AuthenticationException e) {
            lastException = e;
        }
    }

    if (result == null && parent != null) {
        // Allow the parent to try.
        try {
            result = parent.authenticate(authentication);
        }
        catch (ProviderNotFoundException e) {
            // ignore as we will throw below if no other exception occurred prior to
            // calling parent and the parent
            // may throw ProviderNotFound even though a provider in the child already
            // handled the request
        }
        catch (AuthenticationException e) {
            lastException = e;
        }
    }

    if (result != null) {
        if (eraseCredentialsAfterAuthentication
                && (result instanceof CredentialsContainer)) {
            // Authentication is complete. Remove credentials and other secret data
            // from authentication
            ((CredentialsContainer) result).eraseCredentials();
        }

        eventPublisher.publishAuthenticationSuccess(result);
        return result;
    }

    // Parent was null, or didn't authenticate (or throw an exception).

    if (lastException == null) {
        lastException = new ProviderNotFoundException(messages.getMessage(
                "ProviderManager.providerNotFound",
                new Object[] { toTest.getName() },
                "No AuthenticationProvider found for {0}"));
    }

    prepareException(lastException, authentication);

    throw lastException;
}

The difference between version 1.4.0 and 1.4.1 is that the attribute parent is null on version 1.4.1, and then, on the snippet of the method below the condition is false, and the method throws BadClientException

 if (result == null && parent != null) {
    // Allow the parent to try.
    try {
        result = parent.authenticate(authentication);
    }
    catch (ProviderNotFoundException e) {
        // ignore as we will throw below if no other exception occurred prior to
        // calling parent and the parent
        // may throw ProviderNotFound even though a provider in the child already
        // handled the request
    }
    catch (AuthenticationException e) {
        lastException = e;
    }
}

EDIT 3

I've found where this error is coming from. After the update from Spring Boot 1.4.0 to 1.4.1, the dependecy

        <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
       </dependency>

changed from version 2.0.10 to 2.0.11. If I force the version to be 2.0.10 on Spring Boot 1.4.1, the token request works normally. So, it seems to be an issue of Spring Security OAuth2 and not from Spring Boot.

EDIT 4

I commited a sample project on github where you will be able to see what I'm facing when changing the version of spring boot from version 1.4.0 to 1.4.1

Gabriel
  • 952
  • 10
  • 31

1 Answers1

2

It is really a spring oauth security problem. There is an issue open on github.
https://github.com/spring-projects/spring-security-oauth/issues/896