I have this gateway service which is used as a reverse proxy and is built on Spring Cloud Gateway.
It includes a RouteFilter class which uses Spring Cloud Gateway Filter to forward the request either to a custom login-service, to the requested service or proxied to an error-page service. This is based on evaluating a session token, which the login-service creates and stores in a DynamoDB. The RouteFilter also populates some headers with user info, roles for the given service, etc. The RouteFilter also first verifies that the service is healthy. The RouteFilter starts like below (header populating not included).
.
.
.
import .springframework.cloud.gateway.filter.GatewayFilterChain
import .springframework.cloud.gateway.filter.GlobalFilter
import .springframework.core.Ordered
import .springframework.stereotype.Component
import .springframework.web.reactive.function.client.WebClient
import .springframework.web.server.ResponseStatusException
import .springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
@Component
class RouteFilter(
private val config: Configuration,
private val statusService: StatusService,
private val authService: AuthService,
private val webClient: WebClient = WebClient.create(config.loginUrl),
) : GlobalFilter,
Ordered {
override fun filter(
exchange: ServerWebExchange,
chain: GatewayFilterChain,
): Mono<Void> {
val request = exchange.request
val forwardedUrl = request.uri.toString().replaceFirst("http://", "https://")
val serviceName = getServiceName(forwardedUrl)
val sessionId = request.getToken(config.sessionCookieName)
val verificationResult = authService.authenticate(sessionId)
if (!statusService.isServiceHealthy(serviceName)) {
throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE)
}
return when {
verificationResult.isSuccessfulAndUserHasAccess(serviceName) ->
sendUserToService(
serviceName,
exchange,
verificationResult.user!!,
chain,
)
verificationResult.sessionExpired() ->
refreshSessionAndSendUserToService(
exchange,
forwardedUrl,
chain,
)
verificationResult.invalidSession() ->
refreshSessionAndSendUserToService(
exchange,
forwardedUrl,
chain,
)
verificationResult.notSuccessful() -> sendUserToLogin(exchange, forwardedUrl, chain)
else -> accessDenied(serviceName, verificationResult.user!!)
}
}
.
.
.
I want to replace the login-service with Keycloak and I looked at the article / and the repository to get the JWT when logging in with Keycloak. This works great.
package pl.piomin.samples.security.gateway;
import .slf4j.Logger;
import .slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import .springframework.boot.SpringApplication;
import .springframework.boot.autoconfigure.SpringBootApplication;
import .springframework.security.oauth2.client.OAuth2AuthorizedClient;
import .springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import .springframework.web.bind.annotation.GetMapping;
import .springframework.web.bind.annotation.RestController;
import .springframework.web.server.WebSession;
@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());
}
}
The JWT is accessible at /token with the roles the user has for the various clients. However I am not sure how I can apply this to the RouteFilter class in my other project. I am not sure if I need to get a hold of the JWT either. What I really need is to verify user has roles for the given service, populate these roles in a header, before sending request to service.mydomain (where the user originally came from). This is unless the user does not have any roles for the client, in which case it should be proxied to error-pages service.
How can I use Keycloak to log in and access the user token (JWT) inside the RouteFilter, so I can analyse it there and route to the app or proxy to the error-pages service as required?
Below is a sequence diagram trying to explain what I need, when a user has a role for the given client.
I have this gateway service which is used as a reverse proxy and is built on Spring Cloud Gateway.
It includes a RouteFilter class which uses Spring Cloud Gateway Filter to forward the request either to a custom login-service, to the requested service or proxied to an error-page service. This is based on evaluating a session token, which the login-service creates and stores in a DynamoDB. The RouteFilter also populates some headers with user info, roles for the given service, etc. The RouteFilter also first verifies that the service is healthy. The RouteFilter starts like below (header populating not included).
.
.
.
import .springframework.cloud.gateway.filter.GatewayFilterChain
import .springframework.cloud.gateway.filter.GlobalFilter
import .springframework.core.Ordered
import .springframework.stereotype.Component
import .springframework.web.reactive.function.client.WebClient
import .springframework.web.server.ResponseStatusException
import .springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
@Component
class RouteFilter(
private val config: Configuration,
private val statusService: StatusService,
private val authService: AuthService,
private val webClient: WebClient = WebClient.create(config.loginUrl),
) : GlobalFilter,
Ordered {
override fun filter(
exchange: ServerWebExchange,
chain: GatewayFilterChain,
): Mono<Void> {
val request = exchange.request
val forwardedUrl = request.uri.toString().replaceFirst("http://", "https://")
val serviceName = getServiceName(forwardedUrl)
val sessionId = request.getToken(config.sessionCookieName)
val verificationResult = authService.authenticate(sessionId)
if (!statusService.isServiceHealthy(serviceName)) {
throw ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE)
}
return when {
verificationResult.isSuccessfulAndUserHasAccess(serviceName) ->
sendUserToService(
serviceName,
exchange,
verificationResult.user!!,
chain,
)
verificationResult.sessionExpired() ->
refreshSessionAndSendUserToService(
exchange,
forwardedUrl,
chain,
)
verificationResult.invalidSession() ->
refreshSessionAndSendUserToService(
exchange,
forwardedUrl,
chain,
)
verificationResult.notSuccessful() -> sendUserToLogin(exchange, forwardedUrl, chain)
else -> accessDenied(serviceName, verificationResult.user!!)
}
}
.
.
.
I want to replace the login-service with Keycloak and I looked at the article https://piotrminkowski/2020/10/09/spring-cloud-gateway-oauth2-with-keycloak/ and the repository https://github/piomin/sample-spring-security-microservices to get the JWT when logging in with Keycloak. This works great.
package pl.piomin.samples.security.gateway;
import .slf4j.Logger;
import .slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import .springframework.boot.SpringApplication;
import .springframework.boot.autoconfigure.SpringBootApplication;
import .springframework.security.oauth2.client.OAuth2AuthorizedClient;
import .springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import .springframework.web.bind.annotation.GetMapping;
import .springframework.web.bind.annotation.RestController;
import .springframework.web.server.WebSession;
@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());
}
}
The JWT is accessible at /token with the roles the user has for the various clients. However I am not sure how I can apply this to the RouteFilter class in my other project. I am not sure if I need to get a hold of the JWT either. What I really need is to verify user has roles for the given service, populate these roles in a header, before sending request to service.mydomain (where the user originally came from). This is unless the user does not have any roles for the client, in which case it should be proxied to error-pages service.
How can I use Keycloak to log in and access the user token (JWT) inside the RouteFilter, so I can analyse it there and route to the app or proxy to the error-pages service as required?
Below is a sequence diagram trying to explain what I need, when a user has a role for the given client.
Share Improve this question edited Mar 29 at 9:51 Cornelius asked Mar 28 at 15:59 CorneliusCornelius 3612 gold badges7 silver badges14 bronze badges1 Answer
Reset to default 0How can I use Keycloak to log in
with oauth2Login
from spring-security-oauth2-client
. The Boot starter for that is spring-boot-starter-oauth2-client
.
access the user token (JWT)
In the token response at you get within the authorization code flow, you don't get one, but usually 3 tokens: ID, access, and refresh.
ID tokens are always JWTs. Their audience are OAuth2 clients (application with oauth2Login
are clients).
Access tokens may be JWTs (it is with Keycloak). Their audience are OAuth2 resource servers and clients should not try to read it. The only Use of a client for an access token should be for authorizing requests to resource servers.
a RouteFilter class which uses Spring Cloud Gateway Filter to forward the request either to a custom login-service, to the requested service or proxied to an error-page service.
Instead of rewriting such a custom filter, you should consider using:
oauth2Login
to delegate authentication to Keycloak and store tokens in session- the
tokenRelay=
filter to automatically replace session-based authorization with bearer-based authorization when routing a request from the front end to anoauth2ResourceServer
: get the JWT access token in session and set it asAuthorization
header - configure downstream services with
oauth2ResourceServer
: decode the JWT access token in theAuthorization
header and take access dontrol decision based on the claims it contains and accessed resource
I wrote a detailed tutorial for that on Baeldung.