Home > Enterprise >  Unable to set logged user from SecurityContextHolder in Spring Boot
Unable to set logged user from SecurityContextHolder in Spring Boot

Time:06-13

I am trying to implement authentication using JWT in Spring Boot. In the login function I am setting the authentication in the SecurityContextHolder in order to be able to get it when requested. The login functionality works, but when I try to get the current logged user, I am getting unathorized. I debugged and the SecurityContextHolder gives anonymous user. Why is this happening?

UserController class:

@RestController
@CrossOrigin(origins = "http://localhost:3000")
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService;
    
    @Autowired
    private CustomAuthenticationManager authenticationManager;
    
    @Autowired
    private JwtEncoder jwtEncoder;
    
    @PostMapping("/user/login")
      public ResponseEntity<User> login(@RequestBody @Valid AuthDto request) {
        try {
          Authentication authentication = authenticationManager
            .authenticate(new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()));

          String userEmail = (String) authentication.getPrincipal();
          User user = userService.findUserByEmail(userEmail);
          Instant now = Instant.now();
          long expiry = 36000L;

          String scope = authentication.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(joining(" "));

          JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("uni.pu")
            .issuedAt(now)
            .expiresAt(now.plusSeconds(expiry))
            .subject(format("%s,%s", user.getId(), user.getEmail()))
            .claim("roles", scope)
            .build();

          String token = this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
          

          SecurityContextHolder.getContext().setAuthentication(authentication);

          return ResponseEntity.ok()
            .header(HttpHeaders.AUTHORIZATION, token)
            .body(user);
        } catch (BadCredentialsException ex) {
          return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
      }
    
    @GetMapping("/user/current")
    public ResponseEntity<User> getLoggedUser(){
        try{
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            return ResponseEntity.ok()
                    .body((User)auth.getPrincipal());
        }
        catch(Exception e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
         
    }
}

WebSecurityConfig:

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)

public class WebSecurityConfig {
private static final String[] WHITE_LIST_URLS = {"/api/user/login", "/api/user/current"};

@Autowired
private MyUserDetailsService userDetailsService;

@Value("${jwt.public.key}")
private RSAPublicKey rsaPublicKey;
@Value("${jwt.private.key}")
private RSAPrivateKey rsaPrivateKey;

@Bean
public PasswordEncoder encoder() {
    return new BCryptPasswordEncoder(10);
}

@Bean
public DaoAuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
    authProvider.setUserDetailsService(userDetailsService);
    authProvider.setPasswordEncoder(encoder());

    return authProvider;
}

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // Enable CORS and disable CSRF
    http = http.cors().and().csrf().disable();

    // Set session management to stateless
    http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
    // Set unauthorized requests exception handler
    http = http.exceptionHandling(
            (exceptions) -> exceptions.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                    .accessDeniedHandler(new BearerTokenAccessDeniedHandler()));

    http = http.authenticationProvider(authenticationProvider());
    
    // Set permissions on endpoints
    http.authorizeHttpRequests().antMatchers(WHITE_LIST_URLS).permitAll().antMatchers("/api/**").authenticated()
            // Our private endpoints
            .anyRequest().authenticated()
            // Set up oauth2 resource server
            .and().httpBasic(Customizer.withDefaults()).oauth2ResourceServer().jwt();
    
    return http.build();
}

@Bean
public JwtEncoder jwtEncoder() {
    JWK jwk = new RSAKey.Builder(this.rsaPublicKey).privateKey(this.rsaPrivateKey).build();
    JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
    return new NimbusJwtEncoder(jwks);
}

// Used by JwtAuthenticationProvider to decode and validate JWT tokens
@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withPublicKey(this.rsaPublicKey).build();
}

// Extract authorities from the roles claim
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
    jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
}
@Bean
public CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
        throws Exception {
    return authenticationConfiguration.getAuthenticationManager();
}

@Bean
public RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    String hierarchy = "ROLE_ADMIN > ROLE_INSPECTOR \n ROLE_INSPECTOR > ROLE_STUDENT";
    roleHierarchy.setHierarchy(hierarchy);
    return roleHierarchy;
}

}

CodePudding user response:

In Spring documentation, section Storing the SecurityContext between requests says :

Depending on the type of application, there may need to be a strategy in place to store the security context between user operations. In a typical web application, a user logs in once and is subsequently identified by their session Id. The server caches the principal information for the duration session. In Spring Security, the responsibility for storing the SecurityContext between requests falls to the SecurityContextPersistenceFilter, which by default stores the context as an HttpSession attribute between HTTP requests. It restores the context to the SecurityContextHolder for each request and, crucially, clears the SecurityContextHolder when the request completes

So basically, when you create the security context manually no session object is created. Only when the request finishes processing does the Spring Security mechanism realize that the session object is null (when it tries to store the security context to the session after the request has been processed).

At the end of the request Spring Security creates a new session object and session ID. However this new session ID never makes it to the browser because it occurs at the end of the request, after the response to the browser has been made. This causes the new session ID (and hence the Security context containing my manually logged on user) to be lost when the next request contains the previous session ID.

I found two solutions to hande this situation:

1.First solution : Save SecurityContext object in session and then extract it from session when needed :

HttpSession session = request.getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);

and then, extract it from session.

  1. Second solution according to this answer would be to refactor your login function like this:

    private void doAutoLogin(String username, String password, HttpServletRequest request) {
    
     try {
         // Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
         UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
         token.setDetails(new WebAuthenticationDetails(request));
         Authentication authentication = this.authenticationProvider.authenticate(token);
         logger.debug("Logging in with [{}]", authentication.getPrincipal());
         SecurityContextHolder.getContext().setAuthentication(authentication);
     } catch (Exception e) {
         SecurityContextHolder.getContext().setAuthentication(null);
         logger.error("Failure in autoLogin", e);
     }
    

    };

This is how you shoud get authenticationProvider :

@Configuration public class WebConfig extends WebSecurityConfigurerAdapter {  
 
@Bean          
public AuthenticationManager authenticationProvider() throws Exception{  
   return super.authenticationManagerBean();     
  } 
}
  • Related