3

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();
    }
PraveenKumar Lalasangi
  • 3,255
  • 1
  • 23
  • 47
gene b.
  • 10,512
  • 21
  • 115
  • 227
  • Justa dd that parameter to the `logoutSuccesUrl` value. – M. Deinum Sep 26 '19 at 06:35
  • 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. Sep 26 '19 at 15:02
  • Parameters don't survive redirects, so either use flash attributes or put it in the session. – M. Deinum Sep 26 '19 at 17:31

2 Answers2

1

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.

PraveenKumar Lalasangi
  • 3,255
  • 1
  • 23
  • 47
  • thanks, do I need something like `super.logout()` here to make it go downstream and do the normal logout? Or is this my custom code on top only (the logout is assured)? – gene b. Sep 26 '19 at 12:50
  • No. Not required. See my updated answer. You just reconstruct redirect URL by getting your custom param in logoutSuccessHandler. – PraveenKumar Lalasangi Sep 26 '19 at 13:12
  • Thanks, it's a good idea, but on the Response Redirect I'm getting `java.lang.IllegalStateException: Cannot call sendRedirect() after the response has been committed`. Someone else reported the same issue: https://stackoverflow.com/questions/35555976/spring-security-cannot-call-sendredirect-after-the-response-has-been-committe – gene b. Sep 26 '19 at 14:22
  • explain scenario. on calling logout url? Also post your modified code – PraveenKumar Lalasangi Sep 26 '19 at 14:51
  • I am doing what was posted in the Custom Logout Success Handler: `response.sendRedirect("/app/login?logout=true&exitMsg=message");`. But this doesn't work because `Cannot call sendRedirect() after the response has been committed` on that line. I'm not the only one getting this error, I gave the link above to another poster who had this issue. Thanks – gene b. Sep 26 '19 at 15:14
  • 1
    Have you used `response.getWriter().write() or print()`? before sendRedirect()? or you might be using sendRedirect twice? Look at your sendRedirect should be in `if(){} else if{} else{}` block. if you placed outside you will get that.. It should work. I know you are doing something wrong! – PraveenKumar Lalasangi Sep 26 '19 at 15:18
  • No, there's no custom code anywhere writing anything. I just have the Spring Configuration `.and().logout().logoutUrl("/participant/logout").logoutSuccessHandler(new CustomLogoutSuccessHandler())`, and that's it. No writing or printing of anything. – gene b. Sep 26 '19 at 15:22
  • I mean you are doing something wrong in CustomLogoutSuccessHandler. Believe it should work. Post your CustomLogoutSuccessHandler code as answer. Later you can delete answer.Or you can append question with Edit heading. – PraveenKumar Lalasangi Sep 26 '19 at 15:26
  • Yes you were right - I had the sendResponse outside the If block. I made it exactly within If/Else and then I stopped getting the exception. But now I see that `/login` loses the parameter when it comes to that method. When I test `request.getQueryString()` I get NULL instead of the specified param string. Also, I sometimes get that param string when I log in a **second** time, but not the first time. – gene b. Sep 26 '19 at 15:37
  • In other words, the **first time** I get the /login after /logout I do not have my parameters, I lost them. But then when I type in the UserName/Pwd and login, **then** the /login activation does have my passed parameters from before! I will also post a separate answer here. – gene b. Sep 26 '19 at 15:45
  • losing parameter? No way. Check you are passing correctly? Are you passing `/logout?exitMsg=yourMessage` ? If you are passing it, `request.getParameter("exitMsg")` should give that in that scenario. You can post another question with your complete code if not resolved. Because without seeing your code it is very hard to suggest. – PraveenKumar Lalasangi Sep 26 '19 at 15:51
  • Thanks just posted my code in a separate answer, above yours!! I'm doing `login?logout&exitMsg=...`, see that post! Thanks a LOT! – gene b. Sep 26 '19 at 15:53
  • Problem is with `.antMatchers("/participant/**").authenticated()` remove that line and Best practice is to keep /login as URL in controller instead of /participant/login and you can continue using `.antMatchers("/participant/**").authenticated()` – PraveenKumar Lalasangi Sep 26 '19 at 18:08
  • So I discussed with a colleague, and unfortunately I can't take out the line .authenticated because that removes the Spring Security enforcement of /login for all unauthenticated URLs. If I remove that line, then other URLs like /home after logout don't take me to the Login page, which they should. I've taken Spring Security enforcement out of the equation. What happens with that redirect is, Spring enforces its basic Login first (no params), and then after authentication it serves what I requested with the sendRedirect. That's why I'm getting it on the 2nd try not the 1st. Hard to fix! – gene b. Sep 26 '19 at 18:55
1

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.

gene b.
  • 10,512
  • 21
  • 115
  • 227