I want to create some authentication service to be used for WebClient, so it automatically refresh the token when needed:
#Service
public class AuthService {
private String token;
private final WebClient webClient;
private final Map<String, String> bodyValues;
#Autowired
public AuthService(WebClient webClient) {
this.webClient = webClient;
this.bodyValues = new HashMap<>();
this.bodyValues.put("user", "myUser");
this.bodyValues.put("password", "somePassword");
}
public String getToken() {
if (this.token == null || this.isExpired(this.token) {
this.refreshToken();
}
return this.token;
}
private void refreshToken() {
this.token = webClient.post()
.uri("authEndpointPath")
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(bodyValues))
.retrieve()
.bodyToMono(String.class)
.block();
}
private boolean isExpired() {
//implementation
}
}
It gets the token correctly when it is expired. Is there a way to use it ONLY ONCE, without injecting it to other services? I was thinking about defining the Bean which uses authService.getToken() method:
#Configuration
public class CustomWebClientConfig {
private final AuthService authService;
#Autowired
public CustomWebClientConfig(AuthService authService) {
this.authService = authService;
}
#Bean("myCustomWebClient")
WebClient webClient() {
return WebClient.builder()
.defaultHeader("Access-Token", authService.getToken())
.build()
}
}
But obviously it will get the token only once at Application startup. Is there a way to inject it somehow or to intercept all of the webclient request and add the token then?
You can declare a custom WebClient with filter that is applied on each request.
#Configuration
public class CustomWebClientConfig {
private final AuthService authService;
#Autowired
public CustomWebClientConfig(AuthService authService) {
this.authService = authService;
}
#Bean("myCustomWebClient")
WebClient webClient() {
return WebClient.builder()
.filter(ExchangeFilterFunction.ofRequestProcessor(
(ClientRequest request) -> Mono.just(
ClientRequest.from(request)
.header("Access-Token", authService.getToken())
.build()
)
))
.build();
}
}
Related
I have a Web Filter that sets an object in a ThreadLocal attribute and I'm trying to understand how/when this Thread local should be cleaned-up (ThreadLocal.remove()) to avoid the exception "User context already initiated." that happens because it is being retrieved from the Spring Boot Thread Pool with the previous values set.
I'm using Spring Webflux.
Where can I hook this SecurityAuthorizationContext.clean() call?
public class SecurityAuthorizationContext
{
private static final ThreadLocal<PrivilegeHolder> userContext = new ThreadLocal<>();
private final List<String> roles;
private SecurityAuthorizationContext(List<String> roles)
{
this.roles = roles;
}
public static void create(List<String> roles)
{
if (nonNull(userContext.get()))
{
log.error("User context already initiated.");
throw new AuthorizationException("User context already initiated.");
}
PrivilegeHolder privilegeHolder = new PrivilegeHolder();
userContext.set(privilegeHolder);
// example of privileges retrieved from database by the user roles
privilegeHolder.add(INSERT);
privilegeHolder.add(DELETE);
}
public static void clean()
{
userContext.remove();
}
public static boolean hasInsertPrivilege()
{
return userContext.get().hasPrivilege(INSERT);
}
public static boolean hasDeletePrivilege()
{
return userContext.get().hasPrivilege(DELETE);
}
}
public class AuthorizationFilter implements OrderedWebFilter
{
private static final String USER_ROLES = "user-roles";
#Override
public int getOrder()
{
return SecurityWebFiltersOrder.AUTHORIZATION.getOrder();
}
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain)
{
ServerHttpRequest request = serverWebExchange.getRequest();
HttpHeaders headers = request.getHeaders();
List<String> roles = headers.get(USER_ROLES);
SecurityAuthorizationContext.create(roles);
return webFilterChain.filter(serverWebExchange);
}
}
#Configuration
#EnableWebFluxSecurity
#EnableTransactionManagement
public class ApplicationConfiguration
{
#Autowired
private AuthorizationFilter authorizationFilter;
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
{
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/**").permitAll()
.and()
.addFilterAt(authorizationFilter, AUTHORIZATION)
.build();
}
}
UPDATE: Long story short ... I just want to extract something from request headers and make it available to all the stack without passing it as parameter.
So, better to use reactor context instead of ThreadLocal, here you can read about: https://projectreactor.io/docs/core/release/reference/#context
I am trying to use an ApiClient generated by swagger-codegen-maven-plugin (Version 3.0.0), to consume an OAuth2 secured REST API from within my spring boot application. The auth server (keycloak) provides a JWT and refresh token, but I cannot figure out how to best handle tokens in my bean.
At the moment my bean looks like this:
#Configuration
public class SomeApiClientConfiguration {
#Bean
public SomeApi someApi() {
return new SomeApi(apiClient());
}
#Bean
public ApiClient apiClient() {
ApiClient apiClient = new ApiClient();
OAuth oAuth = (OAuth) apiClient.getAuthentication("auth");
oAuth.setAccessToken("");
return apiClient;
}
}
Question is: What is the best approach for getting the token and handling the refresh token?
EDIT: In order to get the token I want to use client ID, username, and password. Grant type: Password Credentials.
Best,
Marc
I was able to solve this problem and want to share the solution for future reference:
This is my SomeApiClientConfiguration:
#Configuration
public class SomeApiClientConfiguration{
#Value("${app.api.url}")
private String apiURL;
#Bean
public SomeApi someApi(OAuth2RestTemplate restTemplate) {
return new SomeApi(apiClient(restTemplate));
}
#Bean
public ApiClient apiClient(OAuth2RestTemplate restTemplate) {
var apiClient = new ApiClient(restTemplate);
apiClient.setBasePath(apiURL);
return apiClient;
}
}
Additionally I needed a SomeApiOAuth2Config class, which look as follows:
#Configuration
#EnableOAuth2Client
public class SomeApiOAuth2Config {
#Value("${app.api.client-id}")
private String clientId;
#Value("${app.api.token-endpoint}")
private String accessTokenUri;
#Value("${app.api.name}")
private String username;
#Value("${app.api.password}")
private String password;
#Bean
public ClientHttpRequestFactory httpRequestFactory() {
return new HttpComponentsClientHttpRequestFactory(httpClient());
}
#Bean
public HttpClient httpClient() {
var connectionManager = new PoolingHttpClientConnectionManager();
var maxPoolSize = 1;
connectionManager.setMaxTotal(maxPoolSize);
// This client is for internal connections so only one route is expected
connectionManager.setDefaultMaxPerRoute(maxPoolSize);
return HttpClientBuilder.create().setConnectionManager(connectionManager).build();
}
#Bean
public OAuth2ProtectedResourceDetails oauth2ProtectedResourceDetails() {
var details = new ResourceOwnerPasswordResourceDetails();
var resourceId = "";
details.setId(resourceId);
details.setClientId(clientId);
var clientSecret = "";
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
return details;
}
#Bean
public AccessTokenProvider accessTokenProvider() {
var tokenProvider = new ResourceOwnerPasswordAccessTokenProvider();
tokenProvider.setRequestFactory(httpRequestFactory());
return new AccessTokenProviderChain(
Collections.<AccessTokenProvider>singletonList(tokenProvider)
);
}
#Bean
#Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public OAuth2RestTemplate restTemplate(#Qualifier("oauth2ClientContext") OAuth2ClientContext oauth2ClientContext) {
var template = new OAuth2RestTemplate(oauth2ProtectedResourceDetails(), oauth2ClientContext);
template.setRequestFactory(httpRequestFactory());
template.setAccessTokenProvider(accessTokenProvider());
template.getOAuth2ClientContext().getAccessTokenRequest().set("username", username);
template.getOAuth2ClientContext().getAccessTokenRequest().set("password", password);
return template;
}
}
I am trying to follow the best practise of autowiring Webclient using WebClient Builder but little confused.
Here is my Main Application in which i am producing a Webclient Builder and autowiring it in one of my service class
#SpringBootApplication
public class MyApplication {
#Bean
public WebClient.Builder getWebClientBuilder() {
return WebClient.builder();
}
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}}
ServiceImpl Class
public class MyServiceImpl implements MyService {
private static final String API_MIME_TYPE = "application/json";
private static final String API_BASE_URL = "http://localhost:8080";
private static final String USER_AGENT = "Spring 5 WebClient";
private static final Logger logger = LoggerFactory.getLogger(MyServiceImpl.class);
#Autowired
private WebClient.Builder webClientBuilder;
#Override
public Mono<Issue> createIssue(Fields field) {
return webClientBuilder.build()
.post()
.uri("/rest/api/")
.body(Mono.just(field), Fields.class)
.retrieve()
.bodyToMono(Issue.class);
}}
I am trying to build the webClientBuilder with BaseURl, DefaultHeader etc. I tried to initialize it inside MyServiceImpl Constructer but not sure if its correct or not.
public MyServiceImpl() {
this.webClientBuilder
.baseUrl(API_BASE_URL).defaultHeader(HttpHeaders.CONTENT_TYPE, API_MIME_TYPE)
.defaultHeader(HttpHeaders.USER_AGENT, USER_AGENT)
.build();
}
Am i doing it correct or is there a better way to do it.
Currently I have 2 ServiceImpls to call Different Apis and thats the reason i tried to set the 'baseurl' and other defaults in service itself.
Please Help. TIA
Usually, your approach would be something like this:
#SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
#Configuration
public class MyApplicationConfiguration {
#Bean
public WebClient myWebClient(WebClient.Builder webClientBuilder) {
return webClientBuilder
.baseUrl(API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, API_MIME_TYPE)
.defaultHeader(HttpHeaders.USER_AGENT, USER_AGENT)
.build();
}
}
#Service
public class MySericeImpl implements MyService {
#Autowired
private WebClient myWebClient;
#Override
public Mono<Issue> createIssue(Fields field) {
return myWebClient
.post()
.uri("/rest/api/")
.body(Mono.just(field), Fields.class)
.retrieve()
.bodyToMono(Issue.class);
}
}
The key thing to remember is that WebClient.Builder is already pre-configured for you and Bean is already created. So you just need to autowire it, adjust the configuration and build final WebClient.
It is also possible to use another approach to configure it. There are 3 main approaches to customize WebClient. See official docs for more details https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-webclient.
Edit for consuming more APIs - configure multiple WebClients and autowire them in an appropriate service class.
#Configuration
public class MyApplicationConfiguration {
#Bean
public WebClient myWebClientForApi1(WebClient.Builder webClientBuilder) {
return webClientBuilder
.clone()
.baseUrl(API_1_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, API_MIME_TYPE)
.defaultHeader(HttpHeaders.USER_AGENT, USER_AGENT)
.build();
}
#Bean
public WebClient myWebClientForApi2(WebClient.Builder webClientBuilder) {
return webClientBuilder
.clone()
.baseUrl(API_2_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, API_MIME_TYPE)
.build();
}
}
I am using Feign in my application to make an HTTP call. I have provided an ErrorDecoder class as well but when the request times out, the flow doesn't reach my ErrorDecoder class. What am I missing here? What I would like to do is throw a custom error from the errordecoder class and then catch it in GlobalExceptionHandler and return a 408 status code
FeignClient
#FeignClient(name="finacle-service" ,url = "${feign.client.url.finacleUrl}", configuration = FinacleProxyConfig.class)
public interface FinacleProxy {
#PostMapping
String getCif(#RequestBody String request);
#PostMapping
String updateFinaclePns(#RequestBody String request);
}
FeignConfigclass
public class FinacleProxyConfig {
#Bean
public static Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
#Bean
public static Request.Options requestOptions() {
return new Request.Options(3000, 3000);
}
#Bean
public static FinacleErrorDecoder errorDecoder(){
return new FinacleErrorDecoder();
}
}
application.properties
spring.application.name = bds-intgeration-services
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
spring.cloud.services.registrationMethod=direct
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
feign.httpclient.disableSslValidation=true
feign.httpclient.enabled=false
feign.okhttp.enabled=true
feign.hystrix.enabled = false
Custom Error Decoder class
public class FinacleErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
#Override
public Exception decode(String methodkey, Response response) {
if(response.status() == 408){
return FinacleTimedOutException.builder().message("Request timed out to finacle").build();
}
return defaultErrorDecoder.decode(methodkey, response);
}
}
I am using a #MockBean annotation in my test class for a WebClient.Builder instance and it is leading to a NullPointerException when I try to set SSL context.
I am not sure why I did not see an error when I was not trying to set the context and simply calling build() api as in v1 show below:
Service class v1:
#Service
public class ABCD {
private static final Logger logger = LoggerFactory.getLogger(ABCD.class);
private String apiUrl;
private final WebClient webClient;
private final XYZRepository repository;
public ABCD(WebClient.Builder webClientBuilder,
XYZRepository repository, #Value("${api-root-url}") String apiUrl) {
//------------------------
this.webClient = webClientBuilder.build();
//------------------------
this.repository = repository;
this.apiUrl = apiUrl;
}
}
Service class v2:
#Service
public class ABCD {
private static final Logger logger = LoggerFactory.getLogger(ABCD.class);
private String apiUrl;
private final WebClient webClient;
private final XYZRepository repository;
public ABCD(WebClient.Builder webClientBuilder,
XYZRepository repository, #Value("${api-root-url}") String apiUrl) throws SSLException {
SslContext sslContext = SslContextBuilder
.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
ClientHttpConnector httpConnector = new ReactorClientHttpConnector(options -> options.sslContext(sslContext));
//------------------------
this.webClient = webClientBuilder
.clientConnector(httpConnector)
.build();
//------------------------
this.repository = repository;
this.apiUrl = apiUrl;
}
}
Test Class:
#RunWith(SpringRunner.class)
#WebFluxTest(ABCD.class)
public class ABCDTest {
#MockBean
XYZRepository repository;
#MockBean
WebClient.Builder webClientBuilder;
#SpyBean
ABCD ABCDService;
WebClient webClient;
#Value("${api-root-url}")
String apiRootUrl;
#Before
public void setup() {
this.objMapper = new ObjectMapper();
this.mockWebServer = new MockWebServer();
String baseUrl = this.mockWebServer.url("/").toString();
this.webClient = WebClient.create(baseUrl);
MockitoAnnotations.initMocks(this);
ReflectionTestUtils.setField(ABCDService,
"apiRootUrl", API_ROOT_URL);
ReflectionTestUtils.setField(ABCDService,
"webClient", this.webClient);
}
}
Here in v2
this.webClient = webClientBuilder
.clientConnector(httpConnector)
.build();
causes an NPE on build() call. How can I mock webclient without an causing NPE?
I tried, adding following code in setup() method to mock the clientConnector method's response:
when(this.webClientBuilder.clientConnector(any()))
.thenReturn(this.webClientBuilder);
So, I am curious where this code can be added to use the mocking above.
I reckon the webClientBuilder has been created OK as a mock (it is not-null - yes?), but you haven't told that mock what to do when the clientConnector method is called on it. Mockito's default behaviour is therefore to return null - hence your NPE when you call build() on that response.
So you'll need the webClientBuilder to return itself from the clientConnector method, and to return the webClient from the build(). Do this by adding the following lines :
when(webClientBuilder.clientConnector(any(ClientHttpConnector.class))).thenReturn(webClientBuilder);
when(webClientBuilder.build()).thenReturn(webClient);