How to access HttpServletRequest or HttpSession in spring boot service component - spring-boot

I am trying to access HttpServletRequest or HttpSession in my service component.
The service component is where github OAuth2 login is being processed.
Below is my service code.
#RequiredArgsConstructor
#Service
public class GithubOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final JwtTokenUtil jwtTokenUtil;
private final HttpServletRequest request;
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint()
.getUserNameAttributeName();
OAuthAttributes attributes = OAuthAttributes.ofGithub(userNameAttributeName, oAuth2User.getAttributes());
User user = saveOrFindUser(attributes);
request.setAttribute("token", jwtTokenUtil.generateAccessToken(user.getId(), user.getRole()));
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(user.getRole().name())),
attributes.getAttributes(),
attributes.getNameAttributeKey()
);
}
private User saveOrFindUser(OAuthAttributes attributes) {
Optional<User> optionalUser = userRepository.findByEmail(attributes.getEmail());
if(optionalUser.isPresent()) {
return optionalUser.get();
} else {
return userRepository.save(attributes.toEntity());
}
}
}
And below is my Spring Security configuration class.
#RequiredArgsConstructor
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final GithubOAuth2UserService githubOAuth2UserService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.headers().frameOptions().disable()
.and().csrf().disable()
.cors().configurationSource(corsConfigurationSource())
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/v1/health-check")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler())
.userInfoEndpoint()
.userService(githubOAuth2UserService);
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOriginPattern("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new GithubOAuthExceptionHandler();
}
#Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new GithubOAuthOnSuccessHandler();
}
}
I have tried to autowire HttpSession and HttpServletRequest using Lombok's #RequiredArgsConstructor, and also tried the way below.
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
And I am getting the error below.
java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
I am trying to access HttpServletRequest or HttpSession in a #Service component, but I cannot understand why this error is occuring.
Are there any extra configurations to access these classes in components?
I am using spring boot 2.4.3.

I resolved this issue by using comment's expectation.
The answer was to register RequestContextListener as a spring bean in spring configuration class.
#SpringBootApplication
#EnableJpaAuditing
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
}

I realize that this isn't the question that you asked, but if you are able, I'd recommend moving this work to the request layer instead.
Likely, there's value in your User object being in the SecurityContextHolder so that it can be accessed throughout your application.
So, first, if you create a class like so:
static class MyOAuth2User extends User implements OAuth2User {
public MyOAuth2User(User user, OAuthAttributes attributes) {
super(user);
}
public Map<String, Object> getAttributes() {
return this.attributes.getAttributes();
}
public String getName() {
return getAttribute(this.attributes.getNameAttributeKey());
}
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority(getRole().name()));
}
}
Then that gives you the benefit of your User being a member of the security principal. Additionally, it benefits you because you can access it in your GitHubOAuthOnSuccessHandler, where you already have the HttpServletRequest object:
public void onAuthenticationSuccess(...) {
User user = (User) authentication.getPrincipal();
String token = jwtTokenUtil.generateAccessToken(user.getId(), user.getRole());
request.setAttribute("token", token);
// ...
}

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

Log user information on failed attempt

I want to be able to access my user details when login fail so that I can count the number of times of failed attempt a user has. How can i access the userdetails and save it to my database on failure? I read somewhere that AuthenticationFailureHandler might a be a solution to this but from what I see, it works only with formLogin?
I have a signin endpoint
#CrossOrigin
#RequestMapping(value = "/signin", method = RequestMethod.POST)
#ApiOperation(value = "Sign in endpoint", notes = "You have to provide a valid login request")
public ResponseEntity<?> authenticateUser(#ApiParam(value = "The login request", required = true)
#Valid #RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
// Set authentication so that when /authenticate, we can retrieve the authenticated user
. . .
This is my authentrypointjwt when authenticate fails.
#Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
logger.info("> AuthEntryPointJwt");
logger.error("Unauthorized error: {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
This is my websecurity
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSecurityConfig.class);
#Autowired
UserDetailsServiceImpl userDetailsService;
#Autowired
private AuthEntryPointJwt unauthorizedHandler;
#Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
LOGGER.info("> AuthenticationManagerBuilder authenticationJwtTokenFilter");
return new AuthTokenFilter();
}
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
LOGGER.info("> AuthenticationManagerBuilder authenticationManagerBuilder");
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
LOGGER.info("> AuthenticationManagerBuilder authenticationManagerBean");
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/test/**").authenticated()
.antMatchers("/signup").hasAuthority("SUPER_ADMIN");
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("HEAD",
"GET", "POST", "PUT", "DELETE", "PATCH"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("*"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Yes. AuthenticationFailureHandler will only be called if you customise the filter for authentication by extending AbstractAuthenticationProcessingFilter like what formLogin does.
But it seems that you are now implementing a customised authentication way using spring-mvc-rest , and you can do the following which equivalents to what AbstractAuthenticationProcessingFilter does to invoke AuthenticationFailureHandler :
#RestController
public void AuthenticateController {
#Autowired AuthenticationFailureHandler failureHandler;
#RequestMapping(value = "/signin", method = RequestMethod.POST)
public ResponseEntity<?> authenticateUser(LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) {
try {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
}catch (AuthenticationException failed) {
failureHandler.onAuthenticationFailure(request, response, failed);
}
}
}
P.S. As you are customising authentication using spring-mvc-rest rather than following the spring security infrastructure to implement it based on the Servlet Filter , I assume that you also need to configure spring security to totally ignore AuthenticateController and no other spring security feature will be applied to it . I normally will follow the spring security infrastructure which customize the authentication process based on Servlet Filter as it is more compatible with the spring security ecosystem.

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 Security OAuth2 - Need clarification and help to configure Implicit flow

I am struggling to configure Spring Security OAuth2 to support implicit flow (I had no problems with password or authorization code).
These are the different endpoints:
Authorization server
http://localhost:8082/oauth/authorize
http://localhost:8082/oauth/token
...
Resource server
http://localhost:8081/users (protected resource)
Client
http://localhost:8080/api/users invokes http://localhost:8081/users initiating the OAuth2 dance.
What I see is:
http://localhost:8080/api/users gets redirected to the authorization server with this in the URL: http://localhost:8082/oauth/authorize?client_id=themostuntrustedclientid&response_type=token&redirect_uri=http://localhost:8080/api/accessTokenExtractor
I am prompted with the OAuth approval screen, where I grant all the scopes. Then the browser is redirected to the redirect_uri: http://localhost:8080/api/accessTokenExtractor with a fragment containing the access_token: http://localhost:8080/api/accessTokenExtractor#access_token=3e614eca-4abe-49a3-bbba-1b8eea05c147&token_type=bearer&expires_in=55&scope=read%20write
QUESTIONS:
a. HOW CAN I RESUME AUTOMATICALLY THE EXECUTION OF THE ORIGINAL REQUEST?
The spec defines this behaviour with the access_token as a fragment in the URL: since the fragments aren't sent directly to the servers, we have to use a web page script to extract it and send it to the client (my spring-mvc application). This implies setting a redirect_uri pointing at the script, instead of to the original request:
http://localhost:8080/api/accessTokenExtractor#access_token=3e614eca-4abe-49a3-bbba-1b8eea05c147&token_type=bearer&expires_in=55&scope=read%20write
The accessTokenExtractor web page sends the token to the client. The problem is I don't have the original call (http://localhost:8080/api/users) anymore...
b. Below you can see the client invocation:
restTemplate.getOAuth2ClientContext().getAccessTokenRequest()
.setAll(['client_id': 'themostuntrustedclientid',
'response_type': 'token',
'redirect_uri': 'http://localhost:8080/api/accessTokenExtractor'])
HttpHeaders headers = new HttpHeaders()
ResponseEntity<List<String>> response = restTemplate.exchange('http://localhost:8081/users', HttpMethod.GET, null, new ParameterizedTypeReference<List<String>>(){}, [])
response.getBody()
if I don't set manually the parameters client_id, response_type and redirect_uri (necessary for the UserRedirectRequiredException) the authorization server complains, it needs them. ARE WE EXPECTED TO SET THEM MANUALLY?
The strange thing is that they are available in ImplicitAccessorProvider.obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request):
ImplicitResourceDetails resource = (ImplicitResourceDetails) details;
try {
...
resource contains all of them, however they are not copied to request.
If we compare with AuthorizationCodeAccessTokenProvider here the private method getRedirectForAuthorization() does it automatically...WHY THE DIFFERENCE?
CONFIGURATION:
Authorization Server config:
#EnableAuthorizationServer
#SpringBootApplication
class Oauth2AuthorizationServerApplication {
static void main(String[] args) {
SpringApplication.run Oauth2AuthorizationServerApplication, args
}
}
#Configuration
class OAuth2Config extends AuthorizationServerConfigurerAdapter{
#Autowired
private AuthenticationManager authenticationManager
#Bean
public UserDetailsService userDetailsService() throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager([])
manager.createUser(new User("jose","mypassword", [new SimpleGrantedAuthority("ROLE_USER")]))
manager.createUser(new User("themostuntrustedclientid","themostuntrustedclientsecret", [new SimpleGrantedAuthority("ROLE_USER")]))
return manager
}
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//curl trustedclient:trustedclientsecret#localhost:8082/oauth/token -d grant_type=password -d username=user -d password=cec31d99-e5ee-4f1d-b9a3-8d16d0c6eeb5 -d scope=read
.withClient("themostuntrustedclientid")
.secret("themostuntrustedclientsecret")
.authorizedGrantTypes("implicit")
.authorities("ROLE_USER")
.scopes("read", "write")
.accessTokenValiditySeconds(60)
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(this.authenticationManager);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//security.checkTokenAccess('hasRole("ROLE_RESOURCE_PROVIDER")')
security.checkTokenAccess('isAuthenticated()')
}
}
resource server config and protected endpoint:
#EnableResourceServer
#SpringBootApplication
class Oauth2ResourceServerApplication {
static void main(String[] args) {
SpringApplication.run Oauth2ResourceServerApplication, args
}
}
#Configuration
class OAuth2Config extends ResourceServerConfigurerAdapter{
#Value('${security.oauth2.resource.token-info-uri}')
private String checkTokenEndpointUrl
#Override
public void configure(HttpSecurity http) throws Exception {
http
// Since we want the protected resources to be accessible in the UI as well we need
// session creation to be allowed (it's disabled by default in 2.0.6)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers().antMatchers("/users/**")
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/users").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.PUT, "/users/**").access("#oauth2.hasScope('write')")
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
RemoteTokenServices remoteTokenServices = new RemoteTokenServices()
remoteTokenServices.setCheckTokenEndpointUrl(checkTokenEndpointUrl)
remoteTokenServices.setClientId("usersResourceProvider")
remoteTokenServices.setClientSecret("usersResourceProviderSecret")
resources.tokenServices(remoteTokenServices)
}
}
#RestController
class UsersRestController {
private Set<String> users = ["jose", "ana"]
#GetMapping("/users")
def getUser(){
return users
}
#PutMapping("/users/{user}")
void postUser(#PathVariable String user){
users.add(user)
}
}
And this is the client config:
#EnableOAuth2Client
#SpringBootApplication
class SpringBootOauth2ClientApplication {
static void main(String[] args) {
SpringApplication.run SpringBootOauth2ClientApplication, args
}
}
#Configuration
class SecurityConfig extends WebSecurityConfigurerAdapter{
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.eraseCredentials(false)
.inMemoryAuthentication().withUser("jose").password("mypassword").roles('USER')
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest().hasRole('USER')
.and()
.formLogin()
}
}
#Configuration
class OAuth2Config {
#Value('${oauth.resource:http://localhost:8082}')
private String baseUrl
#Value('${oauth.authorize:http://localhost:8082/oauth/authorize}')
private String authorizeUrl
#Value('${oauth.token:http://localhost:8082/oauth/token}')
private String tokenUrl
#Autowired
private OAuth2ClientContext oauth2Context
#Bean
OAuth2ProtectedResourceDetails resource() {
ImplicitResourceDetails resource = new ImplicitResourceDetails()
resource.setAuthenticationScheme(AuthenticationScheme.header)
resource.setAccessTokenUri(authorizeUrl)
resource.setUserAuthorizationUri(authorizeUrl);
resource.setClientId("themostuntrustedclientid")
resource.setClientSecret("themostuntrustedclientsecret")
resource.setScope(['read', 'write'])
resource
}
#Bean
OAuth2RestTemplate restTemplate() {
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resource(), oauth2Context)
//restTemplate.setAuthenticator(new ApiConnectOAuth2RequestAuthenticator())
restTemplate
}
}
My client has the following controller that invokes a protected aouth2 endpoint from the resource server:
#RestController
class ClientRestController {
#Autowired
private OAuth2RestTemplate restTemplate
def exceptionHandler(InsufficientScopeException ex){
ex
}
#GetMapping("/home")
def getHome(HttpSession session){
session.getId()
}
#GetMapping("/users")
def getUsers(HttpSession session){
println 'Session id: '+ session.getId()
//TODO Move to after authentication
Authentication auth = SecurityContextHolder.getContext().getAuthentication()
restTemplate.getOAuth2ClientContext().getAccessTokenRequest().setAll(['client_id': 'themostuntrustedclientid', 'response_type': 'token', 'redirect_uri': 'http://localhost:8080/api/users'])
HttpHeaders headers = new HttpHeaders()
ResponseEntity<List<String>> response = restTemplate.exchange('http://localhost:8081/users', HttpMethod.GET, null, new ParameterizedTypeReference<List<String>>(){}, [])
response.getBody()
}
}

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