ِAs mentioned earlier in my Insights into Transitioning from Spring Authorization Server, migrating from one identity provider to another is not an easy task, especially when you have wide range of clients.
- Hard to set due dates for the clients' migration: For the client to comply to Keycloak schema, you might wait for so long (really long) until that happen.
- Support old clients: The old versions of the apps will keep using the old identity provider hence you will need to keep maintaining the old identity provider as long as the older versions of the apps are supported.
- Rollback: Rollback to use the old identity provider during the migration is not easy if the clients are to manage that. Especially in the early release, I prefer to have control to route the clients to either the old identity provider or Keycloak. In case of any failures (and until the issue get fixed), we have the flexibility to switch the user back to the old identity provider without any degradation to the user experience.
- Avoid risk SLOs degradation for a non-functional change: Although migrating identity providers is a strategic decision and important to the organization when decided, I wouldn't accept customer experience degradation such move. Easy to switch back and forth helps to avoid that.
All above are enough reasons to aim for a seamless migration and for that to happen we need a Gateway
.
Identity Provider Gateway
The idea here is to have a gateway service to intercept all traffic to the old identity provider, and based on configuration, the gateway routes the request to the proper identity provider. Remember, we talk about seamless migration where Keycloak brokering fits here. We talked about a custom identity provider to allow Keycloak to broker OAuth2.0 without OpenID Connect support. Check it out for more details.
In case of old identity provider, the gateway will just bypass the request, however if Keycloak is enabled for the target client and grant type, the gateway will either rewrite the URL, payload or reply with redirection to comply with Keycloak schemas.
Before we go in details, what about the resource servers?
Resource servers
Well, It depends on how resource servers deal with the bearer tokens and the identity provider. Resource servers may already validating the token early in level of gateway or the validation left for the resource server (e.g. backend service).
You may use the identity provider to validate every JWT or you use stateless JWTs where resource servers are validating on their own, independent on the identity provider.
Validate Keycloak JWTs
I'll assume here a stateless JWTs. In this case, the resource servers validate the token signature (using JSON Web Key Set (JWKS)) and expiration date.
For the resource servers to get the JWKS for Keycloak, the gateway intercept the requests to JWKS and combine the JWKS from Keycloak with the old identity provider one(s). Here, the resource servers have the JWKS for both identity providers and can validate the token signature regardless of the issuer.
Authorization Code flow
Although Authorization Code flow is complex compared to other OAuth2.0 flows but not really when it comes to migration.
The authorization code flow starts with the authorization request, where the client sends a request similar to:
https://authorization-server.com/oauth/authorize
?client_id=a17c21ed
&response_type=code
&state=5ca75bd30
&redirect_uri=https%3A%2F%2Fexample-app.com%2Fauth
&scope=photos
The request will go through group of predicates
and filter(s)
in our gateway service to decide about the request.
Prepare your Keycloak client
To make Keycloak ready to serve the clients, make sure that:
- Keycloak has the same clients as the existing authorization server and secrets.
- Give the clients same grant types as the existing authorization server.
- Create same scopes
- Map remote token claims to Keycloak token
Gateway Service
I use Spring Cloud Gateway to act as my identity gateway. To dynamically control the enabled realms or/and clients for Keycloak, below is the configuration you might need:
keycloak:
enabled: true
authorizeUrl: "https://auth.company.com/keycloak/realms/%s/protocol/openid-connect/auth"
tokenUrl: "https://auth.company.com/keycloak/realms/%s/protocol/openid-connect/token"
realms:
- name: myrealm
enabled: true
clients:
- name: app1
enabled: false
- name: app2
enabled: true
where keycloak.enabled
is a master flag to completely control the traffic to Keycloak. realms
goes into the same direction.
We still need a configuration properties
class to access it, here is it:
@ConfigurationProperties(prefix = "keycloak")
data class KeycloakProperties(
val enabled: Boolean = false,
val realms: List<Realm> = emptyList(),
val tokenUrl: String = "",
val authorizeUrl: String = ""
)
data class Realm(
val name: String,
val enabled: Boolean = false,
val clients: List<Client> = emptyList()
)
data class Client(
val name: String,
val enabled: Boolean = false
)
Gateway Routes
Here is the gateway routes configuration:
spring:
cloud:
gateway:
routes:
- id: kc_authorize
uri: https://auth.company.com
predicates:
- Path=/gatewayservice/oauth/authorize**
- name: KeycloakEnabled
args:
enabled: true
- CookieNegate=OAUTHSESSION,.*
- QueryNegate=scope,kc_idp
filters:
- AuthorizeRedirectToKc
- id: old_idp
uri: https://auth.company.com
predicates:
- Path=/gatewayservice/oauth/**
filters:
- RewritePath=/(?<segment>.*), /oldidp/$\{segment}
Let us speak about every predicate and filter.
Path Predicate
The path
predicate is very straight forward. It is a built-in predicate to decide about the request path. We use it to decide if the request is an authorization request. If request path matches /gatewayservice/oauth/authorize**
, we move to the following predicate.
KeycloakEnabled Predicate
This predicate is to check if target client is Keycloak enabled. Here we use our feature flags to check if Keycloak, realm and target client are enabled.
CookieNegate Predicate
Due to the nature of the authorization code flow where authorization request shows twice. The 1st time when the client first trigger the flow and the 2nd time when the client gets the code
from the identity server.
The cookie negate predicate is to make sure that the authorize request is the 1st one.
class CookieNegateRoutePredicateFactory : CookieRoutePredicateFactory() {
override fun apply(config: Config): Predicate<ServerWebExchange> {
return super.apply(config).negate()
}
}
QueryNegate Predicate
Same as CookieNegate predicate but for query parameters. kc_idp
is our defined scope for the Keycloak client in the brokered authorization server. If the request parameters has kc_idp
scope, the request is brokered already and should be skipped.
class QueryNegateRoutePredicateFactory : QueryRoutePredicateFactory() {
override fun apply(config: Config): Predicate<ServerWebExchange> {
return super.apply(config).negate()
}
}
AuthorizeRedirectToKc filter
Now, all predicates passed and the authorization request should be targeting Keycloak. The AuthorizeRedirectToKc
is to map the request to the equivalent Keycloak URL in response Location
header with HTTP status code 301
.
class AuthorizeRedirectToKcGatewayFilterFactory(
val keycloakService: KeycloakService
) : RedirectToGatewayFilterFactory() {
override fun apply(config: Config): GatewayFilter {
// Set the http status by FOUND and uri empty as it will be calculated and not possible to make it fixed through configuration
config.apply {
config.status = HttpStatus.FOUND.value().toString()
config.url = ""
config.isIncludeRequestParams = true
}
return super.apply(config)
}
override fun apply(httpStatus: HttpStatusHolder, uri: URI, isIncludedRequestParams: Boolean): GatewayFilter {
return GatewayFilter { exchange, chain ->
if (!exchange.response.isCommitted) {
val kcAuthorizeUri = getKcAuthorizeUri(exchange)
if (kcAuthorizeUri == null) {
logger.error { "Can't construct the keycloak authorize request from: <${exchange.request.uri}" }
ServerWebExchangeUtils.setResponseStatus(exchange, HttpStatusHolder(HttpStatus.BAD_REQUEST, null))
return@GatewayFilter exchange.response.setComplete()
}
return@GatewayFilter super.apply(httpStatus, kcAuthorizeUri, isIncludedRequestParams).filter(exchange, chain)
}
return@GatewayFilter Mono.empty<Void>()
}
}
fun getKcAuthorizeUri(exchange: ServerWebExchange): URI? {
val clientId = exchange.request.queryParams[CLIENT_ID]?.first() ?: return null
return keycloakService.authorizeUrl(clientId)
}
companion object {
const val CLIENT_ID = "client_id"
}
}
In this post, we described in details how is the authorization code flow can be routed to Keycloak without any need from the client to change their implementation. In Seamless Migration to Keycloak: Refresh token, I describe how could we make it for the refresh token requests.
I hope you find it useful.
Top comments (0)