Spring Boot API Gateway Security with JWT & OAuth2

Spring Boot API Gateway Security – JWT, OAuth2, and Role-Based Access (Complete Guide)

In a microservices architecture, the API Gateway is the single entry point for clients. If the gateway is not secured properly, your entire system is exposed.

In this guide we’ll build a secure Spring Cloud Gateway (Spring Boot 3.x) that uses:

  • OAuth2 / OpenID Connect for authentication (login via Authorization Server like Keycloak)
  • JWT as the access token format
  • Role-based access control (RBAC) at the gateway level
  • Token relay – forward the JWT to downstream microservices
You’ll learn:
  • API Gateway + OAuth2 + JWT architecture
  • Spring Cloud Gateway setup with Spring Security 6
  • Configuring OAuth2 client and resource server at the gateway
  • Extracting roles from JWT and protecting routes
  • Forwarding JWT to downstream services (Token Relay)
  • CORS, CSRF, and best practices for SPA / mobile clients

1. High-Level Architecture

We’ll use a classic, production-style setup:

  • API Gateway – Spring Cloud Gateway (Spring Boot 3.x)
  • Authorization Server – e.g. Keycloak / Auth0 / Spring Authorization Server (OAuth2 provider)
  • Resource Services – e.g. product-service, order-service (Spring Boot REST APIs)
  • Client – SPA / Mobile app / Postman
[ Client ]  ── HTTPS ──►  [ API Gateway ]
                     (OAuth2 login, JWT validation, RBAC)
                                │
                                ▼
                     [ Downstream Microservices ]
                     product-service, order-service, ...

Flow:

  1. User opens client and tries to call API through the gateway.
  2. Gateway redirects user to OAuth2 Authorization Server for login (if using browser flow), or validates a bearer token (for machine-to-machine flows).
  3. Authorization Server issues a JWT access token.
  4. Gateway validates JWT and checks roles/authorities.
  5. If allowed, gateway forwards the request to the appropriate microservice, including the Authorization header.

2. Project Structure

We’ll assume three Spring Boot projects:

  • api-gateway (Spring Cloud Gateway + Security)
  • product-service (protected microservice)
  • authorization-server (Keycloak or another OAuth2 provider; we don’t implement it here, just configure)
You can run Keycloak locally via Docker, or use any OpenID Connect provider. Only URLs & client details differ, the gateway code stays the same.

3. Dependencies for API Gateway (Spring Boot 3.x)

pom.xml for api-gateway:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <!-- Spring Security 6 + OAuth2 client & resource server -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>

    <!-- Optional: Actuator for health & metrics -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

Make sure you’re using a Spring Cloud version that is compatible with Spring Boot 3.x (e.g. Spring Cloud 2023.x release train).


4. Configuring OAuth2 & JWT at the Gateway

The gateway has a dual role:

  • OAuth2 client – browser-based login, redirect to Authorization Server.
  • Resource server – validate bearer JWT tokens on incoming requests.

4.1. application.yml – OAuth2 Client & Resource Server

server:
  port: 8080

spring:
  application:
    name: api-gateway

  cloud:
    gateway:
      default-filters:
        - RemoveRequestHeader=Cookie   # avoid leaking cookies downstream
      routes:
        - id: product-service
          uri: http://localhost:8082
          predicates:
            - Path=/products/**        # http://localhost:8080/products/**
          filters:
            - StripPrefix=1            # /products/** → /**
            - TokenRelay               # forward JWT to downstream

  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: api-gateway-client
            client-secret: YOUR_CLIENT_SECRET
            scope: openid,profile,email
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          keycloak:
            issuer-uri: http://localhost:8080/realms/demo-realm
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8080/realms/demo-realm
Replace issuer-uri, client-id, client-secret with your Authorization Server details (Keycloak, Auth0, Okta, Spring Authorization Server, etc.).

5. Understanding JWT & Role Mapping

A typical JWT from your Authorization Server will contain:

  • sub – subject (user id)
  • preferred_username – username
  • realm_access.roles or roles/authorities
  • scope – OAuth2 scopes

We need to map those into Spring Security authorities like ROLE_ADMIN, ROLE_USER or SCOPE_read.

5.1. Custom JwtAuthenticationConverter

We’ll read roles from the token and convert them to Spring authorities:

@Configuration
public class SecurityJwtConverter {

    @Bean
    public Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {

        JwtGrantedAuthoritiesConverter scopesConverter = new JwtGrantedAuthoritiesConverter();
        scopesConverter.setAuthorityPrefix("SCOPE_");
        scopesConverter.setAuthoritiesClaimName("scope"); // or "scp"

        return jwt -> {
            Collection<GrantedAuthority> authorities = new ArrayList<>();

            // 1. Add scope-based authorities
            authorities.addAll(scopesConverter.convert(jwt));

            // 2. Map realm roles to ROLE_ authorities (Keycloak-style)
            Map<String, Object> realmAccess = jwt.getClaim("realm_access");
            if (realmAccess != null && realmAccess.get("roles") instanceof Collection) {
                Collection<String> roles = (Collection<String>) realmAccess.get("roles");
                roles.stream()
                    .map(role -> "ROLE_" + role.toUpperCase())
                    .map(SimpleGrantedAuthority::new)
                    .forEach(authorities::add);
            }

            return new JwtAuthenticationToken(jwt, authorities);
        };
    }
}

6. Securing Routes in API Gateway (Role-Based Access)

Now we define which roles can call which routes. For example:

  • /products/** – accessible by users with ROLE_USER or ROLE_ADMIN
  • /admin/** – only ROLE_ADMIN
  • /actuator/** – maybe admin-only or internal

6.1. Security Configuration (Spring Security 6)

@Configuration
@EnableWebFluxSecurity
public class GatewaySecurityConfig {

    private final Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter;

    public GatewaySecurityConfig(
        Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter) {
        this.jwtAuthenticationConverter = jwtAuthenticationConverter;
    }

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {

        http
            .csrf(ServerHttpSecurity.CsrfSpec::disable)  // typically disabled for APIs
            .authorizeExchange(exchanges -> exchanges
                .pathMatchers("/actuator/health", "/actuator/info").permitAll()
                .pathMatchers("/login**", "/oauth2/**").permitAll()

                // Role-based access at gateway level
                .pathMatchers("/admin/**").hasRole("ADMIN")
                .pathMatchers("/products/**").hasAnyRole("USER", "ADMIN")

                // Everything else requires authentication
                .anyExchange().authenticated()
            )
            .oauth2Login(Customizer.withDefaults())       // for browser-based login
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter))
            );

        return http.build();
    }
}
🔐 Gateway enforces both authentication and role-based authorization. If the token is missing, invalid, or lacks the required role → request is rejected at the edge.

7. Token Relay – Forwarding JWT to Microservices

Downstream microservices should not perform login again. Instead, the gateway forwards the same Authorization: Bearer <jwt> header.

In application.yml we already added:

filters:
  - StripPrefix=1
  - TokenRelay

The TokenRelay filter (from Spring Security / Spring Cloud) takes the JWT from the authenticated principal and adds it as a Bearer token when calling the downstream service.

This allows each microservice to be a resource server that validates the JWT and optionally performs its own fine-grained authorization.

8. Securing a Downstream Microservice (product-service)

For product-service we just need resource-server configuration. No need for oauth2-client here.

8.1. Dependencies (product-service)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
</dependencies>

8.2. application.yml (product-service)

server:
  port: 8082

spring:
  application:
    name: product-service

  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8080/realms/demo-realm

8.3. Security Config (product-service)

@Configuration
@EnableWebSecurity
public class ProductServiceSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/products/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

        return http.build();
    }
}

Now product-service trusts the same Authorization Server as the gateway. The gateway forwards the user’s JWT; the microservice validates it and enforces more fine-grained authorization if needed.


9. CORS & CSRF Considerations (SPA / Mobile)

If you have a SPA (React/Angular/Vue) talking to the gateway:

  • Configure CORS on the API Gateway: allowed origins, methods, headers.
  • CSRF is typically disabled for pure token-based APIs (no cookies).

9.1. Simple CORS Configuration in Gateway

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://localhost:3000"));
        configuration.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
        configuration.setAllowedHeaders(List.of("Authorization","Content-Type"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

For WebFlux, you can configure CORS through WebFluxConfigurer or directly via ServerHttpSecurity.cors().


10. End-to-End Flow: What Happens on a Request?

  1. Client calls GET /products on the gateway.
  2. If not authenticated:
    • For browser: user is redirected to Authorization Server login page (OAuth2 login).
    • For Postman / machine: request must include Authorization: Bearer <jwt>.
  3. Gateway validates JWT (signature, issuer, expired or not).
  4. Gateway extracts roles from JWT and checks authorization rules (e.g. hasAnyRole('USER','ADMIN')).
  5. If authorized, gateway forwards request to product-service:
    • Rewrites path (StripPrefix).
    • Relays the JWT (TokenRelay adds Authorization header).
  6. product-service validates the token and applies its own authorization rules.
  7. Response flows back through gateway to client.

11. Common Pitfalls & Debugging Tips

11.1. 401 vs 403 Confusion

  • 401 Unauthorized – no/invalid token.
  • 403 Forbidden – token is valid, but user lacks required roles/scopes.

Check logs & token claims carefully.

11.2. Roles Not Being Mapped

  • Confirm where your provider puts roles: realm_access.roles, resource_access, or a custom claim.
  • Update jwtAuthenticationConverter accordingly.
  • Remember hasRole("ADMIN") looks for ROLE_ADMIN authority.

11.3. TokenRelay Not Working

  • Ensure the user is authenticated at the gateway level (check logs).
  • Verify the Authorization header is present when gateway calls the microservice (use logging / Wireshark / mock server).
  • Make sure you added TokenRelay and not accidentally removed the header with filters.

11.4. Mixed HTTP/HTTPS Issues

  • Always use HTTPS in production between client ↔ gateway.
  • Configure correct redirect-uri and issuer-uri (match protocol & host).

12. Best Practices for API Gateway Security

  • Terminate TLS at the gateway and use HTTPS between client & gateway.
  • Keep gateway stateless – rely on JWT, not server sessions.
  • Rate-limit at gateway – protect from abuse & DoS.
  • Centralize cross-cutting concerns (auth, logging, correlation IDs, CORS) at the gateway layer.
  • Monitor gateway metrics & logs (Actuator + distributed tracing).
  • Use short-lived access tokens and refresh tokens (handled by client & Authorization Server, not microservices).
The API Gateway is your security boundary. Implement strict authN + authZ here, and then add microservice-specific rules where needed.

13. Summary & Next Steps

In this guide, we built a secure Spring Boot 3 + Spring Cloud Gateway setup with:

  • OAuth2 login & JWT validation at the gateway
  • Role-based access rules per route
  • Token relay to downstream microservices
  • Resource server configuration in microservices
  • Custom JWT role mapping, CORS, and best practices

With this pattern, you can protect all your microservices behind a single, secure gateway and centralize authentication & authorization logic while still letting each service enforce its own business rules.

Next steps:

  • Integrate with Resilience4j at the gateway for retries and circuit breaking.
  • Add Redis caching for common read endpoints behind the gateway.
  • Add monitoring and tracing across gateway and microservices.

14. FAQ: Spring Boot API Gateway Security with JWT & OAuth2

Q1. Should authentication happen at the API Gateway or in each microservice?

Authentication (validating JWT, checking signature and issuer) can be done at the gateway, the microservices, or both. A common pattern is: gateway validates & enforces coarse-grained rules, microservices optionally enforce fine-grained rules.

Q2. Do microservices need to know about OAuth2?

They don’t need to handle login or redirects, but they should act as resource servers and validate the JWT they receive from the gateway. This avoids trusting the gateway blindly.

Q3. How do I map Keycloak roles to Spring Security roles?

Keycloak typically stores roles under realm_access.roles or resource_access. You can write a custom JwtAuthenticationConverter to read those roles and convert them to ROLE_* authorities.

Q4. Can I use API keys instead of JWT for the gateway?

For machine-to-machine scenarios, API keys may work, but they are weaker than JWT + OAuth2. JWTs carry user identity and roles, are standardized, and are a better choice for most modern microservice systems.

Q5. What about refresh tokens?

Refresh tokens are usually handled between the client and the Authorization Server, not by each microservice. The gateway and microservices should only deal with short-lived access tokens (JWT).