SecurityContextHolder returns null after I call setAuthentication with the custom authentication token - spring-boot

I have a small Spring Boot project which has username/password authentication. I use spring-security sign-in with our LDAP connection. What I want, and I did manage to do in several projects, is extending AbstractAuthenticationToken class in order to add my own fields.
In my custom GenericFilterBean class, I want to create my own authentication object and set into SecurityContextHolder as below:
KfsMsgToken kfsMsgToken = new KfsMsgToken(
kfsInMessageInfo.getObjId(),
new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(kfsMsgToken);
And, here is my custom Authentication class:
public class KfsMsgToken extends AbstractAuthenticationToken {
String kfsInMsgOid;
public KfsMsgToken(String kfsInMsgOid, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.kfsInMsgOid = kfsInMsgOid;
}
/**
*
* #return
*/
#Override
public Object getCredentials() {
return null;
}
/**
*
* #return
*/
#Override
public Object getPrincipal() {
return null;
}
public String getKfsInMsgOid() {
return kfsInMsgOid;
}
public void setKfsInMsgOid(String kfsInMsgOid) {
this.kfsInMsgOid = kfsInMsgOid;
}
}
The problem is, after login successfully, I see UsernamePasswordAuthenticationToken which has been already setted. I reset authentication field using my custom token object, returns null in service layer. I have no idea what is the reason.
All advises are appreciated!
My security configuration:
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${ldap.urls}")
private String ldapUrl;
#Value("${ldap.domain}")
private String ldapDomain;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/css/**", "/fonts/**", "/img/**", "/js/**", "/pdf/**").permitAll()
.and().formLogin().defaultSuccessUrl("/index", true).loginProcessingUrl("/login").permitAll().and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
#Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider = new
ActiveDirectoryLdapAuthenticationProvider(ldapDomain, ldapUrl);
return activeDirectoryLdapAuthenticationProvider;
}
}
How I register my filter:
#Bean
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new KfsInMsgFilter(kfsInMessageService, kfsMsgToken, messageChannelService));
registrationBean.setOrder(0);
registrationBean.addUrlPatterns(SECURE);
registrationBean.setDispatcherTypes(DispatcherType.REQUEST);
return registrationBean;
}
This is what I do in my filter:
public class KfsInMsgFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
...
// at this point, Authentication holds UserNamePasswordAuthenticationToken
KfsMsgToken kfsMsgToken = new KfsMsgToken(
kfsInMessageInfo.getObjId(),
new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(kfsMsgToken);
...
}
}

You need to extend AbstractAuthenticationProcessingFilter and have your custom LDAP authentication inside there and register and finally use
http.addFilterAfter(
new CustomFilter(), UsernamePasswordAuthenticationProcessingFilter.class);

Setting authenticated field over my custom token,
kfsMsgToken.setAuthenticated(Boolean.TRUE.booleanValue());
solved my problem.
But,
I think there is a better way. Using scoped beans is more efficient and practical.
I changed my custom filter to a request-scoped bean:
#Data
#AllArgsConstructor
#NoArgsConstructor
#Component
#Scope(value="request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class KfsMsgToken {
String kfsInMsgOid;
}
By autowiring, I can get and read the value that I need.

Related

SecurityContextHolder return wrong user context on concurrent request

I am experiencing a weird problem, When multiple concurrent requests comes to a controllerSecurityContextHolder.getContext().getAuthentication().getPrincipal()
returns same user object sometimes even if the JWT token is different.
#RequestMapping(value = {"/users/{userId}/solveDetail/create"}, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
#Transactional
public ResponseEntity<CreateSolveDetail> createSolve(#PathVariable("userId") Long userId, #RequestBody CreateSolveDetail createSolveDetail){
User user =SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
So far tried changing session management to .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) and thread strategy is set to SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_THREADLOCAL) still the isssue persists.
Below is the WebSecurityConfig class configured and a custom filter is added which overrides getPreAuthenticatedPrincipal and getPreAuthenticatedPrincipal of AbstractPreAuthenticatedProcessingFilter class.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityBasicConfig {
#Autowired
private Http403ForbiddenEntryPoint http403ForbiddenEntryPoint;
#Bean
public Http403ForbiddenEntryPoint http403ForbiddenEntryPoint() {
return new Http403ForbiddenEntryPoint();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.exceptionHandling()
.authenticationEntryPoint(http403ForbiddenEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(preAuthFilter(), BasicAuthenticationFilter.class);
httpSecurity.csrf().disable();
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_THREADLOCAL);
}
}
public class PreAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest httpServletRequest) {
String auth = httpServletRequest.getHeader("PRE-AUTH");
try {
User user = new ObjectMapper().readValue(auth, User.class);
return user;
} catch (Exception e) {
return new User();
}
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest httpServletRequest) {
String auth = httpServletRequest.getHeader("PRE-AUTH");
return auth;
}
}
Please let me know what I am doing wrong here.
Thanks in advance.
Spring boot version : 2.1.6.RELEASE
Architecture: Microservice

Implement Spring Security for Rest Api

I use this code for Rest API authentication:
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Optional<String> basicToken = Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION))
.filter(v -> v.startsWith("Basic"))
.map(v -> v.split("\\s+")).filter(a -> a.length == 2).map(a -> a[1]);
if (!basicToken.isPresent()) {
return sendAuthError(response);
}
byte[] bytes = Base64Utils.decodeFromString(basicToken.get());
String namePassword = new String(bytes, StandardCharsets.UTF_8);
int i = namePassword.indexOf(':');
if (i < 0) {
return sendAuthError(response);
}
String name = namePassword.substring(0, i);
String password = namePassword.substring(i + 1);
// Optional<String> clientId = authenticationService.authenticate(name, password, request.getRemoteAddr());
Merchants merchant = authenticationService.authenticateMerchant(name, password, request.getRemoteAddr());
if (merchant == null) {
return sendAuthError(response);
}
request.setAttribute(CURRENT_CLIENT_ID_ATTRIBUTE, merchant.getId());
return true;
}
How I can rewrite the code with Spring Security in order to get the same result but for different links to have authentication? For example:
localhost:8080/v1/notification - requests should NOT be authenticated.
localhost:8080/v1/request - requests should be authenticated.
Here you can find a working project https://github.com/angeloimm/springbasicauth
I know in the pom.xml file there are a lot of useless dependencies but I started from an already existing project and I had no time to depure it
Basically you must:
configure spring security
configure spring mvc
implements your own authentication provider according to spring security. Note I used an inMemoryAuthentication. Please modify it according to yuor own wishes
Let me explain the code.
Spring MVC Configuration:
#Configuration
#EnableWebMvc
#ComponentScan(basePackages= {"it.olegna.test.basic"})
public class WebMvcConfig implements WebMvcConfigurer {
#Override
public void configureMessageConverters(final List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter());
}
}
Here we don't do anything else that configuring spring MVC by telling it where to find controllers and so on and to use a single message converter; the MappingJackson2HttpMessageConverter in order to produce JSON responses
Spring Security Configuration:
#Configuration
#EnableWebSecurity
#Import(value= {WebMvcConfig.class})
public class WebSecConfig extends WebSecurityConfigurerAdapter {
#Autowired private RestAuthEntryPoint authenticationEntryPoint;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("test")
.password(passwordEncoder().encode("testpwd"))
.authorities("ROLE_USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/securityNone")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
Here we configure Spring Security in order to use HTTP Basic Authentication for all requests except the ones starting with securityNone. We use a NoOpPasswordEncoder in order to encode the provided password; this PasswrodEncoder does absolutly nothing... it leaves the passwrod as it is.
RestEntryPoint:
#Component
public class RestAuthEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
This entrypoint disables all requests not containg the Authentication header
SimpleDto: a very simple DTO representing the JSON answer form a controller
public class SimpleDto implements Serializable {
private static final long serialVersionUID = 1616554176392794288L;
private String simpleDtoName;
public SimpleDto() {
super();
}
public SimpleDto(String simpleDtoName) {
super();
this.simpleDtoName = simpleDtoName;
}
public String getSimpleDtoName() {
return simpleDtoName;
}
public void setSimpleDtoName(String simpleDtoName) {
this.simpleDtoName = simpleDtoName;
}
}
TestBasicController: a very simple controller
#RestController
#RequestMapping(value= {"/rest"})
public class TestBasicController {
#RequestMapping(value= {"/simple"}, method= {RequestMethod.GET}, produces= {MediaType.APPLICATION_JSON_UTF8_VALUE})
public ResponseEntity<List<SimpleDto>> getSimpleAnswer()
{
List<SimpleDto> payload = new ArrayList<>();
for(int i= 0; i < 5; i++)
{
payload.add(new SimpleDto(UUID.randomUUID().toString()));
}
return ResponseEntity.ok().body(payload);
}
}
So if you try this project by using postman or any other tester you can have 2 scenarios:
authentication required
all ok
Let's suppose you want to invoke the URL http://localhost:8080/test_basic/rest/simple without passing the Authentication header. The HTTP Status code will be 401 Unauthorized
This means that the Authentication Header is required
By adding this header to the request Authorization Basic dGVzdDp0ZXN0cHdk all works pretty good
Note that the String dGVzdDp0ZXN0cHdk is the Base64 encoding of the string username:password; in our case is the Base64 encoding of test:testpwd defined in the inMemoryAuthentication
I hope this is usefull
Angelo
WEB SECURITY USER DATAIL SERVICE
In order to configure Spring security to retrieve user details from DB you must do the following:
create a org.springframework.security.core.userdetails.UserDetailsService implementation like this:
#Service
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private BasicService svc;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
BasicUser result = svc.findByUsername(username);
if( result == null )
{
throw new UsernameNotFoundException("No user found with username "+username);
}
return result;
}
}
Inject it to the spring security configuration and use it like this:
public class WebSecConfig extends WebSecurityConfigurerAdapter {
#Autowired private RestAuthEntryPoint authenticationEntryPoint;
#Autowired
UserDetailsService userDetailsService;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// auth
// .inMemoryAuthentication()
// .withUser("test")
// .password(passwordEncoder().encode("testpwd"))
// .authorities("ROLE_USER");
auth.userDetailsService(userDetailsService);
auth.authenticationProvider(authenticationProvider());
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/securityNone")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
I pushed the code on the github link I provided. There you can find a full working example based on:
spring 5
spring security 5
hibernate
h2 DB
Feel free to adapt it to your own scenario
You can use a default spring-security configuration described on various websites, like baeldung.com or mkyong.com. The trick in your sample seems to be the call to get the Merchant. Depending on the complexity of the authenticationService and the Merchant object, you can either use the following code, or implement a facade to get similar behaviour.
#Autowired
public void authenticationManager(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(new AuthenticationProvider() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Merchants merchant = authenticationService.authenticateMerchant(name, password, request.getRemoteAddr());
if(merchant == null) {
throw new AuthenticationException("No Merchant found.");
}
return new UsernamePasswordAuthenticationToken(name, password, merchant.getAuthorities());
}
#Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
});
}
Setting the attribute on the request, if necessary could be done by a separate filter which takes the Principal from the SecurityContext and puts it on the request as an attribute.

Spring web and security. Add /api prefix to login route

I am using spring security. I need to add the /api prefix to the login route. I am extending the UsernamePasswordAuthenticationFilter class
JWTAuthenticationFiler.class
public class JWTAuthenticationFilter extends
UsernamePasswordAuthenticationFilter{
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
User creds = new ObjectMapper()
.readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject(((org.springframework.security.core.userdetails.User) authResult.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SecurityConstants.SECRET.getBytes())
.compact();
response.addHeader(SecurityConstants.HEADER_STRING, token);
}
}
The base class contains in constructor instantiation of the login route:
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
...
I have to override somehow the "/login" to "/api/login". How to do that?
Thank you!
To change the default value for filterProcessesUrl in your custom authentication filter extending UsernamePasswordAuthenticationFilter you have to use setRequiresAuthenticationRequestMatcher, see AbstractAuthenticationProcessingFilter:
This filter will intercept a request and attempt to perform authentication from that request if the request matches the setRequiresAuthenticationRequestMatcher(RequestMatcher).
with a RequestMatcher that matches your custom URL.
You can call the setter in your constructor, in your factory method or in XML configuration.
I suppose you use the WebSecurityConfigurerAdapter. Therefore you have to use the formLogin in order to set the login endpoint.
public class SecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/apilogin*").permitAll()
.anyRequest().authenticated()
.and().formLogin().loginPage("/apilogin")
.and().httpBasic();
}
}
I believe you need to call setRequiresAuthenticationRequestMatcher on the filter when defining the bean in your configuration class.
code.setRequiresAuthenticationRequestMatcher(
new AntPathRequestMatcher("/api/login","POST"));
#Bean
public UsernamePasswordAuthenticationFilter authenticationFilter() {
UsernamePasswordAuthenticationFilter authFilter = new UsernamePasswordAuthenticationFilter();
authFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST"));
return authFilter;
}

Multiple custom authentication with spring security

I have a spring application which uses a custom Authentication Filter say filter1 to authorize the request, this filter uses an authentication manager for authentication and is applicable for all urls in application.
Now, I want to implement a different Authentication Filter say filter2 which has to authorize special kind of request say with url (/api/). That is the all the request which has the url like (/api/**) has to use filter2.
Below is the code I've tried so far for this purpose.
public class SecurityAppConfig {
#Configuration
#Order(1)
public static class APISecurityConfig extends WebSecurityConfigurerAdapter {
private CustomAuthenticationManager1 manager1 = new CustomAuthenticationManager1();
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.formLogin().disable().csrf().disable().cors().disable().logout().disable();
if (manager1 != null) {
http.addFilterAfter(new Filter1(manager1),
AnonymousAuthenticationFilter.class);
}
}
}
#Configuration
#Order(2)
public static class OtherApiSecurityConfig extends WebSecurityConfigurerAdapter {
private AuthenticationManager2 manager2 = new AuthenticationManager2();
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.formLogin().disable().csrf().disable().cors().disable().logout().disable();
if (manager2 != null) {
http.antMatchers("/api/**").addFilterAfter(new Filter2(manager2),
AnonymousAuthenticationFilter.class);
}
}
}
}
At the time of app start up both the filter are getting registered with their manager but when this ("/api/**") request comes it goes to the first filter for authentication but never goes to the second filter. If I remove the first filter then it works properly but that would override the filters for other api request.
Below is how I've implemented managers and filters
public class Filter1 extends AbstractAuthenticationProcessingFilter {
//implementation omitted for brevity.
}
public class Filter2 extends AbstractAuthenticationProcessingFilter {
//implementation omitted for brevity.
}
public class AuthenticationManager1 implements AuthenticationManager {
//implementation omitted for brevity.
}
public class AuthenticationManager2 implements AuthenticationManager {
//implementation omitted for brevity.
}
Any thoughts on how can I get this working.
I don't think that you need two configs for your case. And I don't see why you need to implement your own authentication manager, even two of them. I guess you should use shared authentication manager instead, implement your own AuthenticationProvider (one for each type of authentication), and implement youe own authentication tokens. Besides that, since you're using AbstractAuthenticationProcessingFilter as a base class for you filters - you can set filterProcessesUrl into it, so your filter knows to which URL's it should be applied. So, in brief:
Authentication Tokens:
public class MyAuth1AuthenticationToken extends AbstractAuthenticationToken {
// Implementation depends on you auth scheme (you can look on
// `UsernamePasswordAuthenticationToken` for example)
}
public class MyAuth2AuthenticationToken extends AbstractAuthenticationToken {
// ...
}
Authentication Providers:
public class MyAuth1AuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// Implementation really depends on you auth scheme (you can look on
// `AbstractUserDetailsAuthenticationProvider` for example)
}
#Override
public boolean supports(Class<?> authentication) {
// By this we're saying that this auth provider is responsible for our MyAuth1 auth request
return (MyAuth1AuthenticationToken.class.isAssignableFrom(authentication));
}
}
public class MyAuth2AuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// ...
}
#Override
public boolean supports(Class<?> authentication) {
return (MyAuth2AuthenticationToken.class.isAssignableFrom(authentication));
}
}
Filters:
public class Auth1Filter extends AbstractAuthenticationProcessingFilter {
public Auth1Filter(AuthenticationManager authManager, String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// extract user info here
// ...
// populate auth request with your info
MyAuth1AuthenticationToken authRequest = new MyAuth1AuthenticationToken(...);
// authenticate
return this.getAuthenticationManager().authenticate(authRequest);
}
}
public class Auth2Filter extends AbstractAuthenticationProcessingFilter {
public Auth2Filter(AuthenticationManager authManager, String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// extract user info here
// ...
// populate auth request with your info
MyAuth2AuthenticationToken authRequest = new MyAuth1AuthenticationToken(...);
// authenticate
return this.getAuthenticationManager().authenticate(authRequest);
}
}
Security Config:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception
{
// registering our providers
auth
.authenticationProvider(new MyAuth1AuthenticationProvider())
.authenticationProvider(new MyAuth2AuthenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.csrf().disable()
.cors().disable()
.logout().disable();
AuthenticationManager authManager = http.getSharedObject(AuthenticationManager.class);
http.addFilterAfter(new Auth1Filter(authManager, "/**"), BasicAuthenticationFilter.class);
http.addFilterAfter(new Auth2Filter(authManager, "/api/**"), BasicAuthenticationFilter.class);
}
}
Hope it helps.

How can I have list of all users logged in (via spring security) my web application

I'm using spring security in my web application, and now I want to have a list of all users who are logged in my program.
How can I have access to that list? Aren't they already kept somewhere within spring framework? Like SecurityContextHolder or SecurityContextRepository?
For accessing the list of all logged in users you need to inject SessionRegistry instance to your bean.
#Autowired
#Qualifier("sessionRegistry")
private SessionRegistry sessionRegistry;
And then using injcted SessionRegistry you can access the list of all principals:
List<Object> principals = sessionRegistry.getAllPrincipals();
List<String> usersNamesList = new ArrayList<String>();
for (Object principal: principals) {
if (principal instanceof User) {
usersNamesList.add(((User) principal).getUsername());
}
}
But before injecting session registry you need to define session management part in your spring-security.xml (look at Session Management section in Spring Security reference documentation) and in concurrency-control section you should set alias for session registry object (session-registry-alias) by which you will inject it.
<security:http access-denied-page="/error403.jsp" use-expressions="true" auto-config="false">
<security:session-management session-fixation-protection="migrateSession" session-authentication-error-url="/login.jsp?authFailed=true">
<security:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" expired-url="/login.html" session-registry-alias="sessionRegistry"/>
</security:session-management>
...
</security:http>
In JavaConfig, it would look like this:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
// ...
http.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry());
}
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
#Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
}
}
With the calling code looking like this:
public class UserController {
#Autowired
private SessionRegistry sessionRegistry;
public void listLoggedInUsers() {
final List<Object> allPrincipals = sessionRegistry.getAllPrincipals();
for(final Object principal : allPrincipals) {
if(principal instanceof SecurityUser) {
final SecurityUser user = (SecurityUser) principal;
// Do something with user
System.out.println(user);
}
}
}
}
Note that SecurityUser is my own class which implements UserDetails.
Please correct me if I'm wrong.
I think #Adam's answer is incomplete. I noticed that sessions already expired in the list were appearing again.
public class UserController {
#Autowired
private SessionRegistry sessionRegistry;
public void listLoggedInUsers() {
final List<Object> allPrincipals = sessionRegistry.getAllPrincipals();
for (final Object principal : allPrincipals) {
if (principal instanceof SecurityUser) {
final SecurityUser user = (SecurityUser) principal;
List<SessionInformation> activeUserSessions =
sessionRegistry.getAllSessions(principal,
/* includeExpiredSessions */ false); // Should not return null;
if (!activeUserSessions.isEmpty()) {
// Do something with user
System.out.println(user);
}
}
}
}
}
Hope it helps.
Please correct me if I'm wrong too.
I think #Adam's and #elysch`s answer is incomplete. I noticed that there are needed to add listener:
servletContext.addListener(HttpSessionEventPublisher.class);
to
public class AppInitializer implements WebApplicationInitializer {
#Override
public void onStartup(ServletContext servletContext) {
...
servletContext.addListener(HttpSessionEventPublisher.class);
}
with security conf:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
// ...
http.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry());
}
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
And then you will get current online users!
You need to inject SessionRegistry (as mentioned eariler) and then you can do it in one pipeline like this:
public List<UserDetails> findAllLoggedInUsers() {
return sessionRegistry.getAllPrincipals()
.stream()
.filter(principal -> principal instanceof UserDetails)
.map(UserDetails.class::cast)
.collect(Collectors.toList());
}
Found this note to be quite important and relevant:
"[21] Authentication by mechanisms which perform a redirect after
authenticating (such as form-login) will not be detected by
SessionManagementFilter, as the filter will not be invoked during the
authenticating request. Session-management functionality has to be
handled separately in these cases."
https://docs.spring.io/spring-security/site/docs/3.1.x/reference/session-mgmt.html#d0e4399
Also, apparently a lot of people have troubles getting sessionRegistry.getAllPrincipals() returning something different from an empty array. In my case, I fixed it by adding the sessionAuthenticationStrategy to my custom authenticationFilter:
#Bean
public CustomUsernamePasswordAuthenticationFilter authenticationFilter() throws Exception {
...
authenticationFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
}
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
//cf. https://stackoverflow.com/questions/32463022/sessionregistry-is-empty-when-i-use-concurrentsessioncontrolauthenticationstrate
public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
List<SessionAuthenticationStrategy> stratList = new ArrayList<>();
SessionFixationProtectionStrategy concStrat = new SessionFixationProtectionStrategy();
stratList.add(concStrat);
RegisterSessionAuthenticationStrategy regStrat = new RegisterSessionAuthenticationStrategy(sessionRegistry());
stratList.add(regStrat);
CompositeSessionAuthenticationStrategy compStrat = new CompositeSessionAuthenticationStrategy(stratList);
return compStrat;
}
Similar to #rolyanos solution, mine for me always works:
- for the controller
#RequestMapping(value = "/admin")
public String admin(Map<String, Object> model) {
if(sessionRegistry.getAllPrincipals().size() != 0) {
logger.info("ACTIVE USER: " + sessionRegistry.getAllPrincipals().size());
model.put("activeuser", sessionRegistry.getAllPrincipals().size());
}
else
logger.warn("EMPTY" );
logger.debug(log_msg_a + " access ADMIN page. Access granted." + ANSI_RESET);
return "admin";
}
- for the front end
<tr th:each="activeuser, iterStat: ${activeuser}">
<th><b>Active users: </b></th> <td align="center" th:text="${activeuser}"></td>
</tr>
- for spring confing
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
#Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.logout()
.logoutSuccessUrl("/home")
.logoutUrl("/logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
http.authorizeRequests()
.antMatchers("/", "/home")
.permitAll()
.antMatchers("/admin")
.hasRole("ADMIN")
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/home")
.defaultSuccessUrl("/main")
.permitAll()
.and()
.logout()
.permitAll();
http.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry());
http.authorizeRequests().antMatchers("/webjars/**").permitAll();
http.exceptionHandling().accessDeniedPage("/403");
}

Resources