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:
- 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.