Spring Cloud Gateway 403 Handling Issue: Both authenticationEntryPoint and accessDeniedHandler Called Simultaneously

71 Views Asked by At

I am facing a problem with Spring Security in a project where both authenticationEntryPoint and accessDeniedHandler are being called simultaneously when a 403 error occurs.

The project uses Spring Cloud Gateway, Spring Security, and OAuth2 Resource Server. The issue persists regardless of using oauth2ResourceServer or exceptionHandling.

I've created a new project from zero only to show these scenarios

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'

    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
    //testImplementation 'org.springframework.security:spring-security-test'
}

application.yml

spring:
  application:
    name: api-gateway-demo
  profiles:
    active: dev
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: ${KEYCLOAK_SERVER_URL:http://localhost:9090}/realms/ecommerce
  cloud:
    gateway:
      routes:
        - id: ORDER-SERVICE
          uri: http://localhost:3001
          predicates:
            - Path=/order/**
        - id: USER-SERVICE
          uri: http://localhost:8082
          predicates:
            - Path=/users/**

server:
  port: 8045

SecurityConfig.class

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
                                                            CustomAuthenticationEntryPoint customAuthenticationEntryPoint,
                                                            CustomAccessDeniedHandler customAccessDeniedHandler) {
        return http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                .authorizeExchange(
                        (item) -> item
                                .pathMatchers("/order/**").hasRole("user")
                                .anyExchange().authenticated())
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(Customizer.withDefaults())
                        .accessDeniedHandler(customAccessDeniedHandler)
                        .authenticationEntryPoint(customAuthenticationEntryPoint)
                )
                /*.exceptionHandling(exceptionHandling -> exceptionHandling
                        .authenticationEntryPoint(customAuthenticationEntryPoint)
                        .accessDeniedHandler(customAccessDeniedHandler)
                )*/
                .build();
    }

}

CustomAuthenticationEntryPoint.class

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

@Component
public class CustomAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
    private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {

        logger.error("Unauthorized error: {}", ex.getMessage());

        var request =  exchange.getRequest();
        var response =  exchange.getResponse();

        //    response.set(MediaType.APPLICATION_JSON_VALUE);
        response.setStatusCode(HttpStatus.UNAUTHORIZED);

        final Map<String, Object> body = new HashMap<>();
        body.put("status", HttpStatus.UNAUTHORIZED);
        body.put("error", "Unauthorized");
        body.put("message", ex.getMessage());
        body.put("path", request.getURI().getPath());
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json");
        byte[] bytes;
        var objectMapper = new ObjectMapper();
        try {
           bytes = objectMapper.writeValueAsBytes(body);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
        return exchange.getResponse().writeWith(Flux.just(buffer));
    }

}

CustomAccessDeniedHandler.class

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

@Component
public class CustomAccessDeniedHandler implements ServerAccessDeniedHandler {

    private static final Logger logger = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException ex) {

        logger.error("Denied error: {}", ex.getMessage());
        var request =  exchange.getRequest();
        var response =  exchange.getResponse();

        response.setStatusCode(HttpStatus.FORBIDDEN);

        final Map<String, Object> body = new HashMap<>();
        body.put("status", HttpStatus.FORBIDDEN);
        body.put("error", "FORBIDDEN");
        body.put("message", ex.getMessage());
        body.put("path", request.getURI().getPath());
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json");
        byte[] bytes;
        var objectMapper = new ObjectMapper();
        try {
            bytes = objectMapper.writeValueAsBytes(body);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
        return exchange.getResponse().writeWith(Flux.just(buffer));
    }
}

This is the result:

  1. When code 403 happens
2024-01-12T12:11:54.324-05:00 ERROR 46086 --- [api-gateway-demo] [  parallel-5] c.e.d.CustomAuthenticationEntryPoint  : Unauthorized error: Not Authenticated
2024-01-12T12:11:54.326-05:00 ERROR 46086 --- [api-gateway-demo] [  parallel-5] c.e.d.CustomAccessDeniedHandler   : Denied error: Access Denied

2)When code 401 happens

2024-01-12T15:41:47.251-05:00 ERROR 50099 --- [api-gateway-demo] [     parallel-1] c.e.d.CustomAuthenticationEntryPoint     : Unauthorized error: Not Authenticated

I want to log only the relevant exception:

  • Log only CustomAuthenticationEntryPoint for 401 errors.
  • Log only CustomAccessDeniedHandler for 403 errors.

I appreciate any insights or solutions to address this issue.

0

There are 0 best solutions below