Careeroza
OAuth2 at the Gateway
Two Ways to Use the Gateway
| Mode | Who holds the token? | Gateway role | Best for |
|---|---|---|---|
| Resource server | The client (SPA, mobile app) sends Authorization: Bearer | Validates the JWT and forwards the header | Mobile apps, public APIs, SPAs with their own OIDC library |
| OAuth2 client (BFF) | The gateway keeps tokens in a server session; browser only gets a cookie | Performs login, then adds the token with the TokenRelay filter | Browser apps: tokens never reach JavaScript |
Gateway as a Resource Server
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>api-gateway
application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://keycloak:8180/realms/shopease # downloads the JWKS keys from here
cloud:
gateway:
routes:
- id: products
uri: lb://product-service
predicates:
- Path=/api/products/**
- id: orders
uri: lb://order-service
predicates:
- Path=/api/orders/**@Configuration
@EnableWebFluxSecurity // Spring Cloud Gateway runs on WebFlux
public class GatewaySecurityConfig {
@Bean
SecurityWebFilterChain gatewaySecurity(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable) // stateless bearer-token API
.authorizeExchange(ex -> ex
.pathMatchers(HttpMethod.GET, "/api/products/**").permitAll() // catalogue is public
.pathMatchers("/actuator/health/**").permitAll()
.anyExchange().authenticated())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
.build();
}
}Note: The gateway forwards the Authorization header to the routed service unchanged, so the token reaches order-service automatically. Spring Cloud Gateway 2025.x moves route properties to spring.cloud.gateway.server.webflux.routes; the old prefix still works with a deprecation warning.
Gateway as OAuth2 Client with Token Relay
api-gateway (BFF mode)
For a browser app, let the gateway do the login and keep the tokens. TokenRelay adds the logged-in user's access token to every proxied request:
application.yml
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: http://keycloak:8180/realms/shopease
registration:
shopease-web:
provider: keycloak
client-id: shopease-web
client-secret: ${KEYCLOAK_CLIENT_SECRET} # from the environment, never committed
authorization-grant-type: authorization_code
scope: openid,profile,email
cloud:
gateway:
default-filters:
- TokenRelay= # Authorization: Bearer <access token> on every route
- SaveSession # persist the session (needed with Spring Session/Redis)Example output
GET http://localhost:8080/api/orders (no session cookie)
HTTP/1.1 302 Found
Location: /oauth2/authorization/shopease-web -> Keycloak login page
... after login ...
GET http://localhost:8080/api/orders Cookie: SESSION=8c1f...
-> gateway forwards to order-service with Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6...
HTTP/1.1 200 OK
[{"id":1001,"status":"CONFIRMED","total":2998.00}]@Bean
SecurityWebFilterChain bffSecurity(ServerHttpSecurity http) {
return http
.authorizeExchange(ex -> ex
.pathMatchers("/", "/assets/**", "/api/products/**").permitAll()
.anyExchange().authenticated())
.oauth2Login(Customizer.withDefaults()) // redirect to Keycloak
.csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
.build(); // cookies => keep CSRF ON
}Tip: Add spring-boot-starter-oauth2-client for BFF mode. Because the browser authenticates with a cookie, CSRF protection must stay enabled (the React app reads the XSRF-TOKEN cookie and sends it back as a header).