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
- 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:
- User opens client and tries to call API through the gateway.
- Gateway redirects user to OAuth2 Authorization Server for login (if using browser flow), or validates a bearer token (for machine-to-machine flows).
- Authorization Server issues a JWT access token.
- Gateway validates JWT and checks roles/authorities.
- 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)
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
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 withROLE_USERorROLE_ADMIN/admin/**– onlyROLE_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();
}
}
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.
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?
- Client calls
GET /productson the gateway. - If not authenticated:
- For browser: user is redirected to Authorization Server login page (OAuth2 login).
- For Postman / machine: request must include
Authorization: Bearer <jwt>.
- Gateway validates JWT (signature, issuer, expired or not).
- Gateway extracts roles from JWT and checks authorization rules (e.g.
hasAnyRole('USER','ADMIN')). - If authorized, gateway forwards request to
product-service:- Rewrites path (
StripPrefix). - Relays the JWT (
TokenRelayadds Authorization header).
- Rewrites path (
product-servicevalidates the token and applies its own authorization rules.- 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
jwtAuthenticationConverteraccordingly. - Remember
hasRole("ADMIN")looks forROLE_ADMINauthority.
11.3. TokenRelay Not Working
- Ensure the user is authenticated at the gateway level (check logs).
- Verify the
Authorizationheader is present when gateway calls the microservice (use logging / Wireshark / mock server). - Make sure you added
TokenRelayand not accidentally removed the header with filters.
11.4. Mixed HTTP/HTTPS Issues
- Always use HTTPS in production between client ↔ gateway.
- Configure correct
redirect-uriandissuer-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).
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).