Spring Webflux: Read request-body in ServerWebExchangeMatcher - spring-boot

I have to create an application with just one endpoint where users can log in and do their other operational stuff. Users should be able to log in with a request body like:
{
"login": {
"token": "12345"
}
}
So every request with a body like this should be allowed via .permitAll() by the WebFilterChain.
The chain is configured like this:
#Bean
public SecurityWebFilterChain chain(ServerHttpSecurity http) {
return http.authorizeExchange(exchanges -> {
exchanges.matchers(new LoginRequestMatcher())
.permitAll()
.pathMatchers("api/v1*")
.hasRole("USER");
})
.csrf()
.disable()
.build();
}
The LoginRequestMatcher analyzes the body of the request and returns a Match if the pattern is ok. So far, this works quite fine.
My problem is that later in the Controller, the request body can not be accessed any more, so I'm currently trying to put it into a cache or the context to be able to access it later on.
Here is my current implementation of the LoginRequestMatcher:
public class LoginRequestMatcher implements ServerWebExchangeMatcher {
private static final Pattern loginRequestPattern = Pattern.compile("...loginPattern");
#Override
public Mono<MatchResult> matches(final ServerWebExchange exchange) {
return exchange.getRequest().getBody()
.map(dataBuffer -> {
byte[] byteArray=new byte[dataBuffer.readableByteCount()];
dataBuffer.read(byteArray);
DataBufferUtils.release(dataBuffer);
return byteArray;
}).defaultIfEmpty(new byte[0])
.map(bytes -> {
ServerHttpRequestDecorator decorator = initDecorator(exchange, bytes);
exchange.mutate().request(decorator).build();
Mono.just(decorator).contextWrite(ctx -> ctx.put("decorator",decorator))
return decorator;
}
)
// Rest of the code checks for Regex in the body and works fine.
.next();
Now, when I try to get the cached request-body after the RequestMatcher is processed (f.e. in a WebFilter), the context does not contain an object with the key "decorator".
#Configuration
#ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class HttpRequestBodyCachingFilter implements WebFilter {
#Override
public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
final HttpMethod method = exchange.getRequest().getMethod();
// Nothing to cache for GET and DELETE
if (method==null || HttpMethod.GET==method || HttpMethod.DELETE==method) {
return chain.filter(exchange);
}
// Get decorator from context
return Mono.just(exchange)
//ctx.get("decorator") throws Exception here, because the key does not exist.
.flatMap(s -> Mono.deferContextual(ctx -> ctx.get("decorator")))
.map(decorator ->
(ServerHttpRequestDecorator) decorator)
.flatMap(decorator -> chain.filter(exchange.mutate().request(decorator).build()));
}
}
How can I make the body readable after accessing it in inside a ServerWebExchangeMatcher?

Related

Spring Boot WebFlux with RouterFunction log request and response body

I'm trying to log request and response body of every call received by my microservice which is using reactive WebFlux with routing function defined as bellow:
#Configuration
public class FluxRouter {
#Autowired
LoggingHandlerFilterFunction loggingHandlerFilterFunction;
#Bean
public RouterFunction<ServerResponse> routes(FluxHandler fluxHandler) {
return RouterFunctions
.route(POST("/post-flux").and(accept(MediaType.APPLICATION_JSON)), fluxHandler::testFlux)
.filter(loggingHandlerFilterFunction);
}
}
#Component
public class FluxHandler {
private static final Logger logger = LoggerFactory.getLogger(FluxHandler.class);
#LogExecutionTime(ActionType.APP_LOGIC)
public Mono<ServerResponse> testFlux(ServerRequest request) {
logger.info("FluxHandler.testFlux");
return request
.bodyToMono(TestBody.class)
.doOnNext(body -> logger.info("FluxHandler-2: "+ body.getName()))
.flatMap(testBody -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue("Hello, ")));
}
}
and a router filter defined like this:
#Component
public class LoggingHandlerFilterFunction implements HandlerFilterFunction<ServerResponse, ServerResponse> {
private static final Logger logger = LoggerFactory.getLogger(LoggingHandlerFilterFunction.class);
#Override
public Mono<ServerResponse> filter(ServerRequest request, HandlerFunction<ServerResponse> handlerFunction) {
logger.info("Inside LoggingHandlerFilterFunction");
return request.bodyToMono(Object.class)
.doOnNext(o -> logger.info("Request body: "+ o.toString()))
.flatMap(testBody -> handlerFunction.handle(request))
.doOnNext(serverResponse -> logger.info("Response body: "+ serverResponse.toString()));
}
}
Console logs here are:
Inside LoggingHandlerFilterFunction
LoggingHandlerFilterFunction-1: {name=Name, surname=Surname}
FluxHandler.testFlux
As you can see I'm able to log the request but after doing it the body inside the FluxHandler seems empty because this is not triggered .doOnNext(body -> logger.info("FluxHandler-2: "+ body.getName()) and also no response body is produced. Can someone show me how to fix this or is there any other way to log request/response body when using routes in webflux?

Configuring AWS Signing in Reactive Elasticsearch Configuration

In one of our service I tried to configure AWS signing in Spring data Reactive Elasticsearch configuration.
Spring provides the configuring the webclient through webclientClientConfigurer
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.usingSsl()
.withWebClientConfigurer(
webClient -> {
return webClient.mutate().filter(new AwsSigningInterceptor()).build();
})
. // ... other options to configure if required
.build();
through which we can configure to sign the requests but however AWS signing it requires url, queryparams, headers and request body(in case of POST,POST) to generate the signed headers.
Using this I created a simple exchange filter function to sign the request but in this function I was not able to access the request body and use it.
Below is the Filter function i was trying to use
#Component
public class AwsSigningInterceptor implements ExchangeFilterFunction
{
private final AwsHeaderSigner awsHeaderSigner;
public AwsSigningInterceptor(AwsHeaderSigner awsHeaderSigner)
{
this.awsHeaderSigner = awsHeaderSigner;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next)
{
Map<String, List<String>> signingHeaders = awsHeaderSigner.createSigningHeaders(request, new byte[]{}, "es", "us-west-2"); // should pass request body bytes in place of new byte[]{}
ClientRequest.Builder requestBuilder = ClientRequest.from(request);
signingHeaders.forEach((key, value) -> requestBuilder.header(key, value.toArray(new String[0])));
return next.exchange(requestBuilder.build());
}
}
I also tried to access the request body inside ExchangeFilterFunction using below approach but once i get the request body using below approach.
ClientRequest.from(newRequest.build())
.body(
(outputMessage, context) -> {
ClientHttpRequestDecorator loggingOutputMessage =
new ClientHttpRequestDecorator(outputMessage) {
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
log.info("Inside write with method");
body =
DataBufferUtils.join(body)
.map(
content -> {
// Log request body using
// 'content.toString(StandardCharsets.UTF_8)'
String requestBody =
content.toString(StandardCharsets.UTF_8);
Map<String, Object> signedHeaders =
awsSigner.getSignedHeaders(
request.url().getPath(),
request.method().name(),
multimap,
requestHeadersMap,
Optional.of(
requestBody.getBytes(StandardCharsets.UTF_8)));
log.info("Signed Headers generated:{}", signedHeaders);
signedHeaders.forEach(
(key, value) -> {
newRequest.header(key, value.toString());
});
return content;
});
log.info("Before returning the body");
return super.writeWith(body);
}
#Override
public Mono<Void>
setComplete() { // This is for requests with no body (e.g. GET).
Map<String, Object> signedHeaders =
awsSigner.getSignedHeaders(
request.url().getPath(),
request.method().name(),
multimap,
requestHeadersMap,
Optional.of("".getBytes(StandardCharsets.UTF_8)));
log.info("Signed Headers generated:{}", signedHeaders);
signedHeaders.forEach(
(key, value) -> {
newRequest.header(key, value.toString());
});
return super.setComplete();
}
};
return originalBodyInserter.insert(loggingOutputMessage, context);
})
.build();
But with above approach I was not able to change the request headers as adding headers throws UnsupportedOperationException inside writewith method.
Has anyone used the spring data reactive elastic search and configured to sign with AWS signed headers?
Any help would be highly appreciated.

Kotlin Spring Session concurrent session control on already defined Security config

This is a repost from the Github issues page.
Confused on why there are no behavior changes when adding the concurrency session control with Spring session with this documentation
Versions:
Spring Security 5.2.1.RELEASE
Spring Session-JDBC 2.2.0.RELEASE
Spring Boot 2.2.4.RELEASE
Code snippets from our code:
SecurityConfig.kt
#EnableWebSecurity
#Profile("auth")
#Configuration
class SecurityConfig<S: Session> : WebSecurityConfigurerAdapter() {
//<some-code-here>
#Autowired
private lateinit var sessionRepository: FindByIndexNameSessionRepository<S>
override fun configure(http: HttpSecurity) {
/*
TODO enforce requiring application/json content type everywhere (apparently except file upload)
*/
val publicPaths = arrayOf(
"/api/v1/centralPortal/current/forgot-password",
"/api/v1/centralPortal/current/reset-password/*",
"/api/v1/centralPortal/current/onboard/*",
"/login",
"/api/v1/public/**" // CSP reporting is there, needs CSRF disabled
)
val filter = JsonLoginFilter(objectMapper, audit, rateLimiter, userManagementService, getAuthMode(env))
filter.rateLimitPPS = rateLimitPermits / rateLimitSeconds
filter.setSessionAuthenticationStrategy(sas)
filter.setAuthenticationManager(authenticationManager())
filter.setAuthenticationSuccessHandler { request, response, _ ->
val csrfToken = request.getAttribute(CsrfToken::class.java.name) as CsrfToken
response.addHeader(csrfToken.headerName, csrfToken.token)
}
filter.setAuthenticationFailureHandler { request, response, exception ->
excHandlerAdvice.handleUnauthenticated(request, response, exception, filter.obtainRetries()) }
http
.cors().and()
.httpBasic().disable()
.formLogin().disable()
.rememberMe().disable()
.headers()
// do custom handling of the X-Frame-Options header because some pages need to be iframed
.frameOptions().disable()
.addHeaderWriter(ConfigurableFrameOptionsHeaderWriter("/assets/pdfjs/web/viewer.html", "/ws/frame.html"))
.and()
.csrf()
.ignoringAntMatchers(*publicPaths)
.and()
.authorizeRequests()
.antMatchers("/manifest.json", "/manifest.webmanifest").permitAll()
.antMatchers("/", "/index.js", "/main.js", "/vendor.js", "/service-worker.js", "/workbox-v*/workbox-sw.js", "/precache.*.js", "/*.ico", "/ui/**", "/index.html").permitAll()
.regexMatchers("^/\\w\\w/ui/.*$").permitAll()
.antMatchers("/assets/**/*.webp", "/assets/**/*.png", "/assets/**/*.jpg", "/assets/**/*.svg", "/assets/**/*.gif").permitAll()
.antMatchers("/assets/**/*.woff", "/assets/**/*.woff2", "/assets/**/*.ttf", "/assets/**/*.eot").permitAll()
.antMatchers("/assets/**/*.css").permitAll()
.antMatchers(*publicPaths).permitAll()
.antMatchers("/img/logo.png").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.accessDeniedHandler { request, response, accessDeniedException -> excHandlerAdvice.handleAccessDenied(request, response, accessDeniedException) }
.authenticationEntryPoint { request, response, authException -> excHandlerAdvice.handleUnauthenticated(request, response, authException) }
.and()
.addFilter(filter)
.logout()
.logoutSuccessHandler { request, response, auth -> Unit } // don't redirect
.and()
http.headers()
.cacheControl().disable()
.addHeaderWriter(CacheControlWriterWithWorkaround())
// Concurrency control code
http.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry())
}
private fun sessionRegistry(): SpringSessionBackedSessionRegistry<S> {
return SpringSessionBackedSessionRegistry(this.sessionRepository)
}
}
JsonLoginFilter.kt
data class UserLogin(val username: String, val password: String)
class JsonLoginFilter(private val objectMapper: ObjectMapper, val audit: AuditService?, val rateLimiter: RateLimiterAspect, val userManagementService: UserManagementService?, val authMode: AuthMode) : UsernamePasswordAuthenticationFilter() {
var rateLimitPPS: Double = 1 / 2.0 // rate limiter permits per second default value
private var _parsed: UserLogin? = null
private val log = loggerFor<JsonLoginFilter>()
fun parsedLogin(request: HttpServletRequest): UserLogin {
val body = request.inputStream
return objectMapper.readValue(body, UserLogin::class.java)
}
private fun loginAttempt(success: Boolean) {
log.info("Login attempt username=${_parsed?.username} success=$success")
if (success) {
resetRetries()
}
audit?.eventWithPrincipal(
AuditObjectCategory.USERS,
"User",
_parsed?.username,
if (success) OtherActions.USER_LOGIN else OtherActions.USER_LOGIN_FAIL,
_parsed?.username
)
}
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse?): Authentication {
try {
rateLimiter.rateLimit(request, "login", rateLimitPPS)
_parsed = parsedLogin(request)
val authResult = super.attemptAuthentication(request, response)
loginAttempt(authResult != null)
return authResult
} catch (e: RateLimiterException) {
throw e
} catch (ex: AuthenticationException) {
loginAttempt(false)
if (authMode == AuthMode.BUILTIN) userManagementService?.checkUserRetries(_parsed!!.username)
throw ex
}
}
fun obtainRetries(): Int? {
return if (authMode == AuthMode.BUILTIN) userManagementService?.getUserRetries(_parsed!!.username) else 0
}
fun resetRetries() {
if (authMode == AuthMode.BUILTIN) {
userManagementService?.resetUserRetries(_parsed!!.username)
}
}
override fun obtainPassword(request: HttpServletRequest): String {
return _parsed!!.password
}
override fun obtainUsername(request: HttpServletRequest): String {
return _parsed!!.username
}
}
spring.session.store-type=jdbc
So my expected behavior based on the code and the documentation would be when there is current login user then another user logs in into another browser, it should prevent the login with maxSessionPreventsLogin(true).
So the actual behavior on the code above is that 2 browser with the same user can login, which I didn't expect, since I followed the docs. I'm not really sure if I am missing something, as this is my first time I am encountering Spring Session and Spring Security. Please also take note that the sessions are stored in a DB rather than in-memory.
I would really love if someone can point me to the steps of the correct Spring session configuration, or maybe the documentation should have a precursor on what to configure before doing this, or what when is not an applicable approach.
Thanks

Webflux JWT Authorization not working fine

I am following a tutorial about JWT in a spring reactive context (webflux).
The token generation is working fine, however the authorization is not working when I use the Authorization with bearer
Here is what I have done:
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class WebSecurityConfig{
#Autowired private JWTReactiveAuthenticationManager authenticationManager;
#Autowired private SecurityContextRepository securityContext;
#Bean public SecurityWebFilterChain configure(ServerHttpSecurity http){
return http.exceptionHandling()
.authenticationEntryPoint((swe , e) -> {
return Mono.fromRunnable(()->{
System.out.println( "authenticationEntryPoint user trying to access unauthorized api end points : "+
swe.getRequest().getRemoteAddress()+
" in "+swe.getRequest().getPath());
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(()->{
System.out.println( "accessDeniedHandler user trying to access unauthorized api end points : "+
swe.getPrincipal().block().getName()+
" in "+swe.getRequest().getPath());
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
})
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContext)
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers("/auth/login").permitAll()
.anyExchange().authenticated()
.and()
.build();
}
As you can see, I want to simply deny all not authorized requests other than login or options based ones.
The login is working fine and I'm getting a token.
But trying to logout (a tweak that I implemented my self to make it state-full since I m only learning) is not working.
Here is my logout controller:
#RestController
#RequestMapping(AuthController.AUTH)
public class AuthController {
static final String AUTH = "/auth";
#Autowired
private AuthenticationService authService;
#PostMapping("/login")
public Mono<ResponseEntity<?>> login(#RequestBody AuthRequestParam arp) {
String username = arp.getUsername();
String password = arp.getPassword();
return authService.authenticate(username, password);
}
#PostMapping("/logout")
public Mono<ResponseEntity<?>> logout(#RequestBody LogoutRequestParam lrp) {
String token = lrp.getToken();
return authService.logout(token);
}
}
The logout request is as below:
As stated in images above, I believe that I m doing fine, however I m getting the error log message:
authenticationEntryPoint user trying to access unauthorized api end points : /127.0.0.1:45776 in /auth/logout
Here is my security context content:
/**
* we use this class to handle the bearer token extraction
* and pass it to the JWTReactiveAuthentication manager so in the end
* we produce
*
* simply said we extract the authorization we authenticate and
* depending on our implementation we produce a security context
*/
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
#Autowired
private JWTReactiveAuthenticationManager authenticationManager;
#Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
String authorizationHeaderContent = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if( authorizationHeaderContent !=null && !authorizationHeaderContent.isEmpty() && authorizationHeaderContent.startsWith("Bearer ")){
String token = authorizationHeaderContent.substring(7);
Authentication authentication = new UsernamePasswordAuthenticationToken(token, token);
return this.authenticationManager.authenticate(authentication).map((auth) -> {
return new SecurityContextImpl(auth);
});
}
return Mono.empty();
}
#Override
public Mono<Void> save(ServerWebExchange arg0, SecurityContext arg1) {
throw new UnsupportedOperationException("Not supported yet.");
}
}
I'm unable to see or find any issue or error that I have made. Where is the mistake?
There's a difference in writing
//Wrong
Jwts.builder()
.setSubject(username)
.setClaims(claims)
and
//Correct
Jwts.builder()
.setClaims(claims)
.setSubject(username)
Indeed, look at setSubject method in the DefaultJwtBuilder class :
#Override
public JwtBuilder setSubject(String sub) {
if (Strings.hasText(sub)) {
ensureClaims().setSubject(sub);
} else {
if (this.claims != null) {
claims.setSubject(sub);
}
}
return this;
}
When setSubject(username) is called first, ensureClaims() creates a DefaultClaims without yours and if you call setClaims(claims) the precedent subject is lost ! This JWT builder is bogus.
Otherwise, you're importing the wrong Role class in JWTReactiveAuthenticationManager, you have to replace :
import org.springframework.context.support.BeanDefinitionDsl.Role;
by
import com.bridjitlearning.www.jwt.tutorial.domain.Role;
Last and not least, validateToken() will return always false because of the check(token). put call is coming too late, you have to be aware of that. Either you remove this check or you move the put execution before calling the check method.
I'am not sure about what you want to do with resignTokenMemory, so i'll let you fix it by your own:
public Boolean validateToken(String token) {
return !isTokenExpired(token) && resignTokenMemory.check(token);
}
Another thing, your token is valid only 28,8 second, for testing raison i recommend you to expiraiton * 1000.

Why is my RestTemplate ClientHttpRequestInterceptor not called?

I want to use interceptor to add authorization header to every request made via rest template. I am doing it like this:
public FirebaseCloudMessagingRestTemplate(#Autowired RestTemplateBuilder builder, #Value("fcm.server-key") String serverKey) {
builder.additionalInterceptors(new ClientHttpRequestInterceptor() {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().add("Authorization", "key=" + serverKey);
System.out.println(request.getHeaders());
return execution.execute(request, body);
}
});
this.restTemplate = builder.build();
}
However when I do this
DownstreamHttpMessageResponse response = restTemplate.postForObject(SEND_ENDPOINT, request, DownstreamHttpMessageResponse.class);
Interceptor is not called (Iv put breakpoint in it and it did not fire). Request is made and obvious missing auth key response is returned. Why is my interceptor not called?
Ok I know whats happening. After checking build() implementation I discovered that RestTemplateBuilder is not changing self state when calling additionalInterceptors but returns a new builder with given interceptors. Chaining calls solves the issue.
public FirebaseCloudMessagingRestTemplate(final #Autowired RestTemplateBuilder builder, final #Value("${fcm.server-key}") String serverKey) {
this.restTemplate = builder.additionalInterceptors((request, body, execution) -> {
request.getHeaders().add("Authorization", "key=" + serverKey);
log.debug("Adding authorization header");
return execution.execute(request, body);
}).build();
}

Resources