Spring security session timeout for spring reactive - spring-boot

I have a Reactive Application with Spring Security integrated, it was created by spring initilizer with mainly thre3 packages(spring boot, spring security and webflux).
I was trying to configure the session timeout by following configuration in application.properties:
spring.session.timeout=1m
after starting the application with mvn spring-boot:run, It can be accessed by http://localhost:8080 and it asked me to login(by default security setting). I can use the username user and the password generated on the console to login.
Per my configuration, I expected that after 1 minutes idle time, when I refresh the page http://localhost:8080 again, it can ask me to re-login. But in fact it didn't , until 30 minutes later
So I suspect the above configuration is not working
Did I used the wrong configuration?
the reproduce repo can be found here: https://github.com/ZhuBicen/ReactiveSpringSecurity.git

Spring should probably allow an auto-configuration for your case above for the reactive stack as it does for servlet.
However, "session" is state and that state won't scale unless there is some persistent storage backing it. You can use the Spring Session abstraction with an in-memory ReactiveSessionRepository even if you don't (yet) have a backing store like Redis or something. When you do get a proper supported backing store and add the corresponding dependencies, you can delete your in-memory ReactiveSessionRepository as spring boot will auto-configure your ReactiveSessionRepository for you.
First, add the spring session dependency
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
</dependency>
Second, manually create your ReactiveSessionRepository bean. (Note: this can be auto-configured for you if you're using Redis instead of in-memory, etc.)
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.session.SessionProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.ReactiveMapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
import java.util.concurrent.ConcurrentHashMap;
/**
* This ReactiveSessionRepository isn't auto-configured so we need to create it and manually set the timeout on it.
* Later, ReactiveRedisSessionRepository will be auto-configured so we can delete this
*/
// https://www.baeldung.com/spring-session-reactive#in-memory-configuration
#Configuration
#EnableSpringWebSession
#RequiredArgsConstructor // if lombok
#Slf4j // if lombok
public class SessionConfig {
private final SessionProperties sessionProperties;
#Bean
public ReactiveSessionRepository reactiveSessionRepository() {
ReactiveMapSessionRepository sessionRepository = new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
int defaultMaxInactiveInterval = (int) sessionProperties.getTimeout().toSeconds();
sessionRepository.setDefaultMaxInactiveInterval(defaultMaxInactiveInterval);
log.info("Set in-memory session defaultMaxInactiveInterval to {} seconds.", defaultMaxInactiveInterval);
return sessionRepository;
}
}
Third, set the property spring.session.timeout=3600.

I finally fixed it by implementing customized ServerAuthenticationSuccessHandler, eg:
class SuccessHandler extends RedirectServerAuthenticationSuccessHandler {
#Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
// set to -1 means the session will never expired
// webFilterExchange.getExchange().getSession().subscribe(session->session.setMaxIdleTime(Duration.ofSeconds(-1)));
webFilterExchange.getExchange().getSession().subscribe(session->session.setMaxIdleTime(Duration.ofMinutes(60)));
return super.onAuthenticationSuccess(webFilterExchange, authentication);
}
}
The SuccessHandler can be setted by similar way:
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http.formLogin().authenticationSuccessHandler(new SuccessHandler());
return http.build();
}
}

The parameter you have configured has nothing to do with the cookie session you have with the web-browser.
That parameter configures Spring Session which is a way to handle session between services, rest api's and such by providing a session through a header.
https://spring.io/projects/spring-session
Session cookies are historically bound to threads in threadlocal, which doesn't work in reactive applications. So in order to store sessions, you need be able to store the sessions somewhere else. Redis is one example ofwhere you can store websessions.
Here is a tutorial of using webflux, redis and spring session to manage websessions.
https://www.baeldung.com/spring-session-reactive

Reactive stack configuration is different from the servlet stack.
server.reactive.session.timeout=10m
Spring documentation reference link

Related

Spring Boot 3 Redis Indexed Session - Failure to save login

I am upgrading my team's Spring Boot project from 2.7.5 to 3.0.0, which includes a migration of the now deprecated WebSecurityConfigurerAdapter. I am working in Kotlin, but I do not believe the language impacts the performance of the component.
In both the old and new versions of the security configuration class, I have declared this Autowired field and corresponding bean:
#Autowired
private lateinit var sessionRepository: FindByIndexNameSessionRepository<out Session>
#Bean
fun sessionRegistry(): SessionRegistry {
return SpringSessionBackedSessionRegistry(sessionRepository)
}
At first, my application would not start, because Spring could not wire the session repository into my configuration, but after some digging, it was because I had to change the #EnableRedisHttpSession annotation to
#EnableRedisIndexedHttpSession
class SecurityConfiguration(
Now, my current issue with the application is that I cannot log into my application, and I think it has to do with the Redis session management.
I am able to load the login page, and submit a login request. When I submit the request, I am able to see all of the normal debug messages my application prints associated with logging in, taking me through a successful authentication. When all of that finishes, my application redirects the user to the main page, which checks if a user's authentication != null, and redirects them to the main page if so. However, the authentication at this point is null, and there is not an entry in redis for this user's authentication.
In case it helps, this is what the security configuration had defined in the SecurityFilterChain bean for the session parts before the migration:
.sessionManagement().sessionFixation().migrateSession().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).maximumSessions(1).sessionRegistry(sessionRegistry()).expiredUrl("/api/v1/logout").and()
and after:
.sessionManagement { ssm ->
ssm.sessionFixation { it.migrateSession() }
ssm.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1).sessionRegistry(sessionRegistry()).expiredUrl("/api/v1/logout")
}
Is there something I am missing to allow the user authentication to persist? I have not touched any part of the code for this migration besides the pom.xml file and the rewriting of the security configuration.
UPDATE: I have altered the tags of the security configuration, in a desperate attempt at getting something to save.
#Configuration
#EnableWebSecurity
#EnableSpringHttpSession
#EnableRedisIndexedHttpSession(redisNamespace = "admindev:session", flushMode = FlushMode.IMMEDIATE, saveMode = SaveMode.ALWAYS)
class SecurityConfiguration(
I have increased the logging level for spring security, and everything looks fine, until the session token is not found.
Update 2:
I have tweaked my application as I have described in my answer to this post, and I am now able to access the frontend of my application. However, when I attempt to login through the backend, I get caught up in an infinite redirection loop. The app tries to send me to the error/4xx page, which is the correct behavior, since we don't have a "/" link on the backend, but then the app tries to redirect me according to the "your session has expired" page, and since I'm logged in, it tries to redirect me back to "/"...
Looking through the logs, it seems to be related to the exception handling I set up on my filter chain:
http.exceptionHandling { e -> e.authenticationEntryPoint(LoginRedirectHandler()) }
//...
class LoginRedirectHandler : LoginUrlAuthenticationEntryPoint("/login") {
override fun determineUrlToUseForThisRequest(
request: HttpServletRequest,
response: HttpServletResponse,
exception: AuthenticationException?
): String {
return "${this.loginFormUrl}?error=timeout&requestedUrl=${request.requestURL}"
}
}
Now, the question becomes: the backend webpages were not looping before, what could have changed about this behavior?

How to trace incoming requests with AWS X-Ray on Spring Boot WebFlux?

I have multiple microservices running on AWS ECS and I want to try out AWS X-Ray. Following this developer guide I added a WebConfig.java file with a tracing filter.
Added lines to build.gradle:
implementation "javax.servlet:javax.servlet-api:4.0.1"
implementation "com.amazonaws:aws-xray-recorder-sdk-core:2.8.0"
New file WebConfig.java:
import com.amazonaws.xray.javax.servlet.AWSXRayServletFilter;
import javax.servlet.Filter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class WebConfig {
#Bean
public Filter tracingFilter() {
return new AWSXRayServletFilter("ordermicroservice");
}
}
However, I don't think this is correct, mostly because I had to add the extra dependency for the javax.servlet.Filter. I think this is because I'm using spring-boot-webflux and not spring-boot-web. So I have a Netty webserver and not a Tomcat webserver.
My questions are:
How can I add logging to a servlet filter, to make sure that every incoming HTTP request is passing correctly through the filter?
What is the correct way to write a web filter in spring-boot-webflux projects, which use Netty and not Tomcat?
Edit:
By now I figured out how to write a filter with Spring Boot WebFlux. I will add it to the question here for future reference:
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
#Component
class MyCustomWebFilter : WebFilter {
override fun filter(webExchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
print("Successfully reached the WebFilter.\n")
val request = webExchange.request
val response = webExchange.response
// TODO: Do the actual filtering by looking at the request and modifying the response
return chain.filter(webExchange)
}
}
As mentioned by Michael, you may need to implement the WebFilter interface. Since the AWSXRayServletFilter is a servlet filter, it won't work with WebFilter. Unfortunately there is no built-in support in X-Ray SDK for WebFlux yet. What you'll need to do is within your WebFilter chain, implement an interceptor of your own for tracing incoming requests by creating a segment and adding relevant data to it. You can reference how the AWSXRayServletFilter traces incoming requests here: https://github.com/aws/aws-xray-sdk-java/blob/master/aws-xray-recorder-sdk-core/src/main/java/com/amazonaws/xray/javax/servlet/AWSXRayServletFilter.java
Or, you can use the OpenTelemetry Java SDK to instrument your application and use AWS Collector to send trace data to X-Ray. The OTel SDK has support for WebFlux framework. You can find more info below.
OTel Java SDK instrumentation: https://github.com/open-telemetry/opentelemetry-java-instrumentation
AWS OTel Collector: https://aws-otel.github.io/docs/getting-started/collector

How to disable management context access log in Spring Boot using undertow

I'm using Spring Boot 2.2.4 with embedded Undertow.
I've enabled the access log using server.underdow.accesslog.enabled=true and everything works as expected.
I'm utilizing the actuator endpoints on a different port which sets up a child context. I do not want requests to the actuator to be logged. Currently they automatically go to management_access.log where access. is the prefix of my main access log.
Any ideas on how to disable that access log? I know Spring is creating a separate WebServer via Factory for the actuator context, but I haven't found a way to customize the factory.
I found my own answer (spent way too much time doing it).
It's a little bit of a hack, but it works:
New configuration class: foo.ManagementConfig
package foo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
#ManagementContextConfiguration
public class ManagementConfig {
#Bean
WebServerFactoryCustomizer<UndertowServletWebServerFactory> actuatorCustomizer(#Value("${management.server.port}") int managementPort) {
return factory -> {
if (managementPort == factory.getPort()) {
factory.setAccessLogEnabled(false);
}
};
}
}
Created resources/META-INF/spring.factories so that it gets picked up by the ManagementContext:
org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration=foo.ManagementConfig
The part that's a bit of a hack is the if statement. It would have been great if it applied only to the management context, but for some reason it's trying to apply to both. With the if statement, it just doesn't do anything for the primary context.
This would have unintended consequences if management.server.port was undefined or if it was the same as the primary context.

Can I configure the Keycloak Spring Boot adapter so that it populates the user principal even on requests that don't require authorization?

If I have an endpoint which should be publically available but behave differently if you're logged in, I can't specify it in the security-constraints section of my application configuration (otherwise if I'm not logged in I can't reach the endpoint at all). But without it being listed there, the user principal never gets populated on the request, even if a valid Bearer token is provided in the Authorization header.
Is there a way to get the behaviour I want with the basic Spring Boot adapter? If not, am I likely to have more luck with the Spring Security adapter?
A solution to this is to enable preemptive authentication in the Tomcat context. Assuming you're deploying using the embedded server, you can implement a WebServerFactoryCustomizer as follows:
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
#Component
public class PreemptiveAuthWebServerCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory>
{
#Override
public void customize( final TomcatServletWebServerFactory factory )
{
factory.addContextCustomizers( context -> context.setPreemptiveAuthentication( true ) );
}
}
Now if a request has an Authorization header sent, it will always be processed even if the request isn't explicitly behind a path which requires authorization.

Implement multi-tenanted application with Keycloak and springboot

When we use 'KeycloakSpringBootConfigResolver' for reading the keycloak configuration from Spring Boot properties file instead of keycloak.json.
Now there are guidelines to implement a multi-tenant application using keycloak by overriding 'KeycloakConfigResolver' as specified in http://www.keycloak.org/docs/2.3/securing_apps_guide/topics/oidc/java/multi-tenancy.html.
The steps defined here can only be used with keycloak.json.
How can we adapt this to a Spring Boot application such that keycloak properties are read from the Spring Boot properties file and multi-tenancy is achieved.
You can access the keycloak config you secified in your application.yaml (or application.properties) if you inject org.keycloak.representations.adapters.config.AdapterConfig into your component.
#Component
public class MyKeycloakConfigResolver implements KeycloakConfigResolver {
private final AdapterConfig keycloakConfig;
public MyKeycloakConfigResolver(org.keycloak.representations.adapters.config.AdapterConfig keycloakConfig) {
this.keycloakConfig = keycloakConfig;
}
#Override
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
// make a defensive copy before changing the config
AdapterConfig currentConfig = new AdapterConfig();
BeanUtils.copyProperties(keycloakConfig, currentConfig);
// changes stuff here for example compute the realm
return KeycloakDeploymentBuilder.build(currentConfig);
}
}
After several trials, the only feasible option for spring boot is to have
Multiple instances of the spring boot application running with different spring 'profiles'.
Each application instance can have its own keycloak properties (as it is under different profiles) including the realm.
The challenge is to have an upgrade path for all instances for version upgrades/bug fixes, but I guess there are multiple strategies already implemented (not part of this discussion)
there is a ticket regarding this problem: https://issues.jboss.org/browse/KEYCLOAK-4139?_sscc=t
Comments for that ticket also talk about possible workarounds intervening in servlet setup of the service used (Tomcat/Undertow/Jetty), which you could try.
Note that the documentation you linked in your first comment is super outdated!

Resources