user3610007
user3610007

Reputation: 121

Springboot webflux throwing 401 when Authorization header sent for unrestricted endpoint

I have a springboot webflux application protected by spring security oauth2. I have both restricted and unrestricted endpoints in the application. The application is throwing 401 when pass Authorization header to unrestricted endpoint. It works fine when I don't pass Authorization header for unrestricted endpoint. I can see that AuthenticationManageris getting executed for both restricted and unrestricted endpoints when Authorization header is passed.

SecurityWebFilterChain bean configuration is given below.

public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity serverHttpSecurity) {
        return serverHttpSecurity
                .requestCache()
                .requestCache(NoOpServerRequestCache.getInstance())
                .and()
                .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
                .exceptionHandling()
                .authenticationEntryPoint((swe, e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)))
                .accessDeniedHandler((swe, e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
                .and().csrf().disable()
                .authorizeExchange()
                .pathMatchers("/api/unrestricted").permitAll()
                .and()
                .authorizeExchange().anyExchange().authenticated()
                .and()
                .oauth2ResourceServer()
                .jwt(jwtSpec -> jwtSpec.authenticationManager(authenticationManager()))
                .authenticationEntryPoint((swe, e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)))
                .accessDeniedHandler((swe, e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
                .and().build();

    }

AuthenticationManager code is like below.

private ReactiveAuthenticationManager authenticationManager() {
        return authentication -> {
            log.info("executing authentication manager");
            return Mono.justOrEmpty(authentication)
                    .filter(auth -> auth instanceof BearerTokenAuthenticationToken)
                    .cast(BearerTokenAuthenticationToken.class)
                    .filter(token -> RSAHelper.verifySigning(token.getToken()))
                    .switchIfEmpty(Mono.error(new BadCredentialsException("Invalid token")))
                    .map(token -> (Authentication) new UsernamePasswordAuthenticationToken(
                            token.getToken(),
                            token.getToken(),
                            Collections.emptyList()
                    ));
        };
    }

We found this issue when one of our API consumers sent dummy Authorization header for unrestricted endpoint.

I can find Spring MVC solution for the similar issue in SpringMVC Oauth2.

I have a working example in the github project demo-security. I have written couple of Integration Tests to explain this issue.

@AutoConfigureWebTestClient
@SpringBootTest
public class DemoIT {

    @Autowired
    private WebTestClient webTestClient;


    @Test
    void testUnrestrictedEndpointWithAuthorizationHeader() {
        webTestClient.get()
                .uri("/api/unrestricted")
                .header(HttpHeaders.AUTHORIZATION, "Bearer token") // fails when passing token
                .exchange()
                .expectStatus().isOk();
    }

    @Test
    void testUnrestrictedEndpoint() {
        webTestClient.get()
                .uri("/api/unrestricted")
                .exchange()
                .expectStatus().isOk();
    }

    @SneakyThrows
    @Test
    void testRestrictedEndpoint() {
        webTestClient.get()
                .uri("/api/restricted")
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + RSAHelper.getJWSToken())
                .exchange()
                .expectStatus().isOk();
    }
}

I'm not sure what might be the problem. Is my Security Config misconfigured ? Any help would be really appreciated.

Upvotes: 0

Views: 3075

Answers (3)

hardEngine
hardEngine

Reputation: 11

I've created a custom Authentication manager which excludes authentication for unrestricted endpoints

@Component
@Primary
public class CustomAuthenticationManager implements 
ReactiveAuthenticationManager {
private final ReactiveAuthenticationManager delegate;
private final PathMatcher pathMatcher;

@Value("#{'${gateway.restricted.path.patterns}'.split(',')}")
private List<String> restrictedPathPatterns;

@Autowired
public CustomAuthenticationManager(
        @Qualifier("CustomReactiveAuthenticationManager")
                ReactiveAuthenticationManager delegate,
        @Qualifier("AntPathMatcher") PathMatcher pathMatcher) {
    this.delegate = delegate;
    this.pathMatcher = pathMatcher;
}

/**
 * Authenticate method
 *
 * @param authentication the {@link Authentication} to test
 * @return
 * @throws AuthenticationException
 */
@Override
public Mono<Authentication> authenticate(Authentication authentication)
        throws AuthenticationException {
    return Mono.deferContextual(
            contextView -> {
                ServerWebExchange exchange = contextView.get(ServerWebExchange.class);
                String path = exchange.getRequest().getURI().getPath();
                if (shouldAuthenticate(path)) {
                    return delegate.authenticate(authentication);
                } else {
                    return Mono.just(authentication);
                }
            });
}

/**
 * Returns if the given endpoint should be authenticated or not
 *
 * @param endpoint
 * @return
 */
private boolean shouldAuthenticate(String endpoint) {
    return restrictedPathPatterns.stream()
            .anyMatch(pattern -> pathMatcher.match(pattern, endpoint));
 }
}

This is how I define the bean in Config

@Bean(name = "CustomReactiveAuthenticationManager")
public ReactiveAuthenticationManager customReactiveAuthenticationManager(
        @Qualifier("AntPathMatcher") PathMatcher pathMatcher) {
    UserDetailsRepositoryReactiveAuthenticationManager delegate =
            new UserDetailsRepositoryReactiveAuthenticationManager(reactiveUserDetailsService);
    return new CustomAuthenticationManager(delegate, pathMatcher);
}

And this is how my security filter chain looks

@Bean
public SecurityWebFilterChain securityWebFilterChain(
        ServerHttpSecurity http,
        ReactiveAuthenticationManager customReactiveAuthenticationManager) {
    http.csrf(ServerHttpSecurity.CsrfSpec::disable)
            .authorizeExchange(
                    authorize ->
                            authorize
                                    .pathMatchers("/actuator/**")
                                    .authenticated()
                                    .anyExchange()
                                    .permitAll())
            .httpBasic(Customizer.withDefaults())
            .authenticationManager(customReactiveAuthenticationManager);
    return http.build();
}

This is how you can configure spring boot webflux security to skip authentication for unauthenticated endpoints which contain Authorization header for whatever reason

Upvotes: 0

Michael
Michael

Reputation: 606

First some helpful info:

Authentication and Authorization are done in separate filters in Spring Security: AuthenticationWebFilter and AuthorizationWebFilter.

First authentication checks for any passed in credentials and puts them into the security context. Later Authorization filter checks for whether access is allowed or not based on your ServerHttpSecurity setup.

In your case, I am not 100% sure, but I think the issue might be that your AuthenticationManager is returning an error if there is not a valid authentication

.switchIfEmpty(Mono.error(new BadCredentialsException("Invalid token")))

What worked for me was returning Mono.empty() if authentication failed:

public Mono<Authentication> authenticate(Authentication authentication) {
        String jwt = authentication.getCredentials().toString(); 

        if (StringUtils.hasText(jwt) && this.tokenProvider.validateToken(jwt)) {
            return Mono.just(this.tokenProvider.getAuthentication(jwt));
        }
        else {
            return Mono.empty();
        }
    }

Upvotes: 0

user3610007
user3610007

Reputation: 121

I have finally managed to solve it with different approach. I moved away from using oauth2ResourceServer() method.

The updated SecurityWebFilterChain bean configuration is given below.

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity serverHttpSecurity,
                                                         BearerTokenConverter bearerTokenConverter) {
        return serverHttpSecurity
                .requestCache()
                .requestCache(NoOpServerRequestCache.getInstance()) // disable cache
                .and()
                .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
                .exceptionHandling()
                .authenticationEntryPoint((swe, e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)))
                .accessDeniedHandler((swe, e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
                .and()
                .csrf().disable().authorizeExchange()
                .pathMatchers("/api/unrestricted")
                .permitAll()
                .anyExchange().access((mono, authorizationContext) -> mono.map(authentication -> new AuthorizationDecision(authentication.isAuthenticated())))
                .and()
                .addFilterAt(authenticationWebFilter(bearerTokenConverter), SecurityWebFiltersOrder.AUTHENTICATION)
                .build();
    }

Instead of using oauth2ResourceServer()I have added custom AuthenticationWebFilter in the chain.

AuthenticationWebFilter code is given below.

private AuthenticationWebFilter authenticationWebFilter(BearerTokenConverter bearerTokenConverter) {
        AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager());
        authenticationWebFilter.setServerAuthenticationConverter(bearerTokenConverter);
        authenticationWebFilter.setRequiresAuthenticationMatcher(new NegatedServerWebExchangeMatcher(pathMatchers("/api/unrestricted")));
        authenticationWebFilter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler((swe, e) -> Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))));
        return authenticationWebFilter;
    }

AuthenticationWebFilter will be executed only for restricted endpoints with the help of authenticationWebFilter.setRequiresAuthenticationMatcher().

Now it works even when we pass Authorization header for unrestricted endpoints. The question that would arise is why should one pass. But we didn't want our API to break with unexpected headers. So we took this approach.

This implementation helped us to solve the issue. But the issue is still there with the previous approach.

I have updated the github project demo-security with the working code.

Upvotes: 2

Related Questions