Enforce separate sessions for different tabs in spring security - spring

I am using spring security v 3.1.3 in my web application. The app has a single entry login form customized with custom-filter in spring security. For now, my configurations are allowing a user to automatically log in the app if he opens the URL from a different tab in same browser, which is the default behavior of spring security session management.
I want to ensure that whenever a user log into the application, the session should not get shared across different tabs. On opening a new tab, he should get login page and logging in would create a new session in the same browser. For now i could not find any way to do this with spring security framework. I wouldn't mind integrating JsessionID in URLs, but it would be better if there is another way.

This is not a limitation on Spring Security, this is a general limitation of how the browsers work with cookies; if you set a cookie it's going to be shared by all tabs.
Said that the only reasonable option I can think of right now would be to include the session id in the URL as you suggested.

You can make use of HeaderWebSessionIdResolver.
Spring uses CookieWebSessionIdResolver by default.
To make use of it, use a random sessionId and save it in session storage, and send it along with your headers. This will vary across tabs, and will provide you with different web sessions.
val headerName = "SomeHeaderName"
#Configuration
class SessionConfig {
#Bean
fun headerWebSessionIdResolver(): WebSessionIdResolver {
return HeaderWebSessionIdResolver().apply {
headerName = headerName
}
}
#Bean
fun webSessionManager(webSessionIdResolver: WebSessionIdResolver): DefaultWebSessionManager {
return DefaultWebSessionManager().apply {
sessionIdResolver = webSessionIdResolver
}
}
}

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?

Persist session after browser is closed

I have a NextJS application that uses a Backend for Frontend Architecture with Spring Security OAuth2 Client and Spring Cloud Gateway, which communicates to my Spring Authorization Server, very similar to this sample.
My webapp is working real nice, I'm getting the SESSION and X-CSRF token from my BFF and are being set in the browser on my NextJS app as cookies, so everything is cool to that point. But my doubt is that I closed the browser window and my session goes away, obviously it happens since both the cookies have MAX-AGE as "Session".
I know that the best practice is to let is as is, let the session either expire by the session timeout or when the browser session ends, but I'm curious to know how to persist the SESSION and X-CSRF cookies after the browser closes, so I have these questions:
Is it just enough to set the MAX-AGE to something in both the BFF and Spring Authorization Server?
Is Spring Security Remember Me needed? Though my BFF uses WebFlux Security so that functionality isn't available.
Should the X-CSRF Cookie also be persisted after the browser is closed, just as the session?
Should the session timeout equal the max age that I would set for both the cookies?
Should the X-CSRF token be persisted in a database if I spun up multiple instances of the BFF?
Also I'm confused on how to setup this because of the fact that I do the login on the Spring Authorization Server but I'm also logged in in the BFF since I have the SESSION and X-CSRF token to communicate with my BFF, so I guess that both session configuration should be the same on these two apps since they both create a session cookie even though the browser only gets the BFF one.
Also worth noting that both my BFF and my Spring Authorization Server, use Spring Session with Redis using different namespaces.
Relevant Spring Security configuration in my BFF:
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(authorizeExchange ->
authorizeExchange.anyExchange().authenticated()
)
.exceptionHandling(exceptionHandling ->
exceptionHandling.authenticationEntryPoint(authenticationEntryPoint())
)
.csrf(csrf ->
csrf.csrfTokenRepository(csrfTokenRepository())
)
.cors(Customizer.withDefaults())
.oauth2Login(oauth2 ->
oauth2.authenticationSuccessHandler(authenticationSuccessHandler())
)
.logout(logout ->
logout
.logoutHandler(logoutHandler())
.logoutSuccessHandler(logoutSuccessHandler())
)
.oauth2Client(Customizer.withDefaults());
return http.build();
}
Security Configuration on my Spring Authorization Server:
// AuthorizationServerConfig class
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.cors(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.build();
}
// WebSecurityConfig class
#Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.cors(withDefaults())
.formLogin(withDefaults());
return http.build();
}
I've done some research and for folks looking for something similar this may shed a little bit of light.
To answer my own questions:
Is it just enough to set the MAX-AGE to something in both the BFF and Spring Authorization Server?
I think that setting the max-age for my BFF for my session and x-csrf token is enough for enabling my users to be able to open my SPA again and be able to keep using the application freely without login in again.
You can find more information about how to set the max age when using Spring Security for servlet applications here or here for Reactive applications
I don't think that it makes sense to set the max age for the Spring Authorization Server microservice since the browser only gets and needs the one from the BFF Gateway microservice.
Also it is worth noting that I would guess that the session timeout should be equal or superior for what you set the max age, since when your users are away from the app the inactivity on the BFF would invalidate the session if it timeouts, more on that here
Is Spring Security Remember Me needed? Though my BFF uses WebFlux Security so that functionality isn't available.
If you have a servlet application, Remember me does allow to auto login the user when they close the window back, in my case I use a Reactive application therefore this feature is not yet built in. But if yours is a servlet one, you can try the feature here
Should the X-CSRF Cookie also be persisted after the browser is closed, just as the session?
I would think so, because it won't be generated back since you aren't automatically login on the app when you reopen the browser window, you are just still using the same session. This sounds like a bad practice but I haven't found what to do in this case.
You can set the max age for the X-CSRF token on both servlet and reactive applications by using CookieCsrfTokenRepository or CookieServerCsrfTokenRepository.
Should the session timeout equal the max age that I would set for both the cookies?
Again I would think that the timeout should be equal or superior or depending of the time you would like to give the users to make time it out, since it refreshes every time someone does something on the server. Look more on that here
Should the X-CSRF token be persisted in a database if I spun up multiple instances of the BFF?
I don't think so, since it appears that it's tied to the HTTPSession and you are using something like Spring Session and already storing that on the database, then I don't think that you should try to store it in a different way. More on that here
If anyone wants to add something more, or I said something wrong please correct it.

Cookie management in Quarkus

How do I configure cookie management in the Quarkus rest client?
I am building a client to an API that sets a session cookie on login and then uses that session cookie for auth - a pretty standard stateful API.
It seems that with default behaviour with #RegisterRestClient does not preserve cookies between requests.
Having a dig, it seems that ResteasyClientBuilderImpl has a field cookieManagementEnabled which is false by default. This then disables cookies in ClientHttpEngineBuilder43 by default too.
I also found that if you use the async engine (useAsyncHttpEngine) this sync engine builder is not used and cookie management is also enabled.
The only way I found to configure the ResteasyClientBuilder underlying the RestClientBuilder was to use a RestClientBuilderListener. However, the plot thickens here.
There is a method in RestClientBuilder - property(String name, Object value) - that, in the case of Quarkus at least will delegate to the underlying ResteasyClientBuilder
if (name.startsWith(RESTEASY_PROPERTY_PREFIX)) {
// Makes it possible to configure some of the ResteasyClientBuilder delegate properties
However, a few lines down
Method builderMethod = Arrays.stream(ResteasyClientBuilder.class.getMethods())
.filter(m -> builderMethodName.equals(m.getName()) && m.getParameterTypes().length >= 1)
.findFirst()
.orElse(null);
i.e. it will only allows for non-nullary methods to be called. And to enable cookie management I need to call enableCookieManagement - which has no arguments.
So, the only way I have found to enable cookie management is:
class CookieConfigBuilderListener : RestClientBuilderListener {
override fun onNewBuilder(builder: RestClientBuilder) {
val builderImpl = builder as QuarkusRestClientBuilder
val delegateField = QuarkusRestClientBuilder::class.java.getDeclaredField("builderDelegate")
delegateField.isAccessible = true
val resteasyBuilder = delegateField.get(builderImpl) as ResteasyClientBuilder
resteasyBuilder.enableCookieManagement()
}
}
This is obviously far from ideal, not least because it uses reflection in a GraalVM native image setting and therefore will likely also require me to enable reflection on QuarkusRestClientBuilder.
So, back to the original question, how do I enable cookie management in the Quarkus rest client?

Configure Cookie Domain in spring session

So I already success implement SSO using spring session and redis on development localhost domain.
But when I deploy to server using two sub domain.
login.example.com
apps.example.com
They always create new session Id on each sub domain.
I already try to configure using Context in tomcat configuration.
<Context sessionCookieDomain=".example.com" sessionCookiePath="/">
But no luck.
Spring session moves the session management on application level, so no surprise that trying to configure the container (in your case tomcat) has no effect. Currently there is a TODO in spring-session code to allow setting the domain, but is not implemented.
Maybe it is best to open an issue to allow setting the domain or comment/vote on https://github.com/spring-projects/spring-session/issues/112.
Meanwhile a workaround would be to go with your own implementation of MultiHttpSessionStrategy based on CookieHttpSessionStrategy.
Finally I succeeded to setdomain on application level.
You're right, I hope in the future they implement the feature to set domain.
For now I create CustomCookieHttpSessionStrategy for my own implmentation.
private Cookie createSessionCookie(HttpServletRequest request,
Map<String, String> sessionIds) {
...
sessionCookie.setDomain(".example.com");
// TODO set domain?
...
}
And then register bean as HttpSessionStrategy.

How do I setup login service for Spring-social and spring-security over a REST API?

I want to have a JS application in on client-side (no jsps) that will communicate with back-end only with REST calls. I want also to enable users to be able to login with FB, Twitter accounts. In addition, I also want to enable users to register their own accounts. For this purpose I want to use Spring-security and spring-social on backend and Javascript SDK in front to get access_token from the FB, which will be then passed to backend.
The question is: how do I create a REST controller that would authenticate using spring-social and spring-security facilities?
I read through the examples in:
https://github.com/spring-projects/spring-social-samples
but couldn't really find how I could make use of ProviderSignInController or SpringSocialConfigurer for this purpose. I guess I cannot use the SocialAuthenticationFilter in my case since the "/auth/{providerid}" url is not what I'm looking for. However, I guess the ProviderSingInController seems to be of use here neither. Please correct me if I'm wrong. Ideally I would like to benefit from all capabilities of Spring Security framework.
I will appreciate any suggestions.
Best regards
EDIT
I would like to follow a flow like here: http://porterhead.blogspot.com/2013/01/writing-rest-services-in-java-part-4.html but using the Spring Social and Spring Security combined.
The front-end application is written in AngularJS
2nd EDIT
It turns out that you can simply make use of all the Spring Social modules benefits out of the box. The only thing a client has to do is call a GET on the auth/facebook or whatever link to fire entire 0auth dance which will eventually return the authentication result. Then you can control the flow easily (register account or return some relevant information to the client to let know registration is needed). So the SpringSocialConfigurer works well in this case (apart from the fact that it doesn't support scope setting yet, however, this can be changed manually, check my pull request # github.com/spring-projects/spring-social/pull/141)
3rd EDIT - 14.10.2014
As requested, I will share how I managed to make it work.
Given I have configured my security filter in the following way:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
public void configure(final HttpSecurity http) throws Exception {
http.formLogin()
...
.and().apply(getSpringSocialConfigurer());
}
private SpringSocialConfigurer getSpringSocialConfigurer() {
final SpringSocialConfigurer config = new SpringSocialConfigurer();
config.alwaysUsePostLoginUrl(true);
config.postLoginUrl("http://somehost.com:1000/myApp");
return config;
}
Once my application is set up, the only thing I need to call is http://somehost.com:1000/myApp/auth/facebook
with GET request.
"In addition, I also want to enable users to register their own
accounts"
If you say that you want to allow users to login with their own credentials (without FB/twiter), you need to let them also to create account, and to support forgot password, etc...
If that is the case, maybe this SO thread might be helpful. The auth-flows package also supports REST API.
Create Account, Forgot Password and Change Password

Resources