spring security: disable multiple sessions on the same user not working - spring-boot

HttpSecurity object configed like this:
http.authorizeRequests().antMatchers("/login","/loginPage","/static/login.html","/","/index","/static/authenticationErr.html","/static/duplicatedUserErr.html").permitAll()
.and()
.httpBasic()
.authenticationEntryPoint(customAuthenticationEntryPoint)
.and()
.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.loginPage("/loginPage")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(customLogoutHandler)
.permitAll()
.and()
.sessionManagement() // not working??
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.expiredUrl("/static/duplicatedUserErr.html")
;
Here is what I tried: by following the spring security reference at https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#concurrent-sessions and source code trace, I found out the key to determine if this is a duplicate login session is this part of code written in method onAuthentication of class ConcurrentSessionControlAuthenticationStrategy:
final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
authentication.getPrincipal(), false);
then base on the size of this list, compare with the maximumSessions limit deined in HttpSecurity config to check if this is an duplicated user. After debugging, I know every time a user try to login, this line of code will be called, however no matter how many times try to login in my browsers, sessions object always be null, it turns out the principals field defined in SessionRegistryImpl has been a empty map since it is created and never be filled with new elements.
Here is other detail of my config:
AuthenticationProvider: org.springframework.security.authentication.dao.DaoAuthenticationProvider
UserDetailService:
CustomUserDetailsService implements UserDetailsService
UserDetails:
org.springframework.security.core.userdetails.User
AuthenticationProcessingFilter:
CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter
Updated:
#Bean
public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter("/login");
filter.setAuthenticationManager(this.authenticationManagerBean());
filter.setAuthenticationFailureHandler(failureHandler);
filter.setAuthenticationSuccessHandler(successHandler);
filter.setSessionAuthenticationStrategy(sessionControlAuthenticationStrategy());
return filter;
}
Can someone give me a light of this?

Finally I have figured it out, you have to create beans by following this reference https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#concurrent-sessions, and you have to set CompositeSessionAuthenticationStrategy bean manually to CustomAuthenticationFilter , and then in your ConcurrentSessionControlAuthenticationStrategy bean, set ExceptionIfMaximumExceeded as true so it will throw a SessionAuthenticationException when a duplicate seesion of the same user created.
My code of above description is like this:
#Bean
public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter("/login");
filter.setAuthenticationManager(this.authenticationManagerBean());
filter.setAuthenticationFailureHandler(failureHandler);
filter.setAuthenticationSuccessHandler(successHandler);
//If don't set it here, spring will inject a compositeSessionAuthenticationStrategy bean automatically, but looks like it didn't work as expected for me
filter.setSessionAuthenticationStrategy(compositeSessionAuthenticationStrategy());
return filter;
}
#Bean
public ConcurrentSessionControlAuthenticationStrategy sessionControlAuthenticationStrategy() {
ConcurrentSessionControlAuthenticationStrategy csas = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
csas.setExceptionIfMaximumExceeded(true);
return csas;
}

Related

Spring Security Context Authentication is null

i am trying to add couple of filters in my request processing in spring boot security config.
Below is my code
#EnableWebSecurity
#Configuration
public class JwtSecurityConfiguration {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(this::configureEndpoints)
return http.build();
}
private void configureEndpoints(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry authorizationManagerRequestMatcherRegistry){
authorizationManagerRequestMatcherRegistry.mvcMatchers("/permit")
.permitAll()
.mvcMatchers("/block")
.denyAll()
.and()
.mvcMatcher("/api")
.addFilterBefore(new Filter1(), SecurityContextHolderAwareRequestFilter.class)
// register TenantFilter in the chain after the SecurityContext is made available by the respective filter
.mvcMatcher("/api")
.addFilterAfter(new Filter2(), SecurityContextHolderAwareRequestFilter.class)
.authorizeHttpRequests()
.mvcMatchers("/api")
.authenticated()
.and();
}
}
It seems the authentication does not happen and filters are never hit.
If i try to access the authentication in my runtime code i get SecurityContextHolder.getContext().getAuthentication() as null.
Seems to some problem in the security configuration only.

How to have different filters for different request paths on spring security?

I have two filters and I want to apply one on "/relatorios/**" and another for the rest.
How to do it?
Here is my (not working) version:
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.antMatcher("/relatorios/**")
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(relatoriosFilter)
.addFilterBefore(new ExceptionTranslationFilter(new Http403ForbiddenEntryPoint()),
relatoriosFilter.getClass())
.authorizeRequests()
.and()
.antMatcher("/**")
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(authHeaderTokenFilter)
.addFilterBefore(new ExceptionTranslationFilter(new Http403ForbiddenEntryPoint()),
authHeaderTokenFilter.getClass())
.authorizeRequests()
.anyRequest()
.authenticated();
}
UPDATE:
With this version, when I call any request path, both filters are called. I want "/relatorios/**" to call one filter and everything else to call another filter.
Here are two ways to define the URL paths that filters are applied on.
Firstly, you should be aware that creating a bean of a filter class implementing the Filter interface, the filter is then automatically registered to all endpoints. Since you are trying to achieve different filters for different paths, remove this if you are doing so in your code.
Now you may register your filters in one of the two following methods.
Method 1 - Register Filters with FilterRegistrationBean
In this method, your defined security chain should not define your customer filters, so remove both the addFilter methods from there. You will be setting the paths not via the chain, but rather via registration beans.
#Configuration
public class FilterConfiguration {
#Bean
public FilterRegistrationBean<RelatoriosFilter> relatoriosFilter(){
FilterRegistrationBean<RelatoriosFilter> registrationBean
= new FilterRegistrationBean<>();
registrationBean.setFilter(new RelatoriosFilter());
registrationBean.addUrlPattern("/relatorios/*");
registrationBean.setOrder(ORDERED.HIGHEST_PRECEDENCE);
return registrationBean;
}
#Bean
public FilterRegistrationBean<AuthHeaderTokenFilter> filter2(){
FilterRegistrationBean<AuthHeaderTokenFilter> registrationBean
= new FilterRegistrationBean<>();
registrationBean.setFilter(new AuthHeaderTokenFilter());
registrationBean.addUrlPattern("/*");
registrationBean.setOrder(ORDERED.HIGHEST_PRECEDENCE);
return registrationBean;
}
}
In this manner, you should also control the order of filters by calling setOrder method and giving a lower number for a higher precedence in the filtering chain. The necessary order will depend on the version of Spring you are using and in which part of the chain you are interested to inject in the chain. In my example, it will be the first filter.
Method 2 - Splitting the WebSecurityConfigurerAdapter configurations
A WebSecurityConfigurerAdapter chain cannot define two different filter configurations by path matching. This is one of various limitations of this chain.
This can be solved easily by creating an additional configuration, so that each configuration matches a different path and applies a different filter. You will end up with two configurations.
Configuration 1
#Configuration
#Order(1)
public class RelatoriosSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.and()
.antMatcher("/relatorios/**")
.addFilterBefore(new RelatoriosFilter(), ChannelProcessingFilter.class)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests().anyRequest().authenticated();
}
}
Configuration 2
#Configuration
#Order(2)
public class GeneralSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.antMatcher("/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new AuthHeaderTokenFilter(), ChannelProcessingFilter.class)
.authorizeRequests().anyRequest().authenticated();
}
}
In this method, #Order annotation defines the order of execution of the chains, so much like your original solution, the RelatoriosSecurity chain will be executed before GeneralSecurity chain.
Also, the addFilterBefore will define before which filter the supplied filter will run. The class of the of the filter to come before will depend on your Spring version, but in mine ChannelProcessingFilter is the first, therefore our supplied filter will be executed first, before ChannelProcessingFilter.

How to make a custom UsernamePasswordAuthenticationFilter register at an endpoint other than /login?

I've been following a tutorial to implementing JWT authentication in Spring Boot but am trying to adapt it to a case where I have two WebSecurityConfigurerAdapter classes, one for my API (/api/** endpoints) and one for my web front-end (all other endpoints). In the tutorial, a JWTAuthenticationFilter is created as a subclass of UsernamePasswordAuthenticationFilter and added to the chain. According to the author, this filter will automatically register itself with the "/login" endpoint, but I want it to point somewhere different, such as "/api/login" because I'm using this authentication method for my API only.
Here's the security configuration code for both the API and front-end (with some abbrevation):
#EnableWebSecurity
public class MultipleSecurityConfigurations {
#Configuration
#Order(1)
public static class APISecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
}
#Configuration
public static class FrontEndSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login").permitAll()
.defaultSuccessUrl("/")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/?logout")
.and()
.authorizeRequests()
.mvcMatchers("/").permitAll()
.mvcMatchers("/home").authenticated()
.anyRequest().denyAll()
;
}
}
}
The question is: how can I define an endpoint such as "/api/login" as the endpoint for my custom JWTAuthenticationFilter?
Or, do I need to change the filter to not be a subclass of UsernamePasswordAuthenticationFilter and if so, how would I configure that?
EDIT: Something I've tried:
I guessed that the /api/login endpoint needed to be .permitAll() and I tried using formLogin().loginProcessingUrl(), even though it's not really a form login - it's a JSON login. This doesn't work. When i POST to /api/login I end up getting redirected to the HTML login form as if I were not logged in. Moreover, my Spring boot app throws a weird exception:
org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
The configuration I'm trying now:
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.formLogin().loginProcessingUrl("/api/login").and()
.authorizeRequests()
.antMatchers("/api/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
}
Since JWTAuthenticationFilter is a UsernamePasswordAuthenticationFilter, you could change the login endpoint directly on the filter instance:
JWTAuthenticationFilter customFilter = new JWTAuthenticationFilter(authenticationManager());
customFilter.setFilterProcessesUrl("/api/login");
http.addFilter(customFilter);
This configures JWTAuthenticationFilter to attempt to authenticate POST requests to /api/login.
If you wish also to change the default POST to another method (e.g. GET), you can set the RequiresAuthenticationRequestMatcher instead. For instance:
customFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/api/login", "GET"));

Spring Security: Multiple http elements with Multiple AuthenticationManagers

I am struggling with Java Config for Spring Security. I have multiple entry points but I cannot get the AuthenticationManagers provisioned correctly.
My first configuration file is like this:
#Configuration
#EnableWebSecurity
#Order(100)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.antMatcher("/service/**")
.addFilterAfter(requestHeaderAuthenticationFilter(), SecurityContextPersistenceFilter.class)
.authorizeRequests()
.antMatchers("/service/**").authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.csrf().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.authenticationProvider(preAuthenticatedAuthenticationProvider(null));
}
#Bean
public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter() throws Exception
{
// Takes the value of the specified header as the user principal
RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter();
filter.setPrincipalRequestHeader("SECRET_HEADER");
filter.setAuthenticationManager(authenticationManager());
filter.setExceptionIfHeaderMissing(false);
return filter;
}
This all works correctly. When I set a breakpoint in the RequestHeaderAuthenticationFilter I see an AuthenticationManager with one AuthenticationProvider, and that is the preAuthenticatedAuthenticationProvider (not shown because is just a regular old bean).
I also have a special security chain for admin users and the like:
#Configuration
#Order(101)
public class AdminSecurity extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authenticationProvider(mainSiteLoginAuthenticationProvider())
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.antMatchers("/admin/**").access("SECRET ADMIN ACCESS EXPRESSION")
.antMatchers("/internal/**").access("SECRET INTERNAL ACCESS EXPRESSION")
.anyRequest().permitAll()
.and()
.formLogin()
.defaultSuccessUrl("/admin/thing")
.loginPage("/login")
.loginProcessingUrl("/do_login")
.defaultSuccessUrl("/admin/thing")
.failureUrl("/login?error=true")
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.and()
.exceptionHandling()
//.authenticationEntryPoint(null) // entry-point-ref="loginEntryPoint"
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // create-session="ifRequired"
.and()
.csrf().disable();
}
This is now working (after a lot of struggle), but if I put a breakpoint in the UsernamePasswordAuthenticationFilter, I see that this filter has a different AuthenticationManager instance, which is provisioned with the mainSiteLoginAuthenticationProvider as expected. However, it has a parent AuthenticationManager which is provisioned with the default DaoAuthenticationProvider that generates a temporary password in the logs:
Using default security password: 47032daf-813e-4da1-a224-b6014a705805
So my questions are:
How can I get both security configs to use the same AuthenticationManager? I thought that the SecurityConfig, being order 100, would create one, and then AdminConfig, being 101, would just use it. But I have been unable to get them to use the same AuthenticationManager.
Failing that, how can I prevent the AuthenticationManger of AdminConfig from generating a parent that has the default DaoAuthenticationProvider?
I am using Spring Boot 1.5.9.RELEASE, which means Spring Security 4.2.3.RELEASE.

Spring Boot Authorization Basic Header Never Changes

I am attempting to setup a very basic spring boot authenticated application. I am setting the Authorization header in the client and sending it to the backend. I can verify that the client is sending the correct header.
The backend receives the header correctly on the first attempt to login. However if the login credentials are incorrect subsequent requests retain whatever the header for the intial request was (caching it or something).
I am using Redis to Cache the session. My config is as follows:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired AuthenticationEntryPoint authenticationEntryPoint;
#Autowired CsrfTokenRepository csrfTokenRepository;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf()
.disable()
.authorizeRequests().antMatchers("**")
.permitAll()
.anyRequest()
.authenticated()
;
}
}
AuthenticationEntryPoint
public class AuthenticationEntryPointBean {
#Bean
AuthenticationEntryPoint authenticationEntryPoint() {
return new RestAuthenticationEntryPoint();
}
}
Any direction would be appreciated.
** Edit **
Adding cache settings
#Configuration
#EnableRedisHttpSession
public class HttpSessionConfig {
#Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory(); // <2>
}
}
Also I am trying to invalidate cache but that doesn't seem to work
#CrossOrigin
#RequestMapping(value="/auth/login", method = RequestMethod.GET, produces="application/json")
public #ResponseBody String login(#RequestHeader(name = "authorization") String authorization, HttpSession session, HttpServletRequest request)
{
try
{
authorization = authorization.substring("Basic ".length());
String decoded = new String(Base64.getDecoder().decode(authorization),"UTF-8");
Gson gson = new Gson();
LoginRequest login = gson.fromJson(decoded,LoginRequest.class);
UserAuthenticationEntity entity = service.getSecurityContext(login).orElseThrow(() ->
new BadCredentialsException("Authentication Failed.")
);
session.setMaxInactiveInterval((int)TimeUnit.MINUTES.toSeconds(expiresInMinutes));
SecurityContextHolder.getContext().setAuthentication(new EntityContext(entity,expiresInMinutes));
String response = gson.toJson(BasicResponse.SUCCESS);
return response;
}
catch (Exception e)
{
session.invalidate();
e.printStackTrace();
throw new AuthenticationCredentialsNotFoundException("Authentication Error");
}
}
Adding the following to my web security config seemed to do the trick.
.requestCache()
.requestCache(new NullRequestCache())
I am not sure what side effects are of doing this. I picked it up off of a blog https://drissamri.be/blog/2015/05/21/spring-security-and-spring-session/
If there is any more insight into if this is good practice or bad practice I would appreciate any comments.
My final web security config looks like the following:
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf()
.disable()
.authorizeRequests().antMatchers("**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.requestCache()
.requestCache(new NullRequestCache())
.and()
.sessionManagement()
.sessionFixation()
.newSession()
;

Resources