SessionLocaleResolver equivalent in Spring WebFlux - resolve locale through session data - spring

If we need to resolve the locale value from session data, how can we implement this in a reactive way in Spring WebFlux?
For non-reactive web application, SessionLocaleResolver is available to resolve the locale from session data.
For Spring WebFlux, the way to intercept the locale resolver is using LocaleContextResolver. The default implementation provided in Spring Boot WebFlux starter is AcceptHeaderLocaleContextResolver. The problem with this interface is that it only provides sync method resolveLocaleContext(ServerWebExchange). If I need to resolve the locale from request query parameters or the Accept-Language headers, it works perfectly, because ServerWebExchange provides direct methods to access the request object, which in turn provides methods to access parameters and headers directly.
override fun resolveLocaleContext(exchange: ServerWebExchange): LocaleContext {
val langParams = exchange.request.queryParams["lang"]
var targetLocale: Locale? = null
if (langParams != null && langParams.isNotEmpty()) {
for (lang in langParams) {
val locale = Locale.forLanguageTag(lang)
if (locale != null) {
targetLocale = locale
break
}
}
}
return SimpleLocaleContext(targetLocale ?: Locale.forLanguageTag("en-US"))
}
However, if I need to resolve the locale from session data, there is a problem. ServerWebExchange#getSession() returns Mono<WebSession>. I am not able to get session data in a sync method unless I call Mono#block(). However, this is not an ideal, reactive way.
Is there any way to resolve this? Or are there any plan in Spring to change this?
(There was a similar question previously which got buried: Spring WebFlux - SessionLocaleResolver)

Related

Spring Security 6.0 CsrfToken behavior change

I tested Spring Security as part of my Spring Boot Setup in version 6.0-M5, 6.0-RC1 and 6.0-RC2. I recognized a behavior change and wanted to ask whether this may be a bug. I return the CSRF token as a serialized JSON, but since RC1 the content of the token in the JSON is garbage.
My working code in Spring Boot 6 Milestone 5 still working as expected.
#RestController
public class CsrfController {
#GetMapping("/rest/user/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}
In my use case I query the controller using a unit test.
#LocalServerPort
int serverPort;
#Autowired
private TestRestTemplate webclient;
#Test
public void getCsrf() {
ResponseEntity<String> entity = webclient.getForEntity("http://localhost:" + serverPort +
"/rest/user/csrf", String.class);
// ... here some code to get the token from the JSON body ...
assertTrue(result.matches("^[a-f0-9\\-]+$"));
This is the first query of the server. A session object between client and server is not established in past queries. This worked in M5 but stopped working in Spring Boot 6 RC1 and RC2
The following controller code made it work again in RC2:
#GetMapping("/rest/user/csrf")
public CsrfToken csrf(HttpServletRequest request, HttpServletResponse response) {
CsrfToken repoToken = tokenRepo.loadToken(request);
if (repoToken != null) {
return repoToken;
}
// required because it is required but ay not be initialized by the tokenRepo
request.getSession();
repoToken = tokenRepo.generateToken(request);
tokenRepo.saveToken(repoToken, request, response);
return repoToken;
}
If I tried the old code in RC2, I received on client side a malformed string. I did not receive a UUID styled token in my JSON serialized response body. I think it is related to the uninitialized session object.
Is this a bug or is an uninitialized session and a resulting not working CrsfToken specified behavior?
I think the issue is in the way I try to get and use the XSFR token.
Because I want to use an Angular frontend, I configured my token repository to provide the tokens via Cookie.
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
This produces cookies the old UUID style. However the authentication expects the new tokens as generated by https://github.com/spring-projects/spring-security/issues/11960 . Probably the cookie mechanism still needs to be migrated until final Spring Boot 3.0.

How do you get the Request URL in spring Boot from an AuthorizationFailureEvent

We are using Spring Boot 2.5.2 with Keycloak 14.0. I am trying to log the authorization events in addition to the URL the user called.
I am trying to follow Spring Boot Authentication Auditing Support. The code to retrieve the Request URL is:
private void onAuthorizationFailureEvent(
AuthorizationFailureEvent event) {
Map<String, Object> data = new HashMap<>();
data.put(
"type", event.getAccessDeniedException().getClass().getName());
data.put("message", event.getAccessDeniedException().getMessage());
data.put(
"requestUrl", ((FilterInvocation)event.getSource()).getRequestUrl() );
if (event.getAuthentication().getDetails() != null) {
data.put("details",
event.getAuthentication().getDetails());
}
publish(new AuditEvent(event.getAuthentication().getName(),
AUTHORIZATION_FAILURE, data));
}
When I attempt this I am getting a ClassCastException when calling event.getSource(). The source seems to be a ReflectiveMethodInvocation (to my Controller) and not a FilterInvocation. Can anybody explain this? How do I get the request url?

Spring WebClient header from ReactiveSecurityContext

I'm trying to set WebClient header value accordingly to the authenticated user, something like this:
webClient.post().header(HttpHeaders.AUTHORIZATION, getUserIdFromSession())...
and
public String getUserIdFromSession() {
Mono<Authentication> authentication = ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication);
//do something here to get user credentials and return them
}
Should I store the required header value somewhere else? Because in reactive way, everything returns a Mono/Flux and I'm currently unable to use the authenticated user data as I have used it with Spring MVC. There I could just do SecurityContextHolder.getContext().getAuthentication()
Is there a reason you aren't just flatmapping the Authentication and then calling the webclient? You could also just return the Mono<String> from your method
ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication)
.flatMap(auth -> webClient.post().header(HttpHeaders.AUTHORIZATION, auth.getUserId()).exchange();

Cookies path with Spring Cloud Gateway

Consider this microservices based application using Spring Boot 2.1.2 and Spring Cloud Greenwich.RELEASE:
Each microservice uses the JSESSIONID cookie to identify its own dedicated Servlet session (i.e. no global unique session shared with Spring Session and Redis).
External incoming requests are routed by Spring Cloud Gateway (and an Eureka registry used through Spring Cloud Netflix, but this should not be relevant).
When Spring Cloud Gateway returns a microservice response, it returns the "Set-Cookie" as-is, i.e. with the same "/" path.
When a second microservice is called by a client, the JSESSIONID from the first microservice is forwarded but ignored (since the corresponding session only exists in the first microservice). So the second microservice will return a new JSESSIONID. As a consequence the first session is lost.
In summary, each call to a different microservice will loose the previous session.
I expected some cookies path translation with Spring Cloud Gateway, but found no such feature in the docs. Not luck either with Google.
How can we fix this (a configuration parameter I could have missed, an
API to write such cookies path translation, etc)?
Rather than changing the JSESSIONID cookies path in a GlobalFilter, I simply changed the name of the cookie in the application.yml:
# Each microservice uses its own session cookie name to prevent conflicts
server.servlet.session.cookie.name: JSESSIONID_${spring.application.name}
I faced the same problem and found the following solution using Spring Boot 2.5.4 and Spring Cloud Gateway 2020.0.3:
To be independent from the Cookie naming of the downstream services, I decided to rename all cookies on the way through the gateway. But to avoid a duplicate session cookie in downstream requests (from the gateway itself) I also renamed the gateway cookie.
Rename the Gateway Session Cookie
Unfortunately customizing the gateway cookie name using server.servlet.session.cookie.name does not work using current gateway versions.
Therefore register a custom WebSessionManager bean (name required as the auto configurations is conditional on the bean name!) changing the cookie name (use whatever you like except typical session cookie names like SESSION, JSESSION_ID, …):
static final String SESSION_COOKIE_NAME = "GATEWAY_SESSION";
#Bean(name = WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME)
WebSessionManager webSessionManager(WebFluxProperties webFluxProperties) {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
CookieWebSessionIdResolver webSessionIdResolver = new CookieWebSessionIdResolver();
webSessionIdResolver.setCookieName(SESSION_COOKIE_NAME);
webSessionIdResolver.addCookieInitializer((cookie) -> cookie
.sameSite(webFluxProperties.getSession().getCookie().getSameSite().attribute()));
webSessionManager.setSessionIdResolver(webSessionIdResolver);
return webSessionManager;
}
Rename Cookies created
Next step is to rename (all) cookies set by the downstream server. This is easy as there is a RewriteResponseHeader filter available. I decided to simply add a prefix to every cookie name (choose a unique one for each downstream):
filters:
- "RewriteResponseHeader=Set-Cookie, ^([^=]+)=, DS1_$1="
Rename Cookies sent
Last step is to rename the cookies before sending to the downstream server. As every cookie of the downstream server has a unique prefix, just remove the prefix:
filters:
- "RewriteRequestHeader=Cookie, ^DS1_([^=]+)=, $1="
Arg, currently there is no such filter available. But based on the existing RewriteResponseHeader filter this is easy (the Cloud Gateway will use it if you register it as a bean):
#Component
class RewriteRequestHeaderGatewayFilterFactory extends RewriteResponseHeaderGatewayFilterFactory
{
#Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(httpHeaders -> rewriteHeaders(httpHeaders, config)).build();
return chain.filter(exchange.mutate().request(request).build());
}
#Override
public String toString() {
return filterToStringCreator(RewriteRequestHeaderGatewayFilterFactory.this)
.append("name", config.getName()).append("regexp", config.getRegexp())
.append("replacement", config.getReplacement()).toString();
}
};
}
private void rewriteHeaders(HttpHeaders httpHeaders, Config config)
{
httpHeaders.put(config.getName(), rewriteHeaders(config, httpHeaders.get(config.getName())));
}
}
Simply reset cookie name to GATEWAY_SESSION in gateway project to avoid session conflict:
#Autowired(required = false)
public void setCookieName(HttpHandler httpHandler) {
if (httpHandler == null) return;
if (!(httpHandler instanceof HttpWebHandlerAdapter)) return;
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
CookieWebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver();
sessionIdResolver.setCookieName("GATEWAY_SESSION");
sessionManager.setSessionIdResolver(sessionIdResolver);
((HttpWebHandlerAdapter) httpHandler).setSessionManager(sessionManager);
}

How to get Spring Boot + Data Rest to resolve validation messages based on Accept-Language?

I've got an API application based on Spring Boot (1.2.0.RC2) and Spring Data Rest, using Hibernate bean validation (out-of-the-box).
In order to expose invalid input (400 Bad Request) to the API clients I've created an exception handler class annotated with #ControllerAdvice which has the following method:
#ExceptionHandler
#ResponseBody ResponseEntity handleViolation(ConstraintViolationException exception) {
ErrorRepresentation.Builder builder = new ErrorRepresentation.Builder()
.withViolations(
exception.getConstraintViolations().stream()
.map(v -> {
ErrorRepresentation.Builder violationBuilder = new ErrorRepresentation.Builder();
violationBuilder.withDetail(v.getMessage());
violationBuilder.withInternalRef(v.getPropertyPath().iterator().next().getName());
return violationBuilder;
})
.map(ErrorRepresentation.Builder::build)
.collect(Collectors.toList()));
builder.withStatus(HttpStatus.BAD_REQUEST.value());
ErrorRepresentation error = builder.build();
return createResponseEntity(error);
}
Here I'm mapping javax.validation.ConstraintViolation into my own custom error representation class, which is then serialized to JSON. So far so good!
i18n is working, but only based on the default locale of the server environment.
I was hoping Spring had bootstrapped message interpolation automatically, such that the Accept-Language would be picked up and the Locale would be changed per request before message interpolation occurred.
Any hints on how to put this together?

Resources