I am trying to invoke some backend system which is secured by a client_credentials grant type from a Feign client application.
The access token from the backend system can be retrieved with the following curl structure (just as an example):
curl --location --request POST '[SERVER URL]/oauth/grant' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: WebSessionID=172.22.72.1.1558614080219404; b8d49fdc74b7190aacd4ac9b22e85db8=2f0e4c4dbf6d4269fd3349f61c151223' \
--data-raw 'grant_type=client_credentials' \
--data-raw 'client_id=[CLIENT_ID]' \
--data-raw 'client_secret=[CLIENT_SECRET]'
{"accessToken":"V29C90D1917528E9C29795EF52EC2462D091F9DC106FAFD829D0FA537B78147E20","tokenType":"Bearer","expiresSeconds":7200}
This accessToken should then be set in a header to subsequent business calls to the backend system.
So now my question is, how to implement this using Feign and Spring Boot Security 5. After some research I come to this solution (which doesn't work):
spring:
security:
oauth2:
client:
registration:
backend:
client-id:[CLIENT_ID]
client-secret: [CLIENT_SECRET]
authorization-grant-type: client_credentials
provider:
backend:
token-uri: [SERVER URL]/oauth/grant
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
return authorizedClientManager;
}
public class OAuthRequestInterceptor implements RequestInterceptor {
private OAuth2AuthorizedClientManager manager;
public OAuthRequestInterceptor(OAuth2AuthorizedClientManager manager) {
this.manager = manager;
}
@Override
public void apply(RequestTemplate requestTemplate) {
OAuth2AuthorizedClient client = this.manager.authorize(OAuth2AuthorizeRequest.withClientRegistrationId("backend").principal(createPrincipal()).build());
String accessToken = client.getAccessToken().getTokenValue();
requestTemplate.header(HttpHeaders.AUTHORIZATION, "Bearer" + accessToken);
}
private Authentication createPrincipal() {
return new Authentication() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptySet();
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getDetails() {
return null;
}
@Override
public Object getPrincipal() {
return this;
}
@Override
public boolean isAuthenticated() {
return false;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
@Override
public String getName() {
return "backend";
}
};
}
}
public class FeignClientConfig {
@Bean
public OAuthRequestInterceptor repositoryClientOAuth2Interceptor(OAuth2AuthorizedClientManager manager) {
return new OAuthRequestInterceptor(manager);
}
}
@FeignClient(name = "BackendRepository", configuration = FeignClientConfig.class, url = "${BACKEND_URL}")
public interface BackendRepository {
@GetMapping(path = "/healthChecks", produces = MediaType.APPLICATION_JSON_VALUE)
public Info healthCheck();
}
When running this code, I get the error:
org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse] and content type [text/html;charset=utf-8]
Debugging the code it looks like the DefaultClientCredentialsTokenResponseClient is requesting the auth endpoint using Basic Authentication. Although I never set this up.
Any advise what I can do? Maybe there is a completely different approach to do this.
RELEASE classes such as OAuth2RestTemplate , OAuth2ProtectedResourceDetails and ClientCredentialsAccessTokenProvider have all been marked as deprecated.
Since in Feign Client you usually do everything in the interface, the same will be for the Authorization header. What you need to do is to simply pass your Authorization header in the declaration of your Feign Client method, as an @RequestHeader argument.
Navigate to the Security section. In the Security section, select Client secret (API Key), in addition to Client ID (API Key) which should already be selected by default. to save your changes.
For this to work with Spring Security 5 and Feign you need to have
Here we will register a generic internal-api
client for your oauth2 client credentials
. This is where you specify the client-id
,client-secret
, scopes
and grant type
.
All basic Spring Security 5 stuff. This also involves setting up a provider (here I am using a custom OpenID Connect provider called "yourprovider"
spring:
security:
oauth2:
client:
registration:
internal-api:
provider: yourprovider
client-id: x
client-secret: y
scope:
- ROLE_ADMIN
authorization-grant-type: client_credentials
provider:
yourprovider:
issuer-uri: yourprovider.issuer-uri
resourceserver:
jwt:
issuer-uri: yourprovider.issuer-uri
Next you need your feign config. This will use a OAuth2FeignRequestInterceptor
public class ServiceToServiceFeignConfiguration extends AbstractFeignConfiguration {
@Bean
public OAuth2FeignRequestInterceptor requestInterceptor() {
return new OAuth2FeignRequestInterceptor(
OAuth2AuthorizeRequest.withClientRegistrationId("internal-api")
.principal(new AnonymousAuthenticationToken("feignClient", "feignClient", createAuthorityList("ROLE_ANONYMOUS")))
.build());
}
}
And a RequestInterceptor that looks like this :
The OAuth2AuthorizedClientManager
is a bean that you can configure in your Configuration
public OAuth2AuthorizedClientManager authorizedClientManager(final ClientRegistrationRepository clientRegistrationRepository, final OAuth2AuthorizedClientService authorizedClientService) {
return new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
}
The OAuth2AuthorizeRequest
is provided by the Feign Configuration above.
The oAuth2AuthorizedClientManager
can authorize the oAuth2AuthorizeRequest
, get you the access token, and provide it as an Authorization
header to the underlying service
public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
@Inject
private OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;
private OAuth2AuthorizeRequest oAuth2AuthorizeRequest;
OAuth2FeignRequestInterceptor(OAuth2AuthorizeRequest oAuth2AuthorizeRequest) {
this.oAuth2AuthorizeRequest = oAuth2AuthorizeRequest;
}
@Override
public void apply(RequestTemplate template) {
template.header(AUTHORIZATION,getAuthorizationToken());
}
private String getAuthorizationToken() {
final OAuth2AccessToken accessToken = oAuth2AuthorizedClientManager.authorize(oAuth2AuthorizeRequest).getAccessToken();
return String.format("%s %s", accessToken.getTokenType().getValue(), accessToken.getTokenValue());
}
}
I am quite experienced with Feign and OAuth2 and it took me some good hours to find how to do that. First, let's say that my app is based on latest Spring libraries, so I am using the following dependencies (managed version for spring-cloud-starter-openfeign is 3.0.0)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
In my application.properties I have the following
security.oauth2.client.access-token-uri=https://api.twitter.com/oauth2/token
security.oauth2.client.client-id=my-secret-twitter-id
security.oauth2.client.client-secret=my-secret-twitter-secret
security.oauth2.client.grant-type=client_credentials
And finally my configuration beans
package es.spanishkangaroo.ttanalyzer.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import feign.RequestInterceptor;
@Configuration
public class FeignClientConfiguration {
@Bean
@ConfigurationProperties(prefix = "security.oauth2.client")
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
return new ClientCredentialsResourceDetails();
}
@Bean
public RequestInterceptor oauth2FeignRequestInterceptor(){
return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails());
}
@Bean
public OAuth2RestTemplate clientCredentialsRestTemplate() {
return new OAuth2RestTemplate(clientCredentialsResourceDetails());
}
}
So then the Feign client is as simple as
package es.spanishkangaroo.ttanalyzer.api;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import es.clovelly.ttanalyzer.model.Trends;
@FeignClient(name = "twitterClient", url = "https://api.twitter.com/1.1/")
public interface TwitterClient {
@GetMapping("/trends/place.json")
Trends[] getTrendsById(@RequestParam Long id);
}
As you may have noticed, the code is automatically getting a token (a bearer token) before the client call. If you are using a non-expiring bearer token you can just use something like
@Bean
public OAuth2ClientContext oAuth2ClientContext() {
DefaultOAuth2ClientContext context = new DefaultOAuth2ClientContext();
context.setAccessToken(bearerToken);
return context;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With