6

There are multiple versions of the same questions around and none seem to address the issue. I would like to get online users from spring security. I understand we need to Autowire SessionRegistry and use it. But still, it doesn't work. Here is the code. Not sure whether it is due to custom Username, password authentication or due to custom password encoder or something else. Everything seems to be correct. Even getting the current logged in user's data works fine but not logged in user list.

SessionSecurityConfig.java

@EnableJpaRepositories(basePackageClasses = UsersRepository.class)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SessionSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordencoder;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private UsernamePasswordAuthProvider usernamepasswdauth;

    @Bean
    SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(usernamepasswdauth).userDetailsService(userDetailsService)
                .passwordEncoder(passwordencoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
        http.csrf().disable();
        http.authorizeRequests() //
                .antMatchers("/ua/*").permitAll() //
                .antMatchers("/auth/*").authenticated() //
                .and().requestCache() //
                .requestCache(new NullRequestCache());
        http.httpBasic().disable();
        http.formLogin().disable();
        http.logout().disable();
        http
          .sessionManagement()
          .maximumSessions(1).sessionRegistry(sessionRegistry());
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

}

PasswordUpgrader.java

@Component
@Primary
public class PasswordUpgrader implements PasswordEncoder { // used to upgrade NTML password hashes to Bcrypt

    private final static BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();

    private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();

    @Autowired
    JdbcTemplate jdbc;

    public String encode(CharSequence rawPassword) {
        byte[] bytes = NtlmPasswordAuthentication.nTOWFv1(rawPassword.toString());
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
        }
        return new String(hexChars).toLowerCase();
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword == null || encodedPassword.length() == 0) {
            return false;
        }
        if (encodedPassword.equals(encode(rawPassword))) {
            String sql = "update user_data set password=? where password=?";
            jdbc.update(sql, new Object[] { bcrypt.encode(rawPassword), encode(rawPassword) });
            return true;
        } else {
            return bcrypt.matches(rawPassword, encodedPassword);
        }
    }
}

UsernamePasswordAuthProvider.java

@Component
public class UsernamePasswordAuthProvider implements AuthenticationProvider {
    Log logger = LogFactory.getLog(getClass());
    
    @Autowired
    private PasswordEncoder passwordencoder;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    Userdata userdata;

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
        String username = String.valueOf(auth.getPrincipal());
        String password = String.valueOf(auth.getCredentials());
        UserDetails user = userDetailsService.loadUserByUsername(username);
        String encodedpassword = user.getPassword().toString();
        logger.info("inside username passwd authentication");
        if (encodedpassword != null && password != null && passwordencoder.matches(password, encodedpassword)) {
            logger.info("inside username passwd authentication");
            return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
        }
        throw new BadCredentialsException("Username/Password Incorrect");
    }

    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

UnauthController.java

@RestController
@RequestMapping("/ua")
public class UnauthController {

    @Autowired
    private UsernamePasswordAuthProvider usernamepasswdauth;

    @PostMapping("/login")
    public Map<String, Object> login(HttpServletRequest req, @RequestBody Map<String, Object> map) {
        Authentication auth = usernamepasswdauth.authenticate(new UsernamePasswordAuthenticationToken(
                map.get("username").toString(), map.get("password").toString()));
        SecurityContextHolder.getContext().setAuthentication(auth);
        map.put("sessionid", session.getId());
        return map;
    }
}

AuthController.java

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    Userdata user;

    @Autowired
    SessionRegistry sessionregistry;

    Log logger = LogFactory.getLog(getClass());

    @GetMapping("/onlineusers")
    public List<String> authhello(Authentication authentication) {
        logger.debug(user.getEmail()); // prints current logged in user's email.
        logger.debug(sessionRegistry.getAllPrincipals());//returns empty
        return sessionRegistry.getAllPrincipals().stream()
                .filter(u -> !sessionRegistry.getAllSessions(u, false).isEmpty()).map(Object::toString)
                .collect(Collectors.toList());
    }
}

Tried Approaches:

  1. Baeldung
  2. Stackoverflow
  3. StackOverflow
Mani
  • 5,401
  • 1
  • 30
  • 51
  • Have you found the solution yet? – It's K Dec 13 '20 at 09:32
  • @It'sK Have applied a workaround by creating and storing a set in Redis and adding usernames to it on user login. Have used a scheduler once in 15 mins to check session expiry and remove the username from the set. Still waiting for a proper fix. – Mani Dec 15 '20 at 05:49
  • 1
    I've encountered that when you're using custom authentication and redirect authenticated user by yourself then it doesn't invoke spring session filter and therefore it is not handled by itself and we have to configure it (sessionAuthenticationStrategy and expiredSessionStrategy under sessionManagement). I've set it up and it works like charm now. May be I'll write in the answer later to help others. – It's K Dec 15 '20 at 11:27
  • @It'sK that would be a great help. Also if you could publish the project on GitHub and add a link to it that would be great tutorial. – Mani Dec 15 '20 at 11:41
  • Already planned to do it. I too struggled lot with these configurations so I'd love to help. – It's K Dec 15 '20 at 12:17
  • eager for the answer and to test the solution. – Mani Dec 15 '20 at 13:35

1 Answers1

5

If you read carefully in documentation here, it is well-written(very secluded though). The cause of the problem is in the way data is handled after authentication. In default authentication provided by the spring security, after successful authentication the control is passed through a filter managing sessions. However, if you are using customized authentication and redirecting user after successful authentication that filter doesn't come into the way and that's why no sessions are added in the session registry and it returns empty list.

The solution is to set authentication strategy with session registry into session management configuration of spring security. This will lead to the the expected behaviour. You'll find the code more helpful.


Method 1:

Spring security configuration for session

http
    .sessionManagement()
    .sessionAuthenticationStrategy(concurrentSession())
    .maximumSessions(-1)
                .expiredSessionStrategy(sessionInformationExpiredStrategy())

Define beans for

@Bean
public CompositeSessionAuthenticationStrategy concurrentSession() {

    ConcurrentSessionControlAuthenticationStrategy concurrentAuthenticationStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
    List<SessionAuthenticationStrategy> delegateStrategies = new ArrayList<SessionAuthenticationStrategy>();
    delegateStrategies.add(concurrentAuthenticationStrategy);
    delegateStrategies.add(new SessionFixationProtectionStrategy());
    delegateStrategies.add(new RegisterSessionAuthenticationStrategy(sessionRegistry()));

    return new CompositeSessionAuthenticationStrategy(delegateStrategies);
}


@Bean
SessionInformationExpiredStrategy sessionInformationExpiredStrategy() {
    return new CustomSessionInformationExpiredStrategy("/login");
}


@Bean
public SessionRegistry sessionRegistry() {
    return new SessionRegistryImpl();
}

Here's the CustomSessionInformationExpiredStrategy.java

public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {

    private String expiredUrl = "";

    public CustomSessionInformationExpiredStrategy(String expiredUrl) {
        this.expiredUrl = expiredUrl;
    }

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {

        HttpServletRequest request = sessionInformationExpiredEvent.getRequest();
        HttpServletResponse response = sessionInformationExpiredEvent.getResponse();
        request.getSession();// creates a new session
        response.sendRedirect(request.getContextPath() + expiredUrl);
    }

}

Method : 2

In spring security configuration, use the concurrentSession() from method 1.

http.sessionManagement().sessionAuthenticationStrategy(concurrentSession());
http.addFilterBefore(concurrentSessionFilter(), ConcurrentSessionFilter.class);

Here's CustomConcurrentSessionFilter.java

public class CustomConcurrentSessionFilter extends ConcurrentSessionFilter {

    public CustomConcurrentSessionFilter(SessionRegistry sessionRegistry) {
        super(sessionRegistry);
    }

    public CustomConcurrentSessionFilter(SessionRegistry sessionRegistry, SessionInformationExpiredStrategy sessionInformationExpiredStrategy) {
        super(sessionRegistry, sessionInformationExpiredStrategy);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        super.doFilter(req, res, chain);
    }

}

Still scratching head for something? Find the working example at Github repo. Feel free to raise issues or contribute.

It's K
  • 177
  • 3
  • 15
  • I am authenticating against LDAP (AD). Method 1 worked for me. Thanks. I did not get method 2 to work. `concurrentSessionFilter()` threw me of. Cannot be a @Bean in current state since the class requires constructor parameters. I did not check out the GIT repo where I probably would find out how it should be. – Avec Jul 06 '21 at 13:33