javaspringauthenticationspring-securitymicrosoft-entra-id

How to add a second authentication provider for Spring Boot if i use Aad Jwts


I want to add a second authentication provider to my spring project but i'm using the Spring Boot Active Directory Starter dependency where i only configure the oauth2 resource server with a keyset uri and an authentication converter.

How would i approach this problem? Can i just add a second authentication provider or is some configuration needed for the aad auth before? If some configuration is needed could you explain this then to me?

Here is my current Spring Security Config

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SpringSecurityConfig implements WebMvcConfigurer {

    @Value("${security.oauth2.resource.jwt.key-uri}")
    private String keySetUri;

    @Value("${security.oauth2.resource.id}")
    private String resourceId;

    @Value("${security.oauth2.issuer}")
    private String issuer;

    @Value("${security.oauth2.scope.access-as-user}")
    private String accessAsUserScope;

    private final FigaroAuthenticationConverter authenticationConverter;

    @Autowired
    public SpringSecurityConfig(FigaroAuthenticationConverter authenticationConverter) {
        this.authenticationConverter = authenticationConverter;
    }

    /**
     * Configures the security filter chain for the application.
     *
     * @param http the HttpSecurity to modify
     * @return the configured SecurityFilterChain
     * @throws Exception if an error occurs while configuring the filter chain
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.cors(corsCustomizer ->
                corsCustomizer.configurationSource(request -> {

                    CorsConfiguration config = new CorsConfiguration();

                    config.setAllowedOrigins(List.of("http://localhost:4200"));
                    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
                    config.setAllowCredentials(true);
                    config.setExposedHeaders(List.of("Authorization"));
                    config.setAllowedHeaders(List.of(
                            "Access-Control-Allow-Headers",
                            "Access-Control-Allow-Origin",
                            "Access-Control-Request-Method",
                            "Access-Control-Request-Headers",
                            "Origin",
                            "Cache-Control",
                            "Content-Type",
                            "Authorization"
                    ));

                    config.setMaxAge(3600L);

                    return config;
                }));

        http.sessionManagement(Customizer.withDefaults())
                .sessionManagement(sessionManagementConfigurer ->
                        sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));


        http.oauth2ResourceServer(oauth2 ->
                oauth2.jwt(jwtConfigurer -> {

                    jwtConfigurer.jwkSetUri(this.keySetUri);
                    jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter());

                }));

        http.authorizeHttpRequests(auth -> {
            auth.requestMatchers(HttpMethod.OPTIONS)
                    .permitAll();
            auth.requestMatchers("/actuator/**", "/api-docs").permitAll();
            auth.anyRequest().authenticated();
        });

        return http.build();
    }

    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(this.authenticationConverter);
        return jwtAuthenticationConverter;
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withJwkSetUri(this.keySetUri).build();
    }
}

I havn't tried anything. But i don't really now where to start. I researched before this post a bit and found out that i could achive this with something like that

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(oAuth2ResourceServerConfigurer);
        auth.authenticationProvider(customAuthenticationProvider);
}

----------------------- EDIT -----------------------

I've tried solving this issue with a request filter that is executed before the authentication filter. I thought i could authenticate it in the filter and proceed normally and it would skip the rest of the authentication but this didn't work they way i expected it.

public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {

    private static final String HEADER_NAME = "X-API-KEY";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        logger.info("Checking for API key");

        String apiKey = request.getHeader(HEADER_NAME);
        
        logger.info(apiKey);

        if (StringUtils.hasText(apiKey)) {

            Authentication auth = new ApiKeyAuthenticationToken(apiKey, List.of(new SimpleGrantedAuthority("ROLE_VIEWER")));
            SecurityContextHolder.getContext().setAuthentication(auth);

            return;

        }

        filterChain.doFilter(request, response);

    }
}

As you can see i set the authentication and return immediatly. This raises now the issue that i get a status code 200 but no response body. This is because i return there, right?

The filter doesn't make very much sense for now, because i havn't added the "checking" part, but for now it is just for testing


Solution

  • You can do this by creating a filter which implements the API Key style validation and adding it to the http filter chain. This will leave the oauth2 capability untouched.

    This is a sample implementation from a current project:

    @Service
    @Slf4j
    public class ApiKeyFilter extends GenericFilterBean {
    
        public ApiKeyFilter(ApplicationProperties applicationProperties) {
            
            try {
                ourApiKey = applicationProperties.getCurata().getOurApiKey();            
            } catch (Exception e) {
                log.error("Problems setting up api-key filter. Will not be able to authenticate Curata callbacks in spite of being indicated in the logs as ready for use");
                ourApiKey = "nonesense";
            }
    
        }
    
      
        public static final String API_KEY_HEADER = "X-API-Key";
    
        private String ourApiKey;
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
    
            String apiKey = resolveApiKey(httpServletRequest);
            if (StringUtils.hasText(apiKey) && apiKey.equals(ourApiKey)) {
                Authentication authentication = getAuthentication();
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
            filterChain.doFilter(servletRequest, servletResponse);
        }
    
        private String resolveApiKey(HttpServletRequest request) {
            String submittedApiKey = request.getHeader(API_KEY_HEADER);
            if (StringUtils.hasText(submittedApiKey)) {
                return submittedApiKey;
            }
            return null;
        }
        
        private Authentication getAuthentication() {
    
            Collection<? extends GrantedAuthority> authorities = Arrays
                .stream(new String[] {AuthoritiesConstants.CURATA})
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    
            User principal = new User("curata", "", authorities);
    
            return new UsernamePasswordAuthenticationToken(principal, "from X-API-Key", authorities);
        }
    
    
    }
    

    And add it to the http filter chain as follows:

    @Component
    @NoArgsConstructor
    public class ApiKeyConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
        @Autowired
        ApplicationProperties applicationProperties;
        
        @Override
        public void configure(HttpSecurity http) {
            ApiKeyFilter customFilter = new ApiKeyFilter(applicationProperties);
            http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
        }
    }