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.
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);
}
}