Search code examples
spring-securityokta

Use several spring security configuration and apply them according to the calling url


I currently have a backend application that implements a very simple Spring security based on login / password that must be added in the http headers.

I also have a front end that uses OKTA as a provider and works with JWT tokens.

I now want to make the end points dedicated to the front end applications use the JWT token system and all the others use the current login/password system.

I can make my application work with an OKTA configuration or with a login / password configuration but I can't make both work together.

Looking at the different messages on stack overflow I have implemented a double configuration but it is always the first one that is applied. The second one is simply ignored and the endpoints of the perimeter are allowed without any token or login / password

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Configuration
@Order(1)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) throws Exception {

        http.cors();
        http.csrf().disable();

        http
                .authorizeRequests().antMatchers("/api/v1/end-point/**").authenticated()
                .and().oauth2ResourceServer().jwt();

        Okta.configureResourceServer401ResponseBody(http);
    }
}

@Configuration
@Order(2)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {

    @Value("${http.auth-app-id-header-name}")
    private String appIdRequestHeaderName;
    @Value("${http.auth-api-key-header-name}")
    private String apiKeyRequestHeaderName;
    private final AuthenticationManager authenticationManager;

    @Autowired
    public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
        super();
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors();
        http.csrf().disable();

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().addFilter(initAuthenticationFilter())
                .antMatcher("/api/v1/tools/**")
                .authorizeRequests().anyRequest().authenticated();
    }

    private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
        RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,
                apiKeyRequestHeaderName);
        requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
        requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
        return requestHeaderAuthenticationFilter;
    }
}

@Override
@Bean
@Primary
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}
}

In this code, configuration 2 is never used even if I make a call to /api/v1/tools If I remove configuration 1, configuration 2 is applied.

Can you help me to understand what I am doing wrong?


EDIT 1 :

With the help and suggestion of Eleftheria Stein-Kousathana, i change my configuration (and i add Swagger white list configuration)

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

private static final String[] AUTH_WHITELIST = {
        "/v2/api-docs",
        "/swagger-resources/configuration/ui",
        "/swagger-resources",
        "/swagger-resources/configuration/security",
        "/swagger-ui.html",
        "/webjars/**"
};

@Configuration
@Order(1)
public static class SwaggerConfigurationAdapter extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {

        System.out.println("Loading configuration 1");

        http.cors();
        http.csrf().disable();

        http
                .requestMatchers(matchers -> matchers.antMatchers(AUTH_WHITELIST))
                .authorizeRequests(authz -> {
                    authz.anyRequest().permitAll();
                });
    }
}

@Configuration
@Order(2)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {

        System.out.println("Loading configuration 2");

        http.cors();
        http.csrf().disable();

        http
                .requestMatchers(matchers -> matchers.antMatchers("/api/v1/end-point/**"))
                    .authorizeRequests(authz -> {
                        try {
                            authz.anyRequest().authenticated().and().oauth2ResourceServer().jwt();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    });

        Okta.configureResourceServer401ResponseBody(http);
    }
}

@Configuration
@Order(3)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {

    @Value("${algo.http.auth-app-id-header-name}")
    private String appIdRequestHeaderName;
    @Value("${algo.http.auth-api-key-header-name}")
    private String apiKeyRequestHeaderName;
    private final AuthenticationManager authenticationManager;

    @Autowired
    public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
        super();
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        System.out.println("Loading configuration 3");

        http.cors();
        http.csrf().disable();

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().addFilter(initAuthenticationFilter())
                .requestMatchers(matchers -> matchers.antMatchers("/api/**"))
                .authorizeRequests(authz -> {
                    try {
                        authz.anyRequest().authenticated();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
    }

    private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
        RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,
                apiKeyRequestHeaderName);
        requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
        requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
        return requestHeaderAuthenticationFilter;
    }
}

@Override
@Bean
@Primary
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}
}

I feel that I am very close to succeeding

  • Swaggers is accessible when not authenticated
  • The routes corresponding to "/api/v1/end-point/**" need a JWT token otherwise I get a 401 error
  • The routes corresponding to "/api/** " need a login / password otherwise I get a 401 error

But now I have the following error:

Every time I request a page under swagger or make a call to my api, my web browser asks me for a login / password.

If I cancel I can still navigate on Swagger UI and make call to "/api/v1/end-point/**". Every Login / password are rejected even they are valid in configuration 3.

If I don't fill the login / password and make a call to any route of "/api/**" i got the following error :

2021-07-23 14:49:16.642 [http-nio-8081-exec-9] INFO  c.c.a.a.c.CorrelationIdLoggingAspect - Calling api.controller.endpoint.getActivities executed in 197ms.
2021-07-23 14:49:22.247 [http-nio-8081-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [/secret] threw exception [Filter execution threw an exception] with root cause
java.lang.StackOverflowError: null
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
    at com.sun.proxy.$Proxy236.authenticate(Unknown Source)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)
    at jdk.internal.reflect.GeneratedMethodAccessor220.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
    at com.sun.proxy.$Proxy236.authenticate(Unknown Source)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)

Solution

  • Thank you very much for all the answers. We found the solution thanks your help.

    Here the final code for helping everyone who needs to do the same things as us. ​

    Security configuration :

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        private static final String[] AUTH_WHITELIST = {
                "/v2/api-docs",
                "/swagger-resources/configuration/ui",
                "/swagger-resources",
                "/swagger-resources/configuration/security",
                "/swagger-ui.html",
                "/webjars/**"
        };
    
        @Order(1)
        @Configuration
        public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
    
            protected void configure(HttpSecurity http) throws Exception {
    
    
                http
                        .antMatcher("/api/v1/end-point/**")
                        .authorizeRequests((authz) -> authz.anyRequest().authenticated())
                        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                        .sessionManagement((sessionManagement) ->
                                sessionManagement
                                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                        )
                        .cors(withDefaults())
                        .csrf(CsrfConfigurer::disable);
    
                Okta.configureResourceServer401ResponseBody(http);
            }
        }
    
        @Order(2)
        @Configuration
        public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
    
            @Value("${http.app-id-header-name}")
            private String appIdRequestHeaderName;
            @Value("${http.api-key-header-name}")
            private String apiKeyRequestHeaderName;
    
            private final AuthenticationManager authenticationManager;
    
            @Autowired
            public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
                super();
                this.authenticationManager = authenticationManager;
            }
    
            @Override
            public void configure(WebSecurity web) {
                web.ignoring().antMatchers(AUTH_WHITELIST);
            }
    
            @Override
            protected void configure(HttpSecurity http) throws Exception {
    
                http
                        .addFilterAt(initAuthenticationFilter(), UsernameRequestHeaderAuthenticationFilter.class)
                        .authorizeRequests((authorizeRequests) ->
                                authorizeRequests
                                        .anyRequest().authenticated()
                        )
                        .sessionManagement((sessionManagement) ->
                                sessionManagement
                                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                        )
                        .cors(withDefaults())
                        .csrf(CsrfConfigurer::disable);
            }
    
            private UsernameRequestHeaderAuthenticationFilter initAuthenticationFilter() throws Exception {
                UsernameRequestHeaderAuthenticationFilter usernameRequestHeaderAuthenticationFilter = new UsernameRequestHeaderAuthenticationFilter();
                usernameRequestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
                usernameRequestHeaderAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
                usernameRequestHeaderAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
                usernameRequestHeaderAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
                usernameRequestHeaderAuthenticationFilter.setPostOnly(false);
                usernameRequestHeaderAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
                    // Do nothing
                });
                return usernameRequestHeaderAuthenticationFilter;
            }
        }
    }
    

    Authentication filter :

    public class UsernameRequestHeaderAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    
        @Override
        protected String obtainUsername(HttpServletRequest request) {
            return request.getHeader(getUsernameParameter());
        }
    
        @Override
        protected String obtainPassword(HttpServletRequest request) {
            return request.getHeader(getPasswordParameter());
        }
    
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
            super.successfulAuthentication(request, response, chain, authResult);
            chain.doFilter(request, response);
        }
    
        @Override
        protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
    
            super.unsuccessfulAuthentication(request, response, failed);
        }
    }
    

    We have also adapted our AuthenticationManager to use UserAuthorities

    Thanks again to all