3
votes

I'm using default Spring Security to handle logout/login. I have a Controller method that handles /login.

When I log out, I see that Spring Security redirects me to app/login?logout. The existence of this Spring-created parameter (and also sometimes app/login?error) allows me to write my Login handler as:

@GetMapping("/participant/login")
public ModelAndView  loginPage(HttpServletRequest request, HttpServletResponse response, 
        @RequestParam(value = "error", required = false) String error,
        @RequestParam(value = "logout", required = false) String logout) {
    log.info("Entering participant login page");
    ModelAndView mav = new ModelAndView(LOGIN_JSP);
    if (null != error) {
        // We're coming to the login page after an Error
        mav.addObject("info", "My generic error message");
    } else if(null != logout){
        // We're coming to the login page after a Logout
        mav.addObject("info", "My generic logout message");
    }
    // ...Otherwise, normal Login page, no extra information

Now the problem is that when I log out, I need to pass a custom parameter to /logout with a transfer to /login. The goal is I need to receive a param in /login that I can examine just like the system-created error and logout.

Suppose this custom param is exitMsg.

From my app I issue this Spring Security Logout URL (logout is automatic, so I don't have a specific handler for it):

myapp.com/app/logout?exitMsg=MyMessage

Right away, the Login handler loses this param and I don't have it.

I considered writing my own /logout handler, where I manually log out (invalidate the session), and then redirect to Login myself with this param. This is the suggestion here. But if I do that, I lose the ability to get Spring's automatic ?logout and ?error Request Params. In the automatic scenario I was getting them, and now I'm not. I'm only getting the custom parameter I specify myself. I need to keep ?logout and ?error and also test for my own new param.

Any thoughts highly appreciated.

Spring Security Config:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/participant/**").authorizeRequests()
                .antMatchers("/participant/id/**").permitAll()
                .antMatchers("/participant/faq").permitAll()
                .antMatchers("/participant/forgetPassword").permitAll()
                .antMatchers("/participant/securityQuestions").permitAll()
                .antMatchers("/participant/securityCheck").permitAll()
                .antMatchers("/participant/resetPassword").permitAll()
                .antMatchers("/participant/**").authenticated()
            .and()
                .formLogin().loginPage("/participant/login").permitAll()
                .failureUrl("/participant/login?error").permitAll()
                .defaultSuccessUrl("/participant/home")
                .usernameParameter("username").passwordParameter("password")
            .and()
                .logout().logoutUrl("/participant/logout")
                .logoutSuccessUrl("/participant/login?logout").permitAll()
            .and()
                .csrf().disable();
    }
2
Justa dd that parameter to the logoutSuccesUrl value. - M. Deinum
I don't know in advance the value of that parameter, I only know the name of the parameter. The value comes from whatever I do from my page. So it's not a constant-value parameter! So how would I do this? - gene b.
Parameters don't survive redirects, so either use flash attributes or put it in the session. - M. Deinum

2 Answers

1
votes

Despite accepting the answer above (thanks Praveen for your help!), the only real solution I found is to avoid Spring's default logout->login behavior, and use a custom Logout handler with a new dedicated Logout JSP, which is NOT the Login page. Thus, I avoided the redirect to the Login page, I just have a separate Logout page -- they're not the same anymore. If the user wants to log in again then they may type the URL /app/login to get to Login.

The Logout handler is just a regular controller handler (I named it "myLogout"),

@GetMapping("/participant/myLogout")
public String myLogout(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // Just for testing, the param is available:
    System.out.println(request.getParameter("exitMsg");
    // Manual logoff
    CookieClearingLogoutHandler cookieClearingLogoutHandler = new CookieClearingLogoutHandler(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY);
    SecurityContextLogoutHandler securityContextLogoutHandler = new SecurityContextLogoutHandler();
    cookieClearingLogoutHandler.logout(request, response, null);
    securityContextLogoutHandler.logout(request, response, null);
    // My custom Logout JSP. No auto-redirects to Login
    return LOGOUT_JSP; 
}

And if you need the param, it's there both on the server- and client-side in the Logout JSP:

     `${param.exitMsg}`:  --> Output: `test`. 

All the suggestions of somehow redirecting to Login with a parameter -- whether it's with a CustomLogoutSuccessHandler implementation like here or directly via a controller redirect like here -- don't work when you have the Spring Security Config "login-wall" requirement of

    // ... special URLs listed here, then at the end:
    .antMatchers("/participant/**").authenticated()

after your list of special permissions. It doesn't work because Spring Security will do the Login-Wall requirement of a basic /login with NO parameters first as soon as you attempt even a Redirect to a Login with a parameter. And only then, after you authenticate yourself, will it serve the redirect-with-a-parameter such as login?logout&exitMsg=.... that you asked for. In other words you'll still lose the param on your first Login-Wall-Requirement, because you've already logged out.

I don't think we should get rid of .authenticated() in Config because that's what creates the Login-Wall protection on all requests, which is an important security function. But when you have it, then you can't pass parameters to /login because the basic Login-Wall /login with NO parameters will always happen first.

No real solution to this problem -- and the only way I fixed it is by bypassing the whole default behavior and just having a dedicated Logout page completely different from Login. This solution works and I don't need the Login page redisplayed immediately.

1
votes

You need logoutSuccessHandler instead of .logoutSuccessUrl("/login?logout")
Configure logoutSuccessHandler as given below

@Override
protected void configure(final HttpSecurity http) throws Exception
{
    http
        .authorizeRequests()
            .antMatchers("/resources/**", "/", "/login", "/api/**")
                .permitAll()
            .antMatchers("/app/admin/*")
                .hasRole("ADMIN")
            .antMatchers("/app/user/*")
                .hasAnyRole("ADMIN", "USER")
        .and().exceptionHandling().accessDeniedPage("/403")
        .and().formLogin()
            .loginPage("/login").usernameParameter("userName")
            .passwordParameter("password")
            .defaultSuccessUrl("/app/user/dashboard")
            .failureUrl("/login?error=true")
        .and().logout()
            .logoutSuccessHandler(new CustomLogoutSuccessHandler())
            .invalidateHttpSession(true)
        .and().csrf().disable();

    http.sessionManagement().maximumSessions(1).expiredUrl("/login?expired=true");
}

I considered writing my own /logout handler

In fact that is not logout handler but it is logout initiator.
Use CustomLogoutSuccessHandler, where you can get request param's and you can set it again, as given below.

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;

public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler
{

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException
    {
        Cookie cookie = new Cookie("JSESSIONID", null);
        cookie.setPath(request.getContextPath());
        cookie.setMaxAge(0);
        response.addCookie(cookie);

        if(request.getParameter("expired") != null)
        {
            response.sendRedirect(request.getContextPath()+"/login?expired=true");
        }
        else
        {
            response.sendRedirect(request.getContextPath() + "/login?logout=true");
        }
    }
}

From my app I issue this Spring Security Logout URL (logout is automatic, so I don't have a specific handler for it)

There is no automatic logout feature in servlet/spring security. Only what we can achieve is
1. Automate client to send logout request
2. In server we can set session's maxInactiveInterval, so that session can be invalidated by deleting cookie/setting age of cookie to past date. Once session is invalidated for the next request one of filter in spring security filter chain redirects it to /login page with param expired./login?expired
If you initiates logout spring security will delete the cookie/invalidate the session and redirects to /login page with param logout./login?logout
There are two types of configuration of achieving logout in spring security.

.and().logout()
.invalidateHttpSession(true)
//or
.deleteCookies("JSESSIONID")

EDIT: After Seeing OP's Answer. Missing info adding here. OP and i had a long chat in temporary answer(That chat and answer has been deleted) where we found the LOGIN-WALL.

"login-wall" is a outcome of bad* configuration where you will define a custom login page and it is configured as resource that is allowed to access after authentication. (access=isAuthenticated) but from CustomLogoutSuccessHandler if we redirect to login page after invalidating session spring security blocks login page access(401-Un Authorized) since that page is allowed only for authenticated-user. After blocking spring security takes care of redirects to the page configured in configuration. `.loginPage("/someloginpage"). What it forces to think is logout happening correctly, redirecting to login page correctly but params i sent in query string are not coming to loginpage. Nothing wrong if i call this as puzzle which is very hard to guess.