Spring Cloud Gateway OAuth2 with Keycloak
Spring Cloud Gateway OAuth2 support is a key part of the microservices security process. Of course, the main reason for using an API gateway pattern is to hide services from the external client. However, when we set about hiding our services, we didn’t secure them. In this article, I’m going to show you how to set up Spring Cloud Gateway OAuth2 with Spring Security and Keycloak.
Spring Cloud Gateway is a very useful product. You may take an advantage of the many interesting features it provides. One of them is rate-limiting. You may read more about that in the article Rate Limiting in Spring Cloud Gateway with Redis. It is also worth learning about a circuit breaking and fault tolerance. You may find interesting pieces of information about it in the articles Circuit Breaking in Spring Cloud Gateway with Resilience4j and Timeouts and Retries in Spring Cloud Gateway.
Source code
If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my repository sample-spring-security-microservices. Then you should go to the gateway
directory, and just follow my instructions 🙂 If you are interested in more details about Spring Security you should read its documentation.
Enable OAuth2 in Spring Cloud Gateway
To enable OAuth2 support for the Spring Cloud Gateway application we need to add some dependencies. Of course, the spring-cloud-starter-gateway
dependency is required to enable the gateway feature. We also need to include spring-boot-starter-oauth2-client
enabling Spring Security’s client support for OAuth 2.0 Authorization Framework and OpenID Connect Core 1.0. Finally, we have to add spring-cloud-starter-security
to activate the TokenRelay
filter.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
In the next step, we need to provide the configuration settings for the OAuth2 client. Because we are integrating with Keycloak we should set the name of registrationId (spring.security.oauth2.client.provider.[registrationId]
) to keycloak
. Then we need to set the uris of token
, authorization
and userinfo
endpoints. On the other hand, we can set a value for a single issuer
endpoint. The last important property in that section is user-name-attribute
. Keycloak is returning user login inside the preferred_username
attribute.
We will define two different clients for authorization. The first of them spring-cloud-gateway
contains the scope allowed by our test method, while the second spring-cloud-gateway-2
does not.
spring:
security:
oauth2:
client:
provider:
keycloak:
token-uri: http://localhost:8080/auth/realms/master/protocol/openid-connect/token
authorization-uri: http://localhost:8080/auth/realms/master/protocol/openid-connect/auth
userinfo-uri: http://localhost:8080/auth/realms/master/protocol/openid-connect/userinfo
user-name-attribute: preferred_username
registration:
keycloak-with-test-scope:
provider: keycloak
client-id: spring-with-test-scope
client-secret: c6480137-1526-4c3e-aed3-295aabcb7609
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"
keycloak-without-test-scope:
provider: keycloak
client-id: spring-without-test-scope
client-secret: f6fc369d-49ce-4132-8282-5b5d413eba23
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"
In the last step, we need to configure Spring Security. Since Spring Cloud Gateway is built on top of Spring WebFlux, we need to annotate the configuration bean with @EnableWebFluxSecurity
. Inside the springSecurityFilterChain
method we are going to enable authorization for all the exchanges. We will also set OAuth2 as a default login method and finally disable CSRF.
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange(exchanges -> exchanges.anyExchange().authenticated())
.oauth2Login(withDefaults());
http.csrf().disable();
return http.build();
}
}
Run and configure Keycloak
We are running Keycloak on a Docker container. By default, Keycloak exposes API and a web console on port 8080
. However, that port number must be different than the Spring Cloud Gateway application port, so we are overriding it with 8888
. We also need to set a username and password to the admin console.
$ docker run -d --name keycloak -p 8888:8080 \
-e KEYCLOAK_USER=spring \
-e KEYCLOAK_PASSWORD=spring123 \
jboss/keycloak
Then we need to create two clients with the same names as defined inside the gateway configuration. Both of them need to have confidential
in the “Access Type” section, a valid redirection URI set. We may use a simple wildcard while setting the redirection address as shown below.
The client spring-with-test-scope
will have the scope TEST
assigned. In contrast, the second client spring-without-test-scope
will not have the scope TEST
assigned.
Enable OAuth2 Resourse in Spring Cloud Gateway
Now, we may proceed to the implementation of the downstream application. In order to run it, you need to switch to the callme
directory in the source code. First, we need to include some Maven dependencies. The spring-boot-starter-web
starter provides web support for Spring Boot application. With spring-boot-starter-security
we enable Spring Security for our microservice. The spring-security-oauth2-resource-server
contains Spring Security’s support for OAuth 2.0 Resource Servers. It is also used to protect APIs via OAuth 2.0 Bearer Tokens. Finally, the spring-security-oauth2-jose
module contains Spring Security’s support for the JOSE (Javascript Object Signing and Encryption) framework. The JOSE framework provides a method to securely transfer claims between parties. It supports JWT and JWS (JSON Web Signature).
<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.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
</dependencies>
In the next step, we need to configure a connection to the authorization server. A resource server will use the property spring.security.oauth2.resourceserver.jwt.issuer-uri
to discover the authorization server public keys and then validate incoming JWT tokens.
spring:
application:
name: callme
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/auth/realms/master
We should also provide a Spring Security configuration. First, we need to annotate the Configuration
bean with @EnableWebSecurity
. Then, we should enable annotation-based security for the controller methods. It allows simple role-based access with @PreAuthorize
and @PostAuthorize
. In order to enable a method security feature we need to use annotation @EnableGlobalMethodSecurity
. Finally, we just need to configure Spring Security to authorize all the incoming requests and validate JWT tokens.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
}
Finally, let’s take a look at the implementation of the REST controller class. It a single ping
method. That method may be accessed only by the client with the TEST
scope. It returns a list of assigned scopes from the Authentication
bean.
@RestController
@RequestMapping("/callme")
public class CallmeController {
@PreAuthorize("hasAuthority('SCOPE_TEST')")
@GetMapping("/ping")
public String ping() {
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
return "Scopes: " + authentication.getAuthorities();
}
}
Configure routing on Spring Cloud Gateway
The last step before proceeding to the tests is to configure routing on the Spring Cloud Gateway application. Since the downstream service (callme
) is running on port 8040
we need to set the uri
to http://127.0.0.1:8040
. In order to forward the access token to the callme-service
we have to enable a global filter TokenRelay
. Just to be sure that everything works as expected, we will remove the Cookie
with the session id. The session id is generated on the gateway after performing OAuth2Login
.
spring:
application:
name: gateway
cloud:
gateway:
default-filters:
- TokenRelay
routes:
- id: callme-service
uri: http://127.0.0.1:8040
predicates:
- Path=/callme/**
filters:
- RemoveRequestHeader=Cookie
Finally, let’s take a look at the gateway main class. I added there two useful endpoints. First of them GET /
is returning the HTTP session id. The second of them GET /token
will return the current JWT access token. After the successful Spring Cloud Gateway OAuth2 login, you will see the result from the index
method.
@SpringBootApplication
@RestController
public class GatewayApplication {
private static final Logger LOGGER = LoggerFactory.getLogger(GatewayApplication.class);
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@GetMapping(value = "/token")
public Mono<String> getHome(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
return Mono.just(authorizedClient.getAccessToken().getTokenValue());
}
@GetMapping("/")
public Mono<String> index(WebSession session) {
return Mono.just(session.getId());
}
}
Spring Cloud Gateway OAuth2 testing scenario
First, let’s take a look at the picture that illustrates our use case. We are calling POST /login
endpoint on the gateway (1). After receiving the login request Spring Cloud Gateway try to obtain the access token from the authorization server (2). Then Keycloak is returning the JWT access token. As a result, Spring Cloud Gateway is calling the userinfo
endpoint (3). After receiving the response it is creating a web session and Authentication
bean. Finally, the gateway application is returning a session id to the external client (4). The external client is using a cookie with session-id to authorize requests. It calls GET ping
from the callme
application (5). The gateway
application is forwarding the request to the downstream service (6). However, it removes the cookie and replaces it with a JWT access token. The callme
application verifies an incoming token (7). Finally, it returns 200 OK
response if the client is allowed to call endpoint (8). Otherwise, it returns 403 Forbidded
.
We may start testing in the web browser. First, let’s call the login
endpoint. We have to available clients keycloak-with-test-scope
and keycloak-without-test-scope
. We will use the client keycloak-with-test-scope
.
Then, the gateway redirects us to the Keycloak login
page. We can use the credentials provided during the creation of the Keycloak container.
After a successful login, the gateway will perform the OAuth2 authorization procedure. Finally, it redirects us to the main page. The main page is just a method index
inside the controller. It is returning the current session id.
We can also use another endpoint implemented on the gateway – GET /token
. It is returning the current JWT access token.
$ curl http://localhost:8080/token -H "Cookie: SESSION=9bf852f1-6e00-42f8-a9a2-3cbdced33993"
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0RWpwdkVtQ1ZDZ1VDUm41Y2NJeXRiank0RnR0RXpBRXVrMURoZDRTT0RFIn0.eyJleHAiOjE2MDIyMzM5MTksImlhdCI6MTYwMjIz
MzAxOSwiYXV0aF90aW1lIjoxNjAyMjMzMDE5LCJqdGkiOiIyYWQzYjczNy1mZTdhLTQ3NGUtODhhYy01MGZjYzEzOTlhYTQiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMv
bWFzdGVyIiwiYXVkIjpbIm1hc3Rlci1yZWFsbSIsImFjY291bnQiXSwic3ViIjoiOWVhMDAyYmQtOTQ4Ni00Njk0LWFhYzUtN2IyY2QwNzc2MTZiIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic3ByaW5n
LWNsb3VkLWdhdGV3YXkiLCJzZXNzaW9uX3N0YXRlIjoiMDRhNzQ4YzUtOTA1My00ZmZmLWJjYzctNWY5MThjMzYwZGE4IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjcmVhdGUt
cmVhbG0iLCJST0xFX1RFTExFUiIsIm9mZmxpbmVfYWNjZXNzIiwiYWRtaW4iLCJURUxMRVIiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7Im1hc3Rlci1yZWFsbSI6eyJy
b2xlcyI6WyJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsInZpZXctcmVhbG0iLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsImNyZWF0ZS1jbGllbnQiLCJtYW5hZ2Ut
dXNlcnMiLCJxdWVyeS1yZWFsbXMiLCJ2aWV3LWF1dGhvcml6YXRpb24iLCJxdWVyeS1jbGllbnRzIiwicXVlcnktdXNlcnMiLCJtYW5hZ2UtZXZlbnRzIiwibWFuYWdlLXJlYWxtIiwidmlldy1ldmVu
dHMiLCJ2aWV3LXVzZXJzIiwidmlldy1jbGllbnRzIiwibWFuYWdlLWF1dGhvcml6YXRpb24iLCJtYW5hZ2UtY2xpZW50cyIsInF1ZXJ5LWdyb3VwcyJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5h
Z2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIFRFU1QiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJl
ZF91c2VybmFtZSI6InBpb21pbiJ9.X8XfIHiAiR1YMjiJza75aC294qLwi83RrUU2phorM7FP4phq3h-lx80Zu5xqTrMqMC1-RbHBnX-oUTbs4ViS3DziZlDvoRajdkrh6UTiK5oWgoRW-4qsH5L4X1W
bRfoBZgyHFRSnhaCO4CLgjCyEgeLUR5A-JWY-OMYQIOAxxHB2GwE3MNFfLWeqpmS1AWU8fL0giFFXFDfa1_XZEKgnqe1S75Ps_z8B1sfNfvNpz8daJ8omzXrt6I6TSa0FE3iiZ7Qx18mtkbx-iPuFqDD
RT6DGU-Hing9LnGuOt3Yas-WYdN7PKBigvIZv0LyvRFcilRJQBjOdVfEddL3OQ0rmEg
Just to check, you can decode a JWT token on the https://jwt.io site.
Finally, let’s call the endpoint exposed by the callme
application. We are setting session Cookie
in the request header. The endpoint is returning a list of scopes assigned to the current user. Only user with scope TEST
is allowed to call the method.
$ curl http://localhost:8080/callme/ping -H "Cookie: SESSION=9bf852f1-6e00-42f8-a9a2-3cbdced33993"
Scopes: [SCOPE_profile, SCOPE_email, SCOPE_TEST]
Conclusion
In this article we were discussing important aspects related to microservices security. I showed you how to enable Spring Cloud Gateway OAuth2 support and integrate it with Keycloak. We were implementing such mechanisms like OAuth2 login, token relay, and OAuth2 resource server. The token relay mechanisms will be completely migrated from Spring Cloud Security to Spring Cloud Gateway. Enjoy 🙂
58 COMMENTS