3

I am trying to find a way to persist a db record when a user authenticates via spring-security. Likewise, when they logout or timeout, I would like to update that record with that time. I have been trying to use AuthenticationSuccessHandler for logging in handling, and LogoutSuccessHandler for the logging out. But, when I use that, then my URL redirects after that seem to break.

Here is what I have so far:

@Component
public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {

    public MyLoginSuccessHandler() {
        super();
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("Logged In User " + (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
    }
}

and to trap log out events:

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    public MyLogoutSuccessHandler() {
        super();
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException {

        System.out.println("Logged OUT User " + (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
    }
}

And I configure my security as such:

@Configuration
@EnableWebMvcSecurity
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;

    @Autowired
    private MyLoginSuccessHandler myLoginSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
http
                .formLogin().failureUrl( "/login?error" )
                .successHandler(myLoginSuccessHandler)
                .defaultSuccessUrl( "/" )
                .loginPage( "/login" )
                .permitAll()
                .and()
                .logout().logoutRequestMatcher( new AntPathRequestMatcher( "/logout" ) )
                .permitAll();

        http
                .sessionManagement()
                .maximumSessions( 1 )
                .expiredUrl( "/login?expired" )
                .maxSessionsPreventsLogin( true )
                .and()
                .sessionCreationPolicy( SessionCreationPolicy.IF_REQUIRED )
                .invalidSessionUrl( "/" );

        http
                .authorizeRequests().anyRequest().authenticated();

        http
                .logout()
                .logoutSuccessHandler(myLogoutSuccessHandler)
                .logoutSuccessUrl( "/" );
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        auth.userDetailsService( customUserDetailsService ).passwordEncoder( encoder );
    }
}

If I put the .defaultSuccessUrl( "/" ) before the .successHandler then the handler is invoked, but the page redirect doesn't happen and the login results in a blank page of /login. Similarly for /logout.

Can anyone see what the issue is here

UPDATE: I added the Actuator and my own ApplicationListener:

@Component
public class LoginListener implements ApplicationListener<AuthenticationSuccessEvent> {

    private static final Logger LOG = LoggerFactory.getLogger(LoginListener.class);

    @Override
    public void onApplicationEvent(AuthenticationSuccessEvent event) {
        UserDetails ud = (UserDetails) event.getAuthentication().getPrincipal();

        LOG.info("User " + ud.getUsername() + " logged in successfully");
    }
}

Now, when a login occurs, I get the messages: 2014-11-06 10:10:55.923 INFO 90807 --- [nio-9001-exec-7] o.s.b.a.audit.listener.AuditListener : AuditEvent [timestamp=Thu Nov 06 10:10:55 MST 2014, principal=admin, type=AUTHENTICATION_SUCCESS, data={details=org.springframework.security.web.authentication.WebAuthenticationDetails@21a2c: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 64320375B40CF959936E86F4D1F2973C}]

And I see the code executed. So if I can get to the AuditEvent, I will have the IP and timestamp for my logging. For logging out, I tried my own LogoutHandler:

@Component
public class MyLogoutHandler implements LogoutHandler {

    private static final Logger LOG = LoggerFactory.getLogger(MyLogoutHandler.class);

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

        User user = (User) authentication.getPrincipal();
        LOG.info("User " + user.getUsername() + " logged OUT successfully");

    }
}

I also tried handling via listener:

@Component
public class LogoutListener implements ApplicationListener<SessionDestroyedEvent> {

    private static final Logger LOG = LoggerFactory.getLogger(LogoutListener.class);

    @Override
    public void onApplicationEvent(SessionDestroyedEvent event) {
        List<SecurityContext> lstSecurityContext = event.getSecurityContexts();
        UserDetails ud;
        for (SecurityContext securityContext : lstSecurityContext)
        {
            ud = (UserDetails) securityContext.getAuthentication().getPrincipal();
            LOG.debug("User " + ud.getUsername() + " logged OUT successfully");
        }
    }
}

neither of these calls are ever invoked. nor are there ever any messages to the console when the /logout call is made. I have a HttpSessionListener class that outputs a message on

 public void sessionDestroyed(HttpSessionEvent arg0) {
         totalActiveSessions--;
         System.out.println("sessionDestroyed - deduct one session from counter: " + totalActiveSessions);
     }

and that one is called, so I am sure the logout occurs.

sonoerin
  • 5,015
  • 23
  • 75
  • 132
  • 2
    See [this](http://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-auditing.html) section of the reference guide. Add the `spring-boot-starter-actuator` dependency and implement your own `AuditEventRepository`. You would need an additional `LogoutHandler` to publish an audit event for the logout (I suspect) but that is all you would need. – M. Deinum Nov 06 '14 at 07:02
  • Nice idea. Or just listen for the `AuthenticationSuccessEvent`. But I don't know if there is an event for logout. – Dave Syer Nov 06 '14 at 07:08
  • @DaveSyer Currently there isn't. See [gh-1836](https://github.com/spring-projects/spring-boot/issues/1836) and [SEC-2680](https://jira.spring.io/browse/SEC-2680). – M. Deinum Nov 06 '14 at 11:54

2 Answers2

0

I had the same problem with doing additional stuff after logout and have solved it the following way:

  • I use, like you in your updated approach, an ApplicationListener<SessionDestroyedEvent>.
  • But additionally you have to add an HttpSessionEventPublisher to your deployment descriptor. The reason is described on this page. In short:
    Session-Destroyed events are Java EE HttpSessionEvents, that take place outside the Spring environment. You could catch them with a Java EE HttpSessionListener. But if you want to access Spring related beans - f.e. the Spring-Authentication - then you have to add HttpSessionEventPublisher - it converts Java-EE-Events to Spring-Events:

    <listener>
        <listener-class>
            org.springframework.security.web.session.HttpSessionEventPublisher
        </listener-class>
    </listener>
    

After this you can catch the SessionDestroyedEvent like this:

@Component
public class AuthenticationApplicationListener {

  @EventListener
  public void handleSessionDestroyedEvent(SessionDestroyedEvent event) {
      List<SecurityContext> lstSecurityContext = event.getSecurityContexts();
      for (SecurityContext securityContext : lstSecurityContext) {

        //Try to find out, if this event is caused by a logout,
        //This is true, when the old session has been an authenticated one.
        Authentication auth = securityContext.getAuthentication();
        if (auth == null ||
            !auth.isAuthenticated() ||
            auth instanceof AnonymousAuthenticationToken) {
            return;
        }

        //do something
  }
}


This approach has some advantages compared with a Success-Handler:

  • It is also working, when you use programmatical logout (f.e. via HttpServletRequest.logout())
  • It does not interrupt the Filter Chain, when you use logout via url.


see also this answer

Community
  • 1
  • 1
olivmir
  • 692
  • 10
  • 29
0

I had exactly the same problem. When you use successhandler, defaultsuccessurl isn't called. So you have to redirect to the wished Url in the onAuthenticationSuccess function. Try:

@Component
public class LoginListener implements ApplicationListener<AuthenticationSuccessEvent> {

  private static final Logger LOG = LoggerFactory.getLogger(LoginListener.class);

  private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

  @Override
  public void onApplicationEvent(AuthenticationSuccessEvent event) {
    UserDetails ud = (UserDetails) event.getAuthentication().getPrincipal();

    LOG.info("User " + ud.getUsername() + " logged in successfully");
    redirectStrategy.sendRedirect(request, response,"/");
  }
}

Do the same for the LogoutListener and it should work (even when you don't change your config file).

Palo
  • 974
  • 12
  • 27