spring-bootspring-securityspring-security-rest

Spring security post v5 - how to get authentication manager for filter


I am using Spring security 6.1.3 and Spring boot 3.1.3. For learning purposes, I am trying to connect to the secured service via Basic Auth and receive a JWT token. I keep getting a null AuthenticationManager, any way I try to create it.

Request

POST http://localhost:8081/login
Basic Auth in header
JSON Payload: {
    "username": "shopping_list_username",
    "password": "shopping_list_password"
}

ApplicationSecurityConfig.java


@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@EnableGlobalAuthentication
public class ApplicationSecurityConfig {

    @Value("${application.access.username}")
    private String username;
    @Value("${application.access.password}")
    private String password;


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .csrf(AbstractHttpConfigurer::disable)
                .addFilter(new JwtRestAPIAuthenticationFilter(httpSecurity.getSharedObject(AuthenticationManager.class)))
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers(HttpMethod.GET, "/**").hasAnyAuthority("ADMIN")
                        .requestMatchers(HttpMethod.GET, "/login").hasAnyAuthority("ADMIN")
                        .anyRequest().authenticated()
                )
                .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .httpBasic(withDefaults())
                .formLogin(withDefaults());

        return httpSecurity.build();
    }

    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User
                .withUsername(username)
                .password(password)
                .authorities("ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

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

}

JwtRestAPIAuthenticationFilter.java

public class JwtRestAPIAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    public JwtRestAPIAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }


    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {

        try {
            UsernameAndPasswordRequest authenticationRequest =
                    new ObjectMapper().readValue(request.getInputStream(), UsernameAndPasswordRequest.class);
            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    authenticationRequest.getUsername(),
                    authenticationRequest.getPassword()
            );

            return authenticationManager.authenticate(authentication);

        } 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(authResult.getName())
                .setIssuedAt(new Date())
                .setExpiration(java.sql.Date.valueOf(LocalDate.now().plusWeeks(2)))
                .signWith(Keys.hmacShaKeyFor(Utils.SECURE_KEY.getBytes()))
                .compact();
        response.addHeader("Authorization", "Bearer " + token);
    }
}

Error

java.lang.NullPointerException: Cannot invoke "org.springframework.security.authentication.AuthenticationManager.authenticate(org.springframework.security.core.Authentication)" because the return value of "org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.getAuthenticationManager()" is null
    at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:85) 

I tried creating it in a bean

@Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config)

I tried getting it form HttpSecurity object

http.getSharedObject(AuthenticationManager.class)

I tried to adapt my code to the suggestions in Spring's official migration article but I couldn't make it work.


Solution

  • I found a solution for my problem, not sure if it's best practice, but it works.

    I injected AuthenticationConfiguration in the ApplicationSecurityConfig

        public class ApplicationSecurityConfig {
    
        private final AuthenticationConfiguration authenticationConfiguration;
    
        @Autowired
        public ApplicationSecurityConfig(AuthenticationConfiguration configuration) {
            this.authenticationConfiguration = configuration;
        }
    //......
    }
    

    And retrieved the authenticationManager in the Bean:

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

    Sent it to the filter by calling the method :

    httpSecurity
        .addFilter(new JwtRestAPIAuthenticationFilter(authenticationManager()))
    

    Now the jwt is sent back via the response header. After this, I will implement a different service that will call the /login and use the jwt for subsequent requests. Thanks.