Spring Cloud Gateway OpenID Connect with Keycloak

In this post, we’ll explore how to secure microservices architectures with Spring Cloud Gateway, Resource Servers, and Keycloak.

· Prerequisites
· Overview
∘ How the authentication flow works
· Setting up a Keycloak Instance
∘ Start Keycloak instance
∘ Create a realm
∘ Creating a Client
∘ Create Users
· Spring Cloud Gateway implementation
∘ application.yml
∘ Security Configuration
· Spring Resource Server implementation
∘ Configure application settings
∘ JwtGrantedAuthoritiesConverter
∘ Security Configuration
∘ Resource server Endpoints
· Testing
· Conclusion
· References


Prerequisites

This is the list of all the prerequisites:

Overview

Spring Cloud Gateway offers a powerful and flexible approach to routing requests, enforcing security, and integrating cross-cutting concerns at the system’s edge. When combined with Keycloak, an open-source identity and access management solution, we can implement robust security using OpenID Connect (OIDC) with minimal effort. Additionally, downstream microservices can be secured as Resource Servers, validating JWT tokens issued by Keycloak to authorize incoming requests.

In this post, we’ll learn how to:

  • Configure Spring Cloud Gateway as an OAuth2 client to authenticate users via Keycloak
  • Secure backend services as Resource Servers that trust the tokens from Keycloak
  • Relay access tokens from the gateway to downstream services using Spring Security’s TokenRelay
  • Extract roles and authorities from tokens for fine-grained access control

The complete project will contain the following modules:

  • Keycloak — OpenID Connect provider running in a Docker container. (port 8080)
  • Spring Cloud Gateway handles only routing and OIDC authentication (port 8081)
  • A separate Spring Boot application acts as the resource server (port 8082)

How the authentication flow works

1. Initial Request

  • Client requests to the protected endpoint (/resource/data)
  • Gateway detects an unauthenticated request

2. OIDC Redirection

  • Gateway redirects to Keycloak’s authorization endpoint
  • User authenticates via the Keycloak login page

3. Token Exchange

  • After a successful login, Keycloak returns an authorization code
  • Gateway exchanges code for JWT (access token)

4. Token Relay

  • Gateway forwards the original request to the Resource Server
  • Includes JWT in Authorization: Bearer header

5. Token Validation

  • Resource Server validates JWT against Keycloak’s JWKS endpoint
  • Keycloak confirms token validity

6. Response

  • Validated request proceeds to business logic
  • Response flows back through the chain to the client

Setting up a Keycloak Instance

Start Keycloak instance

The official documentation provides several methods for installing a Keycloak server. This post uses the Docker approach. To do so, we will use the Skycloak website to generate the Docker Compose Keycloak file.

Ensure your machine or container platform can provide sufficient memory and CPU for your desired usage of Keycloak. See Concepts for sizing CPU and memory resources for more on how to get started with production sizing.

We’ll use the latest version of Keycloak (currently 26.1.2). Here is the full content of the docker-compose file:

version: "3"
services:
keycloak:
image: quay.io/keycloak/keycloak:26.1.2
command: start-dev
environment:
KC_DB: postgres
KC_DB_URL_HOST: postgres
KC_DB_URL_DATABASE: keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: password
KC_DB_SCHEMA: public
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- "8080:8080"
- "9000:9000"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/health/live"]
interval: 10s
timeout: 10s
retries: 20

postgres:
image: postgres:15
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak"]
interval: 10s
timeout: 5s
retries: 5

volumes:
postgres_data:

Let’s start the Keycloak service by running the following command:

$ docker compose up -d

Now, Keycloak is exposed on the local port 8080 and creates an initial admin user with the username admin and password admin.

Create a realm

Let’s create a new realm. Click on the master realm on the left side, and then click Create Realm.

  • Enter a name for the realm and click Create.

The current realm is now set to the realm you just created. You can switch between realms by clicking the realm name in the top left corner.

Creating a Client

The next step is to create a new client in Keycloak.

  • Click on “Clients”.
  • Click on “Create Client”.
  • Complete the form as shown in the following screenshot:

Leave the other data as default and browse until you click the Save button.

  • In the Client Settings tab

We’ll enable Client Authentication and keep the “Standard Flow” checked, which allows us to use the OAuth2 mechanism. Additionally, we’ll set a “Valid redirect URI” to “http://localhost:8081/login/oauth2/code/keycloak and can leave the rest of the default settings to save this configuration. The redirect URI refers to our Spring Cloud Gateway application, which will run on port 8081.

  • In the Client Roles tab, let’s add new roles

Create Users

Let’s create users based on the following table:

Spring Cloud Gateway implementation

Let’s create a simple Spring Boot project from start.spring.io, with the following dependencies: OAuth2 Client and Gateway.

application.yml

Here is the full content of the gateway’s application.yaml file:

server:
port: 8081


keycloak-client:
server-url: http://localhost:8080 # Base URL of Keycloak
realm: bootlabs # Realm name in Keycloak
client-id: gateway-labs # Client registered in that realm
client-secret: Yoqb9MsXvDGSxxOtqzTXLC8OYmrPmXXy # Client secret (for confidential clients)


spring:
application:
name: api-gateway
cloud:
gateway:
default-filters: #(1)
- TokenRelay=
routes: #(2)
- id: resource-server
uri: ${RESOURCE_SERVER_URI:http://localhost:8082}
predicates:
- Path=/resource/**
filters:
- StripPrefix=1

security: #(3)
oauth2:
client:
registration:
keycloak:
client-id: ${keycloak-client.client-id}
client-secret: ${keycloak-client.client-secret}
redirect-uri: ${OAUTH2_REDIRECT_URI:{baseUrl}/login/oauth2/code/keycloak}
authorization-grant-type: authorization_code
scope: openid,profile,email
provider:
keycloak:
issuer-uri: ${keycloak-client.server-url}/realms/${keycloak-client.realm}
user-name-attribute: preferred_username
  1. TokenRelay: is used to automatically add the OAuth2 access token (JWT) to downstream requests (in the Authorization header).
  2. routesdefines routing rules. If the route matches requests to /resource/**, it forwards the request to http://localhost:8082 (or whatever ${RESOURCE_SERVER_URI} is set to). StripPrefix=1 Removes the first path segment before forwarding the request (/resource/api becomes /api)
  3. Configures the OAuth2/OpenID Connect client (the gateway itself) to authenticate via Keycloak.

Security Configuration

Here is the SecurityConfig.java file for the Spring Gateway application:

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
).oauth2Login(withDefaults()) // Enables OAuth2 login
.csrf(ServerHttpSecurity.CsrfSpec::disable); // Disable CSRF for APIs

return http.build();
}


@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.setAllowedHeaders(List.of("*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
//config.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

return new CorsWebFilter(source);
}
}

Since we’re using Spring Cloud Gateway with the reactive version, we annotate the security configuration with @EnableWebFluxSecurity. The oauth2Login() method is responsible for redirecting an unauthenticated request to the Keycloak login page.

Spring Resource Server implementation

In this section, we will create a simple Spring Boot project from start.spring.io.

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

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

Configure application settings

This Spring Boot configuration sets up a resource server that uses JWT (JSON Web Token) authentication via Keycloak. Here’s the OAuth2 Resource Server configuration by theapplication.yml file:

server:
port: 8082

spring:
application:
name: resource-server

security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/realms/bootlabs
jwk-set-uri: http://localhost:8080/realms/bootlabs/protocol/openid-connect/certs

JwtGrantedAuthoritiesConverter

The CustomJwtGrantedAuthoritiesConverter class is a custom authority extractor used in a Spring Security + OAuth2 Resource Server to extract and convert user roles from a JWT token issued by Keycloak. It implements JwtGrantedAuthoritiesConverter.

public class CustomJwtGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {

private static final String RESOURCE_ACCESS = "resource_access";
private static final String ROLES = "roles";
private static final String ROLE_PREFIX = "ROLE_";
private static final String CLIENT_ID = "gateway-labs";

private final JwtGrantedAuthoritiesConverter defaultConverter = new JwtGrantedAuthoritiesConverter();

@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
// Start with default authorities
Collection<GrantedAuthority> authorities = new HashSet<>(defaultConverter.convert(jwt));

// Attempt to extract client-specific roles from resource_access
extractClientRoles(jwt).stream()
.map(this::formatRole)
.map(SimpleGrantedAuthority::new)
.forEach(authorities::add);

log.debug("Resolved authorities: {}", authorities);
return authorities;
}

private List<String> extractClientRoles(Jwt jwt) {
return Optional.ofNullable(jwt.getClaimAsMap(RESOURCE_ACCESS))
.map(resourceAccess -> resourceAccess.get(CLIENT_ID))
.filter(Map.class::isInstance)
.map(Map.class::cast)
.map(client -> client.get(ROLES))
.filter(List.class::isInstance)
.map(List.class::cast)
.orElse(Collections.emptyList());
}

private String formatRole(String role) {
return role.startsWith(ROLE_PREFIX) ? role : ROLE_PREFIX + role;
}

@Override
public <U> Converter<Jwt, U> andThen(Converter<? super Collection<GrantedAuthority>, ? extends U> after) {
return Converter.super.andThen(after);
}
}

Keycloak stores roles in the JWT under a nested structure like:

{
....
"resource_access": {
"gateway-labs": {
"roles": [
"ROLE_ADMIN"
]
},
......
}

Spring Security’s default JwtGrantedAuthoritiesConverter does not know how to extract roles from that resource_access section — it expects roles in scope or authorities.

This class fills that gap.

Security Configuration

Here is the SecurityConfig.java file for the Spring Resource server application:

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity //Enables Spring Security's web security support.
@EnableMethodSecurity //Enables method-level security annotations like @PreAuthorize and @PostAuthorize
public class SecurityConfig {

@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private String jwkSetUri;

@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
JwtAuthenticationConverter jwtAuthenticationConverter) throws Exception {

http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter) // custom converter (CustomJwtGrantedAuthoritiesConverter) to extract roles.
.jwkSetUri(jwkSetUri) // Uses the injected jwkSetUri to get public keys for signature verification.
)
);

return http.build();
}

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
var converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(new CustomJwtGrantedAuthoritiesConverter());
return converter;
}
}

Resource server Endpoints

Let’s implement the REST controller class. It exposes a secured API endpoint (/info/user) that returns basic authentication details about the currently logged-in user.

@RestController
@Slf4j
@RequestMapping("/info")
public class InfoController {

@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')")
@GetMapping("/user")
public List<ApiResponse> getUserInfo(Principal principal, JwtAuthenticationToken jwtToken) {
List<ApiResponse> response = new ArrayList<>();
response.add(new ApiResponse("principal", principal.getName()));
response.add(new ApiResponse("user", jwtToken.getToken()));
response.add(new ApiResponse("authorities", jwtToken.getAuthorities()));

return response;
}
}

Testing

We can run the applications and test them.

Open a web browser with the following URL: http://localhost:8081/resource/info/user

The gateway application will redirect us to the Keycloak login page.

Conclusion

Well done !!. In this post, we learned how to implement a secure and scalable authentication solution using Spring Cloud Gateway, Keycloak, and resource servers.

The complete source code is available on GitHub.

Support me through GitHub Sponsors.

Thank you for reading!! See you in the next post.

Additional Readings

Manage Keycloak using Admin REST API

Spring Boot 3 — Keycloak Admin Client integration

Keycloak — Multi-Tenancy with Organizations


References

👉 Link to Medium blog

Related Posts