Seamless Migration to Keycloak: Authorization Code Flow
Tags: keycloak, iam, migration, springsecurity
Published: 2024-03-31 22:27:04

ِAs mentioned earlier in my [Insights into Transitioning from Spring Authorization Server](https://dev.to/mohammedalics/seamless-migration-to-keycloak-insights-into-transitioning-from-spring-authorization-server-47l3), 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](https://dev.to/mohammedalics/seamless-migration-to-keycloak-non-oidc-oauth-20-broker-58a6). Check it out for more details. 
![identity provider gateway](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s380gbqd30krxa8eettu.png)


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:

```curl
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. 
![Authorization Code flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4u6wfjp8vrek6y91cac2.png)

### 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](https://dev.to/mohammedalics/seamless-migration-to-keycloak-non-oidc-oauth-20-broker-58a6)

### Gateway Service 
I use [Spring Cloud Gateway](https://spring.io/projects/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:
```yaml
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:
```java
@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:
```yaml
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. 
```java
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. 
```java
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`. 
![Authorize request mapping to Keycloak](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/px1u9irive8lvtk8cck3.png)

```java
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](https://dev.to/mohammedalics/seamless-migration-to-keycloak-refresh-token-4m4f), I describe how could we make it for the refresh token requests. 

I hope you find it useful.