DEV Community

Cover image for Seamless Migration to Keycloak: Authorization Code Flow
Mohammed Ammer
Mohammed Ammer

Posted on

Seamless Migration to Keycloak: Authorization Code Flow

ِ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.
identity provider gateway

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
Enter fullscreen mode Exit fullscreen mode

The request will go through group of predicates and filter(s) in our gateway service to decide about the request.
Authorization Code flow

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
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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.
Authorize request mapping to Keycloak

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"
    }

}
Enter fullscreen mode Exit fullscreen mode

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)