Spring Boot 3 / Spring Security 6, ignore "invalid" authorization headers for unsecured endpoints
Asked Answered
H

3

5

Background information:

I have two different types of authentication happening in one single application in addition to some unsecured endpoints. Let's call these endpoints the following:

  • /non-oauth/**
  • /oauth2/**
  • /unsecured

Non oauth

This is a piece of legacy code that we still need to support for some old clients. These requests contains an Authorization header with a (Bearer) JWT-token issued by a non OAuth2-server.

We have existing security code implemented as a Spring request filter.

OAuth2

We are using Spring Security for these endpoints. The Spring Security config is as follows:

@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity, NTKeycloakAuthProperties ntKeycloakAuthProperties, HandlerExceptionResolver handlerExceptionResolver) throws Exception {

    return httpSecurity
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> {
                if (ntKeycloakAuthProperties.getPaths() == null) {
                    log.warn("[Keycloak Auth] No secure paths have been added to configuration");
                } else {
                    ntKeycloakAuthProperties.getPaths().forEach(path ->
                            authorizationManagerRequestMatcherRegistry.requestMatchers(path).authenticated());
                }

                authorizationManagerRequestMatcherRegistry.requestMatchers("/**").permitAll();
            })
            .oauth2ResourceServer(httpSecurityOAuth2ResourceServerConfigurer -> {
                httpSecurityOAuth2ResourceServerConfigurer.authenticationManagerResolver(
                        new JwtIssuerAuthenticationManagerResolver(ntKeycloakAuthProperties.getIssuers()));
                httpSecurityOAuth2ResourceServerConfigurer.authenticationEntryPoint(
                        new BearerTokenProblemDetailsAuthenticationEntryPoint(handlerExceptionResolver));
            })
            .build();
}

Unsecured

Unsecured endpoints are as named: Unsecured. One can see in the SecurityFilterChain bean config that we add the following matcher:

authorizationManagerRequestMatcherRegistry.requestMatchers("/**").permitAll();

Expectations

I would expect the code to work as follows:

  1. When requesting the /unsecured path without an Authorization-header, the request should be permitted.
  2. When requesting the /oauth2 path with an Authorization-header containing a token issued by our Keycloak server should be permitted
  3. When requesting the /non-oauthpath with an Authorization-header containig a token issued by the legacy server should be permitted

The problem

When requesting the /non-oauth- or /unsecured-endpoint with a token issued by the legacy server, Spring Security will deny the request. I believe this happens because Spring Security cannot Authenticate the user token, even though the user is authorized to access the endpoints (due to the "/**").permitAll() config).

The question

Is there any way to disable the "Authentication-process" of a token on all paths that are not explicitly secured in the config?

Hadwyn answered 7/8, 2023 at 8:53 Comment(0)
E
4

An invalid token in a resource server filter-chain will throw an exception (which results in 401) and access control won't be evaluated (permitAll() is never reached).

In your case, an option is defining 3 different security filter-chain with @Order and securityMatcher for the first two in order (the 3rd will be used as default and process all requests that weren't intercepted). Something a bit like that:

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain unsecuredFilterChain(HttpSecurity httpSecurity) throws Exception {
    return httpSecurity
            .securityMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/unsecured/**")))
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> {
                authorizationManagerRequestMatcherRegistry.requestMatchers("/**").permitAll();
            })
            .build();
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
SecurityFilterChain legacyFilterChain(HttpSecurity httpSecurity) throws Exception {
    return httpSecurity
            .securityMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/non-oauth/**")))
            // your legacy security configuration
            .build();
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
SecurityFilterChain resourceServerFilterChain(HttpSecurity httpSecurity) throws Exception {
    return httpSecurity
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> {
                authorizationManagerRequestMatcherRegistry.requestMatchers("/**").authenticated();
            })
            .oauth2ResourceServer(httpSecurityOAuth2ResourceServerConfigurer -> {
                httpSecurityOAuth2ResourceServerConfigurer.authenticationManagerResolver(
                        new JwtIssuerAuthenticationManagerResolver(ntKeycloakAuthProperties.getIssuers()));
                httpSecurityOAuth2ResourceServerConfigurer.authenticationEntryPoint(
                        new BearerTokenProblemDetailsAuthenticationEntryPoint(handlerExceptionResolver));
            })
            .build();
}

Note that you can use other matcher than AntPathRequestMatcher for securityMatcher: in the case of your legacy authentication mechanism, maybe, would it make more sense to check for a specific header or header format (like an Authorization header which would contain something else than a Bearer string), rather than a path.

Extract answered 8/8, 2023 at 21:44 Comment(1)
Holy Moly. I've lost 2 days on my code 401'ing everything. Your mention of "permitAll() is never reached" and the light bulb went off. Thanks.Deathwatch
X
2

what about WebSecurityCustomizer

  @Bean
  public WebSecurityCustomizer webSecurityCustomizer() {
    return webSecurity -> webSecurity.ignoring().requestMatchers("/path");
  }
Xerography answered 7/8, 2023 at 12:59 Comment(1)
Yes, I have tried this. However this means that I will need to specify each and every path that I wish for it to not secure. I would want to do something like this: webSecurity.ignoring().requestThatDoesNotMatch("/path"); or webSecurity.ignoring().anyPathThatIsNotSecured();Hadwyn
B
0

Just curious if you came up with an acceptable answer to this? I have same scenario in Config Server where I want to lock down actuator endpoints and requests for properties by role, but allow the monitor (webhook) endpoint to work without any Token header or authentication.

The Order solution, with some compiler corrections, just returns Forbidden on the monitor endpoint. The WebSecurityCustomizer solution throws 500 errors.

Of coarse, this works but requires a Authorization header for the monitor endpoint which I don't think is possible with a Git webhook?

Spring 3, Spring Security 6.1.2

    @Bean
    public SecurityFilterChain secureFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
        http.anonymous(AbstractHttpConfigurer::disable);
        http.csrf(AbstractHttpConfigurer::disable);
        http.authorizeHttpRequests(authorize -> authorize
            //Permit webhook refresh access without authorization
            .requestMatchers("/" + appName + "/monitor").permitAll()
            //Can't add appName, clashes with microservice request for config. /config-server/gdelt-ingest/List.of(profiles)
            .requestMatchers(mvcMatcherBuilder.pattern("/actuator/**")).hasRole("actuator")
            //Microservice request for configurations
            .requestMatchers(mvcMatcherBuilder.pattern("/" + appName + "/**/**")).hasRole("m2m")
            .anyRequest()
            .authenticated()
        );

        http.oauth2ResourceServer(authorize ->
            authorize.jwt(jwt -> jwt.jwtAuthenticationConverter(keycloakJwtTokenConverter))
                .authenticationEntryPoint(customAuthenticationEntryPoint)
        );

        http.sessionManagement(session -> session
            .sessionCreationPolicy(stateless? SessionCreationPolicy.STATELESS: SessionCreationPolicy.IF_REQUIRED)
        );
        return http.build();
    }

Bitterroot answered 20/11, 2023 at 22:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.