Search code examples
springspring-securityoauth-2.0spring-security-oauth2spring-oauth2

Authentication is required to obtain an access token - when using 'password' grant and Spring's ResourceOwnerPasswordResourceDetails


I am new to Spring Security and I want to implement a client for a OAUTH2 secured service that only accepts password grant.

Obtaining the access_token from the auth server is done using data in the http body like this:

client_id={{clientId}}&client_secret={{client_secret}}&grant_type=password&username={{username}}&password={{password}}

Afterwards the access_token must be used in the header field Authorization to access the actual service. (e.g. Authorization=Bearer <access_token>)

My goal is to use the provided features from Spring Security OAuth2 to request an access_token from the auth service, and use it for accessing the service endpoints until token expiration. I also like to have that my access_token is automatically refreshed using the refresh_token value from the auth server. I want to achieve this while fully utilizing Spring's features.

I found that I can use OAuth2RestTemplate with ResourceOwnerPasswordResourceDetails for the grant_type password.

The StackOverflow post oAuth2 client with password grant in Spring Security was very helpful for me, but I have not got it to work. I also found the post Authentication is required to obtain an access token (anonymous not allowed) where a user encountered the same exception, but uses client_credentials and AuthorizationCodeResourceDetails.

At the moment my code looks like this.

@Service
public class MyClient {

    @Autowired
    private OAuth2RestTemplate restTemplate;

    @Value("${authServer.accessTokenUri}")
    private String accessTokenUri;

    @Value("${authServer.clientId}")
    private String clientId;

    @Value("${authServer.clientSecret}")
    private String clientSecret;

    @Value("${authServer.username}")
    private String username;

    @Value("${authServer.password}")
    private String password;

    @Value("${serviceUrl}")
    private String serviceUrl;


    @Bean
    public OAuth2RestTemplate restTemplate(OAuth2ClientContext oauth2ClientContext) {
        OAuth2RestTemplate template = new OAuth2RestTemplate(resource(), oauth2ClientContext);
        template.setAccessTokenProvider(accessTokenProvider());
        return template;
    }

    @Bean
    public AccessTokenProvider accessTokenProvider() {
        ResourceOwnerPasswordAccessTokenProvider tokenProvider = new ResourceOwnerPasswordAccessTokenProvider();
        return new AccessTokenProviderChain(
                Arrays.<AccessTokenProvider>asList(tokenProvider)
        );
    }

    @Bean
    protected OAuth2ProtectedResourceDetails resource() {
        ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
        resource.setId(clientId);
        resource.setAccessTokenUri(accessTokenUri);
        resource.setClientId(clientId);
        resource.setClientSecret(clientSecret);
        resource.setGrantType("password");
        resource.setClientAuthenticationScheme(AuthenticationScheme.form); // fetch access_token by sending authentication data in HTTP Body
        resource.setAuthenticationScheme(AuthenticationScheme.header); // send access_token via HTTP Header 'Bearer' field when accessing actual service
        resource.setUsername(username);
        resource.setPassword(password);
        return resource;
    }


    public void getDataFromService() {
        String response = restTemplate.getForObject(serviceUrl, String.class);
    }

}

An exception is thrown in AccessTokenProviderChain, because of this block.

if (auth instanceof AnonymousAuthenticationToken) {
    if (!resource.isClientOnly()) {
        throw new InsufficientAuthenticationException("Authentication is required to obtain an access token (anonymous not allowed)");
    }
}

Here is the exception stack trace.

org.springframework.security.authentication.InsufficientAuthenticationException: Authentication is required to obtain an access token (anonymous not allowed)
    at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:91) ~[spring-security-oauth2-2.3.4.RELEASE.jar:na]
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221) ~[spring-security-oauth2-2.3.4.RELEASE.jar:na]
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173) ~[spring-security-oauth2-2.3.4.RELEASE.jar:na]
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.createRequest(OAuth2RestTemplate.java:105) ~[spring-security-oauth2-2.3.4.RELEASE.jar:na]
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:731) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.doExecute(OAuth2RestTemplate.java:128) ~[spring-security-oauth2-2.3.4.RELEASE.jar:na]
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:670) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
    at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:311) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]

As you can see I cannot request an access_token. I do not understand why I get this exception, because if I directly request an access_token from the auth server using the curl command, I am able to authenticate using only the provided data as stated.

I manually obtained an access_token successfully like this, when adding the following code before invoking restTemplate.getForObject(...).

ResourceOwnerPasswordAccessTokenProvider accessTokenProvider = new ResourceOwnerPasswordAccessTokenProvider();
OAuth2AccessToken token = accessTokenProvider.obtainAccessToken(resource(), new DefaultAccessTokenRequest());
restTemplate.getOAuth2ClientContext().setAccessToken(token);
String token = restTemplate.getAccessToken();

But, manually obtaining the access_token is not that what I want. Is there something I am missing? Is it possible to automatically obtain an access_token and refresh it using Spring Security with password grant? Although checking code multiple hours on Github, StackOverflow etc. ... I have not been able to get my code to work.


UPDATE:

I found that my ResourceOwnerPasswordResourceDetails instance inside my OAuth2RestTemplate instance is not initialized, when I want to make use of it inside getDataFromService(). (i.e. the fields like username are null). After clarification and help from @JoeGrandja, my question now does not really target Spring Security, but rather Spring.

What can I do to make use of the @Value annotations inside a @Bean annotated method. At the moment, when the restTemplate is constructed using the @Bean annotated method resource(), the values from the application.yml are obviously not available yet.


Solution

  • I found a solution with the help and support of @JoeGrandja. Thank you very much! :)

    If anyone else has problems, here is my working solution. I also recommend reading the comments from @JoeGrandja above.

    @Configuration
    @ConfigurationProperties(prefix = "authserver")
    public class AuthServerConfigProperties {
    
        private String accessTokenUri;
        private String clientId;
        private String grantType;
        private String clientSecret;
        private String username;
        private String password;
    
       // Getter & Setter for all properties ...
    }
    
    
    @Configuration
    public class CommConfig {
    
        @Autowired
        AuthServerConfigProperties configProperties;
    
        @Bean
        public OAuth2RestOperations restTemplate(OAuth2ClientContext oauth2ClientContext) {
            OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(resource(), oauth2ClientContext);
            oAuth2RestTemplate.setAccessTokenProvider(new ResourceOwnerPasswordAccessTokenProvider());
            return oAuth2RestTemplate;
        }
    
        @Bean
        protected OAuth2ProtectedResourceDetails resource() {
            ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
            resource.setId(configProperties.getClientId()); // not necessary
            resource.setAccessTokenUri(configProperties.getAccessTokenUri());
            resource.setClientId(configProperties.getClientId());
            resource.setClientSecret(configProperties.getClientSecret());
            resource.setGrantType(configProperties.getGrantType());
            resource.setClientAuthenticationScheme(AuthenticationScheme.form); // fetch access_token by sending authentication data in HTTP Body
            resource.setAuthenticationScheme(AuthenticationScheme.header); // send access_token via HTTP Header 'Bearer' field when accessing actual service
            resource.setUsername(configProperties.getUsername());
            resource.setPassword(configProperties.getPassword());
            return resource;
        }
    }
    
    
    @RestController
    public class MyController {
    
        @Autowired
        private OAuth2RestOperations restTemplate;
    
        @Value("${serviceUrl}")
        private String serviceUrl;
    
        @RequestMapping(value = "/getData", method = RequestMethod.GET)
        @ResponseBody
        public ResponseEntity<String> getData() {
            String response = restTemplate.getForObject(serviceUrl, String.class);
            return new ResponseEntity(response, HttpStatus.OK);
        }
    }