How to pass parameter along with logout success url in spring security? - spring-security

I am using java based spring security configuration in my spring boot application. When user clicks on logout link, user is redirected to the login page. Here, in this case, I need to pass a custom parameter in the logout success url.
e.g. when I logout, app is redirected to http://localhost:8080/app/login
But I want it to have a parameter like below
http://localhost:8080/app/login?idletimeout=true
I have created my custom LogoutSuccesshandle for this. I get the param value in the handler and then I construct the success url and then redirect to it. But then on logout that parameter goes missing.
Below is my handler code.
public class LogoutSuccessHandlerImpl extends SimpleUrlLogoutSuccessHandler {
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
request.getSession().invalidate();
SecurityContextHolder.clearContext();
request.setAttribute("isLoggedOut", "true");
String contextPath = request.getContextPath();
String redirectURL = "/login";
String isIdleTimeOut = request.getParameter("idleTimeout");
request.setAttribute("idleTimeout", isIdleTimeOut);
System.out.println(isIdleTimeOut + " isIdleTimeOut ");
if (isIdleTimeOut != null && isIdleTimeOut.equalsIgnoreCase("true")) {
System.out.println("in if ");
redirectURL += "?idleTimeout=" + isIdleTimeOut;
}
// setDefaultTargetUrl(redirectURL);
// response.sendRedirect(redirectURL);
// super.onLogoutSuccess(request, response, authentication);
redirectStrategy.sendRedirect(request, response, redirectURL);
}
Below is my java config code.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/checkLogin")
.defaultSuccessUrl("/home")
.failureUrl("/login?login_error=1")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandlerImpl())
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/login**").permitAll()
.antMatchers("/error**").permitAll()
.antMatchers("/checkLogin**").permitAll()
.anyRequest()
.authenticated()
.accessDecisionManager(accessDecisionManager)
.and()
.exceptionHandling()
.accessDeniedPage("/accessDenied")
.and()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.invalidSessionUrl("/login")
.maximumSessions(1);
}

What you can do is to prepare your own logout method (a custom logout url) for your applicatoin:
1) Prepare your LogoutController:
#Controller
#RequestMapping(value = "/logout")
public class LogoutController {
#RequestMapping(value = {"", "/"})
public String logout(HttpServletRequest request) {
SecurityContextHolder.clearContext();
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:" + <your-logout-success-url>;
}
}
Here, invalidating the session and clearing the context provides you a logout mechanism.
2) Update your logout urls inside jsp files:
<span>Log out</span>
3) In addition, for default "log out" scenarios, you can still continue to use the Spring Security log out:
<logout logout-success-url="/" invalidate-session="true" delete-cookies="JSESSIONID"
logout-url="/j_spring_security_logout"/>
So, in this logout method, you can do whatever you want with the request object when user demands the logout process. You do not need to pass parameters now.

It should be something like this
http
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/login?idletimeout=true"));
In xml
<logout invalidate-session="true" logout-success-url="/login?idletimeout=true"/>

Related

Spring Security not sending samesite=none with JSESSIONID

This is how my websecurityconfig is described:
public void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(corsFilter(), SessionManagementFilter.class) //adds your custom CorsFilter
.authorizeRequests()
.antMatchers("/ping/get").permitAll()
.antMatchers("/user/updatePassword").permitAll()
.antMatchers("/user/resetPassword").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.successHandler(successHandler())
.failureHandler(failureHandler())
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.authenticationEntryPoint(authenticationEntryPoint())
.and()
.logout()
.logoutSuccessUrl("/login")
.invalidateHttpSession(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> {
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
})
.permitAll()
;
http.csrf().disable();
http.headers()
.addHeaderWriter(
new StaticHeadersWriter("Access-Control-Allow-Origin", "http://localhost:4200")
);
http.headers()
.addHeaderWriter(
new StaticHeadersWriter("Access-Control-Allow-Credentials", "true")
);
}
and the cors filter i've defined the header for samesite=none as follows
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) servletResponse;
HttpServletRequest req= (HttpServletRequest) servletRequest;
res.setHeader("Access-Control-Allow-Origin", req.getHeader("origin"));
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.setHeader("Access-Control-Max-Age", "3600");
res.setHeader("Access-Control-Allow-Headers", "access-control-allow-origin,content-type");
res.addHeader("Access-Control-Expose-Headers", "xsrf-token");
res.setHeader("Set-Cookie", "HttpOnly;Secure;SameSite=None");
if ("OPTIONS".equals(req.getMethod())) {
res.setStatus(HttpServletResponse.SC_OK);
} else {
filterChain.doFilter(req, res);
}
}
#Override
public void destroy() {
}
}
But whenever i'm calling the login endpoint, i'm only receiving httponly,secure but nnot samesite=none with my JSESSIONID cookkie. How do i make this work?
i've tried all different filter from answers of other questons but none of them work.
The issue is only on chrome. It would be helpful if there is any workaroud also. Recently chrome removed the samesite flag which could be disabled. need to solve this in order to create a webview for my site.
You can set the SameSite attribute when using Spring Session with a custom CookieSerializer.
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setSameSite("None");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
return serializer;
}

spring security loadUserByUsername username empty after login url is not used for a few days

#Autowired
private BCryptPasswordEncoder passwordEncoder;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(cUserDetailService)
.passwordEncoder(passwordEncoder);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/**").authenticated()
.and()
.csrf().disable()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.successHandler(successHandler)
.failureHandler(failureHandler)
.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(authEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(false);
}
public class CUserDetailService implements UserDetailsService {
#Autowired
private UserDetailDataEntityRepository userDetailDataEntityRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("username: " + username + ", " );
log.info(username == null);
Optional<UserDetailDataEntity> optional = userDetailDataEntityRepository.findByUsername(username);
if (optional.isPresent()) {
return optional.get();
} else {
throw new UsernameNotFoundException(username);
}
}
}
I used this config for my spring security, and got success login at first.
but then we find that if nobody access the server(even the login url) for a few days, and do login again the loadUserByUsername username will be empty and get login fail
FYI I used redis for session manage by adding dependency
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
and the login request is a form request

SESSIONID injection for basic authentication requests

I have a Spring-cloud based micro-services application. I also have an API gateway in front of all these services.
I need to support two types of clients.
One of them can call my application using an authorization token (by calling /authorize for example). The token is basically the SESSION ID. All the servers share the session by using Spring Session Redis.
The second client can only send me http basic authentication (user:pass as authorization header).
In the case of the second client, I need to check if the user is already authenticated and has an active session in redis. I added filter before BasicAuthenticationFilter in my security configuration to check that.
If the user has an active session, I'm putting the SESSIONID in the header, and removing the authorization header from the request (I'm using a custom HttpServletRequest wrapper for that). My purpose was that from that point on, Spring will manage the request in the downstream micro-services as if it was sent with a SESSIONID. The reason for that is to avoid a very long login time (more than 1 second).
Here’s my issue: when spring checks if the SESSIONID exists, it checks the original request which doesnt have any sessionId.
Security configuration:
#Resource
#Qualifier("sessions")
private Map<String, String> sessions;
#Autowired
#Qualifier("httpSessionStrategy")
HttpSessionStrategy sessionStrategy;
#Override
protected void configure(HttpSecurity http) throws Exception {
// // #formatter:off
http
.addFilterBefore(setSessionIdInHeader(), BasicAuthenticationFilter.class)
.sessionManagement()
.and()
.exceptionHandling()
.authenticationEntryPoint(restEntryPoint())
.and()
.headers().addHeaderWriter(new StaticHeadersWriter("Server",""))
.and()
.httpBasic()
.authenticationEntryPoint(restEntryPoint())
.and()
.logout().addLogoutHandler(clearTicketOnLogoutHandler())
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.authorizeRequests()
.antMatchers("/index.html", "/login", "/").permitAll()
.antMatchers(HttpMethod.OPTIONS).denyAll()
.antMatchers(HttpMethod.HEAD).denyAll()
.anyRequest().authenticated()
.and()
.authenticationProvider(authenticationProvider)
.csrf()
.disable()
.addFilterAfter(ticketValidationFilter(), SessionManagementFilter.class)
.addFilterAfter(changePasswordFilter(), SessionManagementFilter.class)
.addFilterAfter(httpPolutionFilter(), SessionManagementFilter.class)
.addFilterAfter(saveSessionId(), SessionManagementFilter.class);
// #formatter:on
}
Filter to add header to request:
private Filter setSessionIdInHeader(){
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Jedis jedis = null;
String authorization = request.getHeader("authorization");
String sessionId = null;
if (authorization != null){
if (sessions.get(authorization) != null){ //user already authenticated
sessionId = sessions.get(authorization);
jedis = getJedisPool().getResource();
if (jedis.hgetAll("spring:session:sessions:"+sessionId) != null){ //session alive in redis
log.info("session :"+ sessionId +" exists in redis");
HeaderMapRequestWrapper wrapper = new HeaderMapRequestWrapper(request);
wrapper.addHeader("TOKEN", sessionId);
wrapper.addHeader("mock_authorization", authorization);
filterChain.doFilter(wrapper, response);
}
}
}
filterChain.doFilter(request, response);
}
};
}
Change header name of SESSIONID:
#Bean
public HeaderHttpSessionStrategy httpSessionStrategy(){
HeaderHttpSessionStrategy headerHttpSessionStrategy = new HeaderHttpSessionStrategy();
headerHttpSessionStrategy.setHeaderName("TOKEN");
return headerHttpSessionStrategy;
}
private Filter saveSessionId() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if(request.getHeader("authorization") != null){
sessions.put(request.getHeader("authorization"), request.getSession().getId());
}else{
sessions.put(request.getHeader("mock_authorization"), request.getSession().getId());
}
}
};
}

Is it possible to use two different AuthenticationProvider's with WebSecurityConfigurerAdapter?

I have a SpringBoot based app, with multiple endpoints. Because of different clients that will be accessing the endpoints I would like to have different Authentication providers protecting them. Some endpoints would be protected by Kerberos (KerberosServiceAuthenticationProvider -- http://docs.spring.io/autorepo/docs/spring-security-kerberos/1.0.0.RC1/reference/htmlsingle/). Some endpoints would be protected by AD/LDAP (ActiveDirectoryLdapAuthenticationProvider).
I currently have it working with Kerberos OR LDAP, but not both:
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected class ApplicationSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//For Kerberos
auth.authenticationProvider(kerberosAuthenticationProvider())
.authenticationProvider(kerberosServiceAuthenticationProvider());
//For LDAP
//auth.authenticationProvider(customAuthenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, APPLICATION_ADMIN_ENDPOINTS)
.permitAll()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.PUT, APPLICATION_ADMIN_ENDPOINTS)
.hasAnyAuthority(AUTHENTICATED_APPLICATION_ADMIN_AUTHORITIES)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, APPLICATION_ADMIN_ENDPOINTS)
.hasAnyAuthority(AUTHENTICATED_APPLICATION_ADMIN_AUTHORITIES)
.and()
.authorizeRequests()
.antMatchers(CLIENT_ENDPOINTS)
.permitAll()
.and()
.authorizeRequests()
.antMatchers(SWAGGER_ENDPOINTS)
.permitAll()
.and()
.authorizeRequests()
.antMatchers(MANAGER_ENDPOINTS)
.hasAnyAuthority(AUTHENTICATED_MANAGER_AUTHORITIES)
.and()
.authorizeRequests()
.antMatchers(TRUSTED_AGENT_ENDPOINTS)
.hasAnyAuthority(AUTHENTICATED_TRUSTED_AGENT_AUTHORITIES)
.and()
.authorizeRequests()
.antMatchers("/kerb/**")
.hasAnyAuthority(AUTHENTICATED_APPLICATION_ADMIN_AUTHORITIES)
.and()
.addFilterBefore(spnegoAuthenticationProcessingFilter(authenticationManagerBean()), BasicAuthenticationFilter.class)
.httpBasic()
.and()
.csrf()
.disable();
}
}
#Bean
public AuthenticationProvider customAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(
ldapDomain, ldapUrl);
SimpleCaseAndWhitespaceMitigatingAuthoritiesMapper authoritiesMapper = new SimpleCaseAndWhitespaceMitigatingAuthoritiesMapper();
provider.setAuthoritiesMapper(authoritiesMapper);
provider.setConvertSubErrorCodesToExceptions(true);
return provider;
}
#Bean
public KerberosAuthenticationProvider kerberosAuthenticationProvider() {
KerberosAuthenticationProvider provider = new KerberosAuthenticationProvider();
SunJaasKerberosClient client = new SunJaasKerberosClient();
client.setDebug(true);
provider.setKerberosClient(client);
provider.setUserDetailsService(kerberosUserService());
return provider;
}
#Bean
public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() {
KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
provider.setTicketValidator(sunJaasKerberosTicketValidator());
provider.setUserDetailsService(kerberosUserService());
return provider;
}
#Bean
public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
ticketValidator.setServicePrincipal(kerberosPrincipal);
File f = new File(keytabFile);
try {
LOG.info(String.format("Absolute: %s, Canonical: %s", f.getAbsolutePath(), f.getCanonicalPath()));
if(f.exists()){
LOG.info("File exists.");
}
else{
LOG.info("File DOES NOT exist.");
}
} catch (IOException e) {
e.printStackTrace();
}
ticketValidator.setKeyTabLocation(new FileSystemResource(f));
ticketValidator.setDebug(true);
return ticketValidator;
}
#Bean
public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
filter.setAuthenticationManager(authenticationManager);
return filter;
}
#Bean
public KerberosUserDetailsService kerberosUserService() {
return new KerberosUserDetailsService();
}
Anyway to get this to work for both? I was thinking about making a custom authentication provider that would handle the requests, but wasn't sure if that would work.
Easiest think to do is have to SpringDistpatcherServlets in your Web.xml based on url mapping. Each url mapping is then in a different spring context. Each spring context can then have its own security.
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(kerberosAuthenticationProvider());
auth.authenticationProvider(kerberosServiceAuthenticationProvider());
auth.authenticationProvider(customAuthenticationProvider());
}
What you are doing is adding both authenticationProviders for Kerberos and your custom one to the AuthenticationManagerBuilder. This should register all of them.
What happens during runtime:
There is a ProviderManager handling all of your registered AuthenticationProvider and the build in one.
First it tries to authenticate as anonymous user. If the requested URL is set to permitAll this is the end of the story
Then the ProviderManager iterates over all of your AuthenticationProvider in the order you provided them. It checks if they support the authentication and tries to authenticate with them. If it fails it moves to the next one (saving the exception if there was one)
Finally there is a DaoAuthenticationProvider handling normal username-password-credentials
If any provider succeeds, the user is logged in, otherwise the saved exception is thrown.
Conclusion:
What you did should be quite close if not exactly what you want. For your Kerberos protected endpoint it would use the kerberos AuthenticationProvider. For the other endpoints it would try and fail Kerberos and move on to your custom provider.
If something is still not working I would recommend setting a breakpoint within the class org.springframework.security.authentication.ProviderManager and see how it is handling your provider.

In spring security 4 ,concurrent session not redirecting to expired url

In spring security 4 ,concurrent session not redirecting to expired url,instead it redirects to failure authentication url.
Following is the java configuration code snippet.
/*start of code*/
public class SecurityContextConfig extends WebSecurityConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(SecurityContextConfig.class);
/**
* #param auth
* #throws Exception
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
logger.debug("configureGlobal() : Start : auth={}", auth);
auth.authenticationProvider(userDetailsAuthenticationProvider());
}
#Override
public void configure(WebSecurity web) throws Exception {
logger.debug("configure() : Start : web={}", web);
// This is here to ensure that the static content (JavaScript, CSS, etc)
// is accessible from the login page without authentication
web.ignoring().antMatchers("/resources/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
logger.debug("configure() : Start : http={}", http);
http
.authorizeRequests()
.antMatchers("/resources/**")
.permitAll()
.antMatchers("/login/**")
.permitAll()
.antMatchers("/authenticate/**")
.permitAll()
.antMatchers("/ssoLogout")
.permitAll()
.antMatchers("/forgotpassword/json")
.permitAll()
.antMatchers("/favicon.ico")
.permitAll()
.antMatchers("/secure/**")
.authenticated()
.and()
// This is where we configure our login form.
// login-page: the page that contains the login screen
// login-processing-url: this is the URL to which the login form
// should be submitted
// default-target-url: the URL to which the user will be
// redirected if they login successfully
// authentication-failure-url: the URL to which the user will be
// redirected if they fail login
// username-parameter: the name of the request parameter which
// contains the username
// password-parameter: the name of the request parameter which
// contains the password
.formLogin()
.loginPage("/")
.loginProcessingUrl("/authenticate")
.failureUrl("/")
.successHandler(loginSuccessHandler())
.and()
// This is where the logout page and process is configured. The
// logout-url is the URL to send
// the user to in order to logout, the logout-success-url is
// where they are taken if the logout
// is successful, and the delete-cookies and invalidate-session
// make sure that we clean up after logout
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutHandler())
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.and()
.csrf()
.and()
// The session management is used to ensure the user only has
// one session. This isn't
// compulsory but can add some extra security to your
// application.
.sessionManagement()
//.invalidSessionUrl("/login")
.sessionFixation()
.changeSessionId()
.maximumSessions(1)
.expiredUrl("/login?reason=CONCURRENT_SESSION");
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
logger.debug("configure() : End : http={}", http);
}
/**
* #return
*/
#Bean(name = "loginSuccessHandler")
public LoginSuccessHandler loginSuccessHandler() {
logger.debug("loginSuccessHandler() : Start.");
LoginSuccessHandler loginSuccessHandler = new LoginSuccessHandler();
logger.debug("loginSuccessHandler() : End : loginSuccessHandler={}", loginSuccessHandler);
return loginSuccessHandler;}
/**
* #return
*/
#Bean(name = "logoutHandler")
public LogoutHandler logoutHandler() {
logger.debug("logoutHandler() : Start.");
LogoutHandler logoutHandler = new LogoutHandler();
logger.debug("logoutHandler() : End : logoutHandler={}", logoutHandler);
return logoutHandler;
}
/**
* #return
*/
#Bean(name = "authenticationProvider")
public UserDetailsAuthenticationProvider userDetailsAuthenticationProvider() {
logger.debug("userDetailsAuthenticationProvider() : Start.");
UserDetailsAuthenticationProvider authenticationProvider = new UserDetailsAuthenticationProvider();
logger.debug("userDetailsAuthenticationProvider() : End : authenticationProvider={}", authenticationProvider);
return authenticationProvider;
}
#Bean(name="accessDeniedHandler")
public AccessDeniedHandlerImpl accessDeniedHandler(){
AccessDeniedHandlerImpl accessDeniedHandler=new AccessDeniedHandlerImpl();
accessDeniedHandler.setErrorPage("/login?reason=Access Denied");
return accessDeniedHandler;
}}
The behavior of expired url is not consistent.Sometimes working but sometimes not working .
What can be the issue?
The problem is that when it redirects to the expired URL, your user does not have access to the URL so it sends the user to the log in page (which is the same as the log in failure URL).
You need to ensure you grant access to every user to the expired URL. For example:
http
.authorizeRequests()
.antMatchers("/login")
.permitAll()
...

Resources