Search code examples
spring-bootspring-securitykeycloakspring-cloudspring-cloud-gateway

Spring Cloud Gateway with Spring Security for Register / Login using JWT Authententication. 403 Forbidden Error


I'm learning SpringBoot for a microservice application project. I'm using Keycloak Client Authentication Route with Service Accounts Role and I've added the keycloak Configuration to the API Gateway Service application.properties. Here's the API Gateway Security Config:


@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {


    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity serverHttpSecurity){

        serverHttpSecurity
                .authorizeExchange(exchange -> exchange
                        .pathMatchers(
                                "/api/**",
                                "/eureka/**"
                        )
                        .permitAll()
                        .anyExchange()
                        .authenticated())
                .csrf()
                .disable()

                .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt);
        return serverHttpSecurity.build();
    }

}

Now I'm creating an Identity Service Application that handles user registration and login apart from the User Service for user management. This service utilizes JWT Authentication with Spring Security AuthenticationProvider, JwtFilter and UserNamePasswordAuthenticationFilter. Following is the Identity Service Security Config.


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {


    private final JwtFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
        httpSecurity
                .cors(withDefaults())
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeRequests(request -> request.regexMatchers(
                        "/api/auth/*",
                        "/v2/api-docs",
                        "/v3/api-docs",
                        "/v3/api-docs/*",
                                "/swagger-resources/",
                                "/swagger-resources/*",
                                "/configuration/ui",
                                "/configuration/security",
                                "/swagger-ui/",
                                "/webjars/*",
                                "/swagger-ui.html"
                        )
                        .permitAll()
                        .anyRequest()
                        .authenticated()
                )
                .sessionManagement(
                        session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .authenticationProvider(authenticationProvider)
                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }
}

Register / Localhost This is the endpoint, on executing a postman POST Request using OAuth2.0 with the JWT Token, I'm getting 403: Forbidden as response. Following is the log from API Gateway Service:

2024-04-12 20:15:02.924 TRACE [xpense-gateway,,] 35685 --- [     parallel-8] o.s.c.g.h.p.PathRoutePredicateFactory    : Pattern "[/eureka/web]" does not match against value "/api/auth/register"
2024-04-12 20:15:02.924 TRACE [xpense-gateway,,] 35685 --- [     parallel-8] o.s.c.g.h.p.PathRoutePredicateFactory    : Pattern "/api/auth/**" matches against value "/api/auth/register"
2024-04-12 20:15:02.924 DEBUG [xpense-gateway,,] 35685 --- [     parallel-8] o.s.c.g.h.RoutePredicateHandlerMapping   : Route matched: identity-service
2024-04-12 20:15:02.924 DEBUG [xpense-gateway,,] 35685 --- [     parallel-8] o.s.c.g.h.RoutePredicateHandlerMapping   : Mapping [Exchange: POST http://localhost:8080/api/auth/register] to Route{id='identity-service', uri=lb://identity-service, order=0, predicate=Paths: [/api/auth/**], match trailing slash: true, gatewayFilters=[[[SpringCloudCircuitBreakerResilience4JFilterFactory name = 'resilience', fallback = /identity-fallback], order = 0]], metadata={}}
2024-04-12 20:15:02.924 DEBUG [xpense-gateway,,] 35685 --- [     parallel-8] o.s.c.g.h.RoutePredicateHandlerMapping   : [ade0fee6-4] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@56568494
2024-04-12 20:15:02.924 DEBUG [xpense-gateway,,] 35685 --- [     parallel-8] o.s.c.g.handler.FilteringWebHandler      : Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@7a3a49e5}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@305881b8}, order = -2147482648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@6dbb3d7d}, order = -1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@3ea9a091}, order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.GatewayMetricsFilter@26495639}, order = 0], [[SpringCloudCircuitBreakerResilience4JFilterFactory name = 'resilience', fallback = /identity-fallback], order = 0], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@6c1b82cd}, order = 10000], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@54687fd0}, order = 10150], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerServiceInstanceCookieFilter@6eaf030c}, order = 10151], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@16f4a3c0}, order = 2147483646], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@b2da3a5}, order = 2147483647], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@acd3460}, order = 2147483647]]
2024-04-12 20:15:02.924 TRACE [xpense-gateway,ffa6cb10b03275d4,5984f380554abaa0] 35685 --- [     parallel-8] o.s.c.g.filter.RouteToRequestUrlFilter   : RouteToRequestUrlFilter start
2024-04-12 20:15:02.924 TRACE [xpense-gateway,ffa6cb10b03275d4,5984f380554abaa0] 35685 --- [     parallel-8] s.c.g.f.ReactiveLoadBalancerClientFilter : ReactiveLoadBalancerClientFilter url before: lb://identity-service/api/auth/register
2024-04-12 20:15:02.925 TRACE [xpense-gateway,ffa6cb10b03275d4,5984f380554abaa0] 35685 --- [     parallel-8] s.c.g.f.ReactiveLoadBalancerClientFilter : LoadBalancerClientFilter url chosen: http://VFIEVOX3.Router:35185/api/auth/register
2024-04-12 20:15:02.945 TRACE [xpense-gateway,,] 35685 --- [r-http-epoll-11] o.s.c.gateway.filter.NettyRoutingFilter  : outbound route: dc99dbca, inbound: [ade0fee6-4] 
2024-04-12 20:15:02.951 TRACE [xpense-gateway,,] 35685 --- [r-http-epoll-11] o.s.c.g.filter.NettyWriteResponseFilter  : NettyWriteResponseFilter start inbound: dc99dbca, outbound: [ade0fee6-4] 
2024-04-12 20:15:02.952 TRACE [xpense-gateway,,] 35685 --- [r-http-epoll-11] o.s.c.g.filter.GatewayMetricsFilter      : spring.cloud.gateway.requests tags: [tag(httpMethod=POST),tag(httpStatusCode=403),tag(outcome=CLIENT_ERROR),tag(routeId=identity-service),tag(routeUri=lb://identity-service),tag(status=FORBIDDEN)]

I know that the KeyCloak mechanism can handle user authentication but I'm trying to build my own mechanism in place in the service from scratch to handle user authentication and authorization.

I tried going through the documentation for Spring Security but I'm getting confused now as I'm not clear on how exactly SecurityFilterChain and SecurityWebFilterChain. SecurityWebFilterChain provides Oauth2ResourceServer with ServerHttpSecurity but it does not handle SessionManagement and sessionCreation which I'm looking forward to implement.

Thank you in advance.


Solution

  • The issue in the code configuration was with the order of the of the web filter chain mechanism.

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
      httpSecurity
        .authorizeRequests(request -> request.regexMatchers(
          "/api/auth/*",
          "/v2/api-docs",
          "/v3/api-docs",
          "/v3/api-docs/*",
          "/swagger-resources/",
          "/swagger-resources/*",
          "/configuration/ui",
          "/configuration/security",
          "/swagger-ui/",
          "/webjars/*",
          "/swagger-ui.html"
        )
        .permitAll()
        .anyRequest()
        .authenticated()
      )
      .cors(withDefaults())
      .csrf(AbstractHttpConfigurer::disable)
      .sessionManagement(
        session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
      )
      .authenticationProvider(authenticationProvider)
      .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
    
      return httpSecurity.build();
    }
    

    As the authorizeRequests was not configured properly and instead cors(withDefaults()) customizer was getting initialized in the original configuration with csrf().
    This caused the authentication allowance and permissions for the pattern matched requests.