I am developing a microservice infrastrucutre, and began by implementing a Spring Cloud Gateway to proxy all my requests. I secured my Gateway with keycloak via the spring-boot-starter-oauth2-client Dependency. I use the TokenRelay Filter to append the Bearer to my proxied requests. I basically followed this Blog https://blog.jdriven.com/2019/11/spring-cloud-gateway-with-openid-connect-and-token-relay/
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8090/auth/realms/testrealm
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
spring.security.oauth2.client.registration.keycloak.client-id=yyy
spring.security.oauth2.client.registration.keycloak.client-secret=xxx
spring.cloud.gateway.default-filters[0]=TokenRelay
Now I am routing any requests, that match /ping to my Ping MicroService.
spring.cloud.gateway.routes[0].id=some-id
spring.cloud.gateway.routes[0].uri=http://localhost:8079
spring.cloud.gateway.routes[0].predicates[0]=Path=/ping
The Ping MicroService is a spring-boot-starter-oauth2-resource-server, and configured as such, and only exposes one test endpoint /ping.
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8090/auth/realms/testrealm
@Configuration
@EnableWebSecurity
@RestController
public class SecurityConfig extends KeyCloakSecurityConfig {
@GetMapping("/ping")
public String ping() {
return "hello";
}
// ... resource server configuration in KeyCloakSecurityConfig
}
Now then I access my gateway (which is running on 8765), via http://localhost:8765/ping I get correctly redirected to the login-page of keycloak. I log in with my testuser, and get redirected to my gateway, which will then proxy my request to the Ping MicroService. The PingMicroService validates the AccessToken again against Keycloak and I receive my "hello".
But if I would want to test my API with for example Postman, I would assume, that I could just add the Bearer-Token to the Authorization Header before calling my /ping endpoint. So I get my Accesstoken with the Token-Endpoint http://localhost:8090/auth/realms/testrealm/protocol/openid-connect/token, and copy it into the Authorization Header of my Gateway-Request. But after sending, I get the login-page, and not the requested resource.
Why am I not getting authenticated by my gateway when I am sending my valid bearer-token in the authorization header?
When I am directly calling my resource server, the bearer-token is accepted. That makes sense, because the api-gateway is not doing anything else, than relaying my access-token to the resource-server, so he can do the necessary token introspection.
After some research, I found out that Spring actually sends me a SESSION as a cookie, when I am doing my api-call via the browser. When I copy this SESSION as a cookie into my request in postman, all works fine. Is there some documentation on why Spring uses a SESSION, or is this basically the Auth-Code because we are using the authorization code flow here?
Update: After some research I found out, that my gateway indeed is already stateless, so the sessioncookie does not come from my gateway, but rather from the oauth2-client dependency (see How to make API Gateway Stateless for Authentication/Authorization Process Using Oauth2?). So I reproduced my problem, with a simple Spring Boot App, setting up OAuth2login with keycloak and exposing one endpoint /ping. See https://github.com/smotastic/spring-oauth2-client-keycloak If you call /ping with a valid Bearer Access-Token, the application will redirect you to the Login-Page of keycloak, not accepting the token, because it wants a valid SESSION-Cookie
Create the Controller class with the getEmployeeInfo method which returns a jsp page. Finally create the Spring Boot Bootstrap class with SpringBootApplication annotation. Net create the getEmployees. jsp using which we will POST a request to /authorize in form encoded url format.
So for anyone having a similar problem. The problem was in the spring-boot-starter-oauth2-client dependency. This made my gateway stateful, by sending back a SESSION-Cookie instead of an Access-Token from the authorization server.
Unfortunately i couldn't use the official Spring-Boot-Adapter, provided by Keycloak (https://www.keycloak.org/docs/latest/securing_apps/#_spring_boot_adapter) because this Adapter has some web dependencies, and as the spring-cloud-gateway is built on webflux, the web dependencies required by keycloak cannot be used in conjunction.
My solution is, to not use the spring-cloud-gateway anymore, but the spring-cloud-starter-netflix-zuul gateway. This is built on web, and not on webflux, so i was able to use the official Spring-Boot-Adapter by keycloak with it.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
Zuul also sends a JSessionId, but still accepts the accesstoken (if set) in the request header as a valid authorization.
Note that Zuul was placed into Maintenance Mode (https://cloud.spring.io/spring-cloud-netflix/multi/multi__modules_in_maintenance_mode.html). So it won't receive any new features, and it is recommended to use spring-cloud-gateway instead. But for my usecase it wasn't feasable, so i might go back to this gateway in the future, if the oauth2-client releases an update, where i can do stateless authentication as well.
Here is an example repository, which uses the zuul-gateway, protected by keycloak using the official Spring-Boot-Keycloak-Adapter
https://github.com/smo-snippets/spring-cloud-microservices/tree/master/api-gateway
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With