how to write WebFlux filter which needs to pass Rest body for authorization service call - spring-boot

I need to implement an authorization filter for WebFlux spring-boot application. To invoke authorization service I need to get url, queryParam, headers and body.
I can get first 3 properties from ServerRequest of the HandlerFilterFunction, however I have problem with body. The authorization call requires all four parameters:
Principal authorization(String method,Sting requestUrl,Map<String, ListValue> headersMap, String body) throws ServiceException
The method throws exception if user is not authorized to execute the method otherwise returns Principal object.
The method is never called when filter is executed because I don't have any subscriber to mono object:
public class WebFluxAuthorizationFilter implements HandlerFilterFunction<ServerResponse, ServerResponse> {
#Override
public Mono<ServerResponse> filter(ServerRequest request, HandlerFunction<ServerResponse> handlerFunction) {
String method = request.methodName();
String requestUrl = request.uri().toString();
HttpHeaders headers = request.headers().asHttpHeaders();
Map<String, ListValue> headersMap = new HashMap<>();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
ListValue grpcList = toListValue(entry.getValue());
headersMap.put(entry.getKey(), grpcList);
}
Mono<Principal> mono = request.bodyToMono(String.class)
.flatMap(body -> {
return Mono.just(authorization(method, requestUrl, headersMap, body));
});
return handlerFunction.handle(request);
}
I have the same issue if I use WebFilter instead of HandlerFilterFunction
What should I do to get access to body and run authorization method shown above ?
The current code does not invoke authorization() because lambda body -> ... is never called.
Please let me know how filter should be written to get the body and use during filter execution .

Related

Extract Mono nonblocking response and store it in a variable and use it globally

In my project I have a requirement where I need to call a third party api authentic url to get the the access token. I need to set that access token in every subsequent request header .The access token has some lifetime and when the lifetime expired I need to regenerate the access token.
application.yml
I have hardcoded the client_id,client_secret,auth_url and grant_type .
AuthController.java
here I have created an endpoint to generate the access token.
**`AuthService.java`**
#Services
#Slf4j
public class AuthService{
#Autowired
private WebClient webClient;
static String accessToken="";
public Mono<SeekResponse> getAccessToken(AuthRequest authRequest) throws InvalidTokenException{
Mono<AuthResponse> authResponse=webClient.post()
.bodyValue(authRequest)
.accept(MediaType.APPLICATION_JSON)
.retrive()
.bodyToMono(AuthResponse.class);
authResponse.doOnNext(response->{
String value=response.getAccess_token();
accessToken=accessToken+value;
})
}
}
Although I have updated the "accessToken" value but it will return me null. I understand as I have made async call this value coming as null. I can't use blocking mechanism here.
Is there any other way to generate the access token and pass it as a header for the subsequent request for authentication. Or how can I use the accessToken value globally so that I can set those token value to my subsequent api request call.
I have tried with oAuth2 by following the below article:
https://medium.com/#asce4s/oauth2-with-spring-webclient-761d16f89cdd
But when I execute I am getting the below error :
"An expected CSRF token cannot found".
I'm also learning Webflux. Here's my thought. Please correct me if I'm wrong.
We are not going to rely on doOnNext() nor doOnSuccess() nor other similar method to try to work on an pre-defined variable accessToken (That's not a way to let Mono flow). What we should focus on is converting a mono to another mono, for example converting mono response to mono access token.
The way to do that is .flatmap()/.map()/.zipwith()/...
For example,
Mono<string> tokenMono = responseMono.flatmap(
// in the map or flatmap, we get the chance to operate on variables/objects.
resp -> {
string token = response.getAccess_token();
return Mono.just(token); // with Mono.just(), we are able to convert object to Mono again.
}
) // this example is not practical, as map() is better to do the same thing. flatmap with Mono.just() is meaningless here.
Mono<string> tokenMono2 = responseMono.map(
resp -> {
string token = response.getAccess_token();
return token;
}
)
Everything starting from Mono should be always Mono until subscribed or blocked. And they provide us ways to operate on those variables inside Mono<variables>. Those are map() flatmap(), zipwith(), etc.
https://stackoverflow.com/a/60105107/18412317
Referring to a point this author said, doOnNext() is for side effect such as logging.
It's hard to understand provided sample and implementation is not really reactive. The method returns Mono but at the same time throws InvalidTokenException or usage of onNext that is a so-called side-effect operation that should be used for logging, metrics, or other similar use cases.
The way you implement oauth flow for WebClient is to create filter, Client Filters.
Spring Security provides some boilerplates for common oauth flows. Check Spring Security OAuth2 for more details.
Here is an example of simple implementation of the client credential provider
private ServerOAuth2AuthorizedClientExchangeFilterFunction oauth(String clientRegistrationId, ClientConfig config) {
var clientRegistration = ClientRegistration
.withRegistrationId(clientRegistrationId)
.tokenUri(config.getAuthUrl() + "/token")
.clientId(config.getClientId())
.clientSecret(config.getClientSecret())
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
var authRepository = new InMemoryReactiveClientRegistrationRepository(clientRegistration);
var authClientService = new InMemoryReactiveOAuth2AuthorizedClientService(authRepository);
var authClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
authRepository, authClientService);
var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authClientManager);
oauth.setDefaultClientRegistrationId(clientRegistrationId);
return oauth;
}
then you could use it in the WebClient
WebClient.builder()
.filter(oauth)
.build()
UPDATE
Here is an example of the alternative method without filters
AuthService
#Service
public class AuthService {
private final WebClient webClient;
public AuthService() {
this.webClient = WebClient.create("<url>/token");
}
public Mono<String> getAccessToken() {
return webClient.post()
.bodyValue()
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(AuthResponse.class)
.map(res -> res.getAccessToken());
}
}
ApiService
#Service
public class ApiService {
private final WebClient webClient;
private final Mono<String> requestToken;
public ApiService(AuthService authService) {
this.webClient = WebClient.create("<url>/api");
// cache for token expiration
this.requestToken = authService.getAccessToken().cache(Duration.ofMinutes(10));
}
public Mono<String> request() {
return requestToken
.flatMap(token ->
webClient.get()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
);
}
}

How to capture Http verb and api end point using AOP in Spring Boot application

I am planning to implement an aspect in order to capture the following values for a given rest API on successful return, in my spring boot application:
api endpoint i.e. like /api/ ...
Http verb. i.e. PUT/POST etc
Request payload and request/query param
I am doing this as follows:
#Aspect
public class MyAspect {
private final Logger log = LoggerFactory.getLogger(this.getClass());
#Pointcut("within(com.web.rest.*)")
public void applicationResourcePointcut() {
}
#AfterReturning(value = ("applicationResourcePointcut()"),
returning = "returnValue")
public void endpointAfterReturning(JoinPoint p, Object returnValue) throws Throwable {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
System.out.println("REQUEST PAYLOAD = " + mapper.writeValueAsString(p.getArgs()));
System.out.println("METHOD NAME = " + p.getSignature().getName());
System.out.println("RESPONSE OBJECT = " + mapper.writeValueAsString(returnValue));
//CAN NOT UNDERSTAND HOW TO CAPTURE HTTP VERB AND ENDPOINT HERE
}
}
Could anyone please help here in capturing Http verb and api end point as well ?
You have to get the request object and can get the values you required from it
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
and use methods available in HttpServletRequest
request.getParameterMap()
request.getMethod()
request.getRequestURL()

How to reject the request and send custom message if extra params present in Spring boot REST Api

#PostMapping()
public ResponseEntity<ApiResponse> createContact(
#RequestBody ContactRequest contactRequest) throws IOException {
}
How to reject the API request, if extra params present in the request, by default spring boot ignoring extra parameters.
I believe you can add an HttpServletRequest as a parameter to the controller method (createContact in this case). Then you'll get access to all the parameters that come with the requests (query params, headers, etc.):
#PostMapping
public ResponseEntity<ApiResponse> createContact(HttpServletRequest request,
#RequestBody ContactRequest contactRequest) throws IOException {
boolean isValid = ...// validate for extra parameters
if(!isValid) {
// "reject the request" as you call it...
}
}
First add an additional parameter to the method. This gives you access to information about the request. If Spring sees this parameter then it provides it.
#PostMapping()
public ResponseEntity<ApiResponse> createContact(
#RequestBody ContactRequest contactRequest,
WebRequest webRequest) throws IOException {
if (reportUnknownParameters(webRequest) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
I do something like this to get the bad request into the log.
private boolean reportUnknownParameters(WebRequest webRequest) {
LongAdder unknownCount = new LongAdder();
webRequest.getParameterMap().keySet().stream()
.filter(key -> !knownParameters.contains(key))
.forEach(key -> {
unknownCount.increment();
log.trace("unknown request parameter \"{}\"=\"{}\"", key, webRequest.getParameter(key));});
return unknownCount.longValue() > 0;
}
add #RequestParam annotation in your methods parameter list and add it as a map, then you can access for it's key list and check if it contains anything else other than your required params.
public ResponseEntity<ApiResponse> createContact(#RequestParam Map<String,String> requestParams, #RequestBody ContactRequest contactRequest) throws IOException {
//Check for requestParams maps keyList and then pass accordingly.
}

Limiting Access to Endpoints by Method in Spring Boot

We are implementing a simple health check API our load balancers can call to help with the routing of requests. If the status of the application is "standby", requests should not be sent to it. Only the admins can set the state to "up" or "standby", but anyone (including the load balancers) can get the status of the application.
We are trying this with Spring Boot 2, but are having problems configuring security to grant anonymous access to just one of the routes. Consider the following controller:
#RestController
public class AppStatusController {
private static final String STATUS = "status";
String state = "standby";
private String getState() {
return state;
}
private Map<String, String> getStatusMap() {
Map<String, String> retval = new HashMap<>();
retval.put(STATUS, getState());
return retval;
}
// GET calls are public, all others require AuthN & AuthZ
#GetMapping(path = "/appstatus", produces = "application/json")
public Map<String, String> getStatus() {
return getStatusMap();
}
// Only those with the ADMIN role can POST to this endpoint
#PostMapping(path = "/appstatus", consumes = "application/json", produces = "application/json")
public Map<String, String> setStatus(#RequestBody Map state) {
// Validate and update the state
return getStatusMap();
}
}
There is only one endpoint, /appstatus, but one method is called with an HTTP GET and the other with an HTTP POST. We want calls to getStatus to be public, but allow Spring Security to control access to setStatus. One might expect an annotation such as #Anonymous or something similar to be applied to the getStatus() method but we can't seem to find one.
Some have suggested using a separate #Configuration class and setting up antMatchers but it's not clear how we can match on the HTTP method.
Does anyone have suggestions on how to configure Spring Security to allow public access to GET method requests but control access to other methods?
EDIT: We are trying to avoid any authentication on the getStatus() call. We can't store auth credentials in the health check probe and can't perform a login exchange. This is a simple GET request to see if the application is up and ready for operation.
Have you tried using Method Security Expressions?
It looks like this will do what you want:
// GET calls are public, all others require AuthN & AuthZ
#GetMapping(path = "/appstatus", produces = "application/json")
#PreAuthorize("permitAll")
public Map<String, String> getStatus() {
return getStatusMap();
}
// Only those with the ADMIN role can POST to this endpoint
#PostMapping(path = "/appstatus", consumes = "application/json", produces = "application/json")
#PreAuthorize("hasRole('ADMIN')")
public Map<String, String> setStatus(#RequestBody Map state) {
// Validate and update the state
return getStatusMap();
}
Note: I don't know what roles your admins have, so I used 'ADMIN' as a placeholder.

415 Unsupported Media Type, when NOT sending an optional request body with POST request

I have a REST controller that defines an interface which takes an optional request body.
#RestController
#RequestMapping(ExampleRest.EXAMPLE_URI)
public class ExampleRest {
public static final String EXAMPLE_URI = "/examples";
#RequestMapping(value = "/search", method = POST)
public Page<ExampleDto> search(#RequestBody(required = false) Searchable searchable, Pageable pageable) {
return exampleService.findAll(searchable, pageable);
}
}
The Searchable object contains information to create a JPASpecification. It's pretty much a dto. I would like to make this searchable optional. I understood that #RequestBody(required = false) should do the trick.
I have the following test, where I want to test a request without any request body.
#Test
public void post_NoCriteria_Ok() {
RequestEntity requestEntity = new RequestEntity(HttpMethod.POST, URI.create(ExampleRest.EXAMPLE_URI + "/search"));
ResponseEntity <RestResponsePage<ExampleDto>> response = restTemplate.exchange(requestEntity, new ParameterizedTypeReference <RestResponsePage<ExampleDto>> () {});
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
}
If I run this test, it keeps failing with this response from the RestController:
<415 Unsupported Media Type,Page 1 of 1 containing UNKNOWN
instances,{Content-Type=[application/json;charset=UTF-8],
Transfer-Encoding=[chunked], Date=[Wed, 13 Sep 2017 10:10:22 GMT]}>
The Code execution does not even enter search method implementation inside of the RestController.
As soon I provide an empty Searchable for the test, it runs through.
Is the implementation of #RequestBody(required = false) buggy, or what am I doing wrong here?
You need to set Content-Type as "application/json" in your request while sending from #Test file.

Resources