Seamless Migration to Keycloak: Refresh token
Tags: keycloak, springsecurity, iam, migration
Published: 2024-03-31 22:27:08

In the [previous post](https://dev.to/mohammedalics/seamless-migration-to-keycloak-authorization-code-flow-5ehm), we discussed the seamless migration of **Authorization Code flow** from Spring Authorization Server to Keycloak, also the **JWKs** merging to allow the resource servers validating the Keycloak tokens. 

Now we go in further in the migration of the **refresh token** grant type, which "in most cases" has the most share in any identity server traffic. 

---
## Refresh Token
I gave an example of having a [feature flag per client](https://dev.to/mohammedalics/seamless-migration-to-keycloak-authorization-code-flow-5ehm), where you control the client requests routing. 

The same with refresh token requests. If the client is eligible for Keycloak, the next refresh token request must return an _access token_ and _refresh token_ from Keycloak (not the old identity server anymore). 

Again, the user shouldn't be forced to re-login to be identified by Keycloak. This is exactly what is this article for.

Here is a sample refresh token request:
```shell
curl 'https://example.com/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic xxxxxxxxxxxxxxxxxx' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=<REFRESH TOKEN>'
```

### The routing flow
First, we define the pre-conditions for the refresh token routing flow to Keycloak. 
- The request is a `token` request (e.g. /oauth/token)
- Keycloak is `enabled` for the client
- The refresh token issued by the old identity provider

Once all pre-conditions met, the request should be routed to Keycloak. 

Below diagram is to get an abstract overview about the flow:
![abstract diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ufoseddmyknhwo5bsg62.png)

Below sequence diagram is to describe in more details the flow:
![sequence diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ycstcjg4wpx8tu20wk68.png)

Let us move on to discuss the configuration required in the gateway. 

#### Gateway Configuration
The gateway configuration should look like below:  
- 
```yaml
spring:
  cloud:
    gateway:
      routes:
        - id: refresh_token_target_kc
          uri: https://example.com
          predicates:
            - Path=/oauth/token**
            - name: KeycloakEnabled
              args:
                enabled: true
            - name: GrantType
              args:
                hints:
                  grant_type: [ "refresh_token" ]
            - name: JwtClaim
              args:
                hints:
                  claims:
                    iss: "old-identity-provider"
          filters:
            - oldIdpIntrospect
            - KcTokenExchangeModifyRequestBody
            - KcTokenChangeRequestUri
```
##### 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 a token request. If request path matches `/oauth/token**`, we move to the following predicate.

##### KeycloakEnabled predicate 
Keycloak Enabled 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.

##### GrantType predicate 
Grant Type predicate is to check if the token request grant type is `refresh token`. 

##### JwtClaim predicate
Jwt Claim predicate is to check the JWT claims for the token sent by the client. The predicate here is to validate on the issuer. The `iss` should have the name of the old identity provider.

---
If all conditions are met, then the response token and refresh token must be issued by Keycloak. For that, the three filters `oldIdpIntrospect`, `KcTokenExchangeModifyRequestBody` and `KcTokenChangeRequestUri` are there.

#### Gateway Filters 

##### oldIdpIntrospect Filter 
The refresh token sent by the client must be valid and accepted by the old identity provider for the Keycloak to exchange it. To make that happen, I'll use **[OAuth 2.0 Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)** from the old identity provider to validate the refresh token, if the refresh token is `active`, then I call Keycloak to exchange the token. 

You may need to implement the `introspect` endpoint in the old identity provider (for instance Spring Security OAuth) if not exist. Here is an example of introspect request:
```shell
curl --location --globoff 'https://example.com/oauth/introspect' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic <ENCODED_CREDENTIALS>' \
--data-urlencode 'token=<TOKEN>'
```

##### Token Exchange Filters
Although the client is known by Keycloak, the refresh token is unknown. Hence, if we route the traffic to Keycloak, the request will be rejected by default. 

If you are into OAuth 2.0, the first thing may come to your mind is the **[OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)**, where Keycloak exchanges the old identity provider token by an access token and refresh token issued by Keycloak. 

Here is an example of a token exchange request:
```shell
curl --location --globoff 'https://example.com/realms/myrealm/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=<CLIENT_ID>' \
--data-urlencode 'client_secret=<CLIENT_SECRET>' \
--data-urlencode 'subject_token=<REFRESH_TOKEN>' \
--data-urlencode 'subject_issuer=<OLD_IDP_ISSUER>' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
--data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:refresh_token' \
--data-urlencode 'requested_token_type=urn:ietf:params:oauth:token-type:refresh_token' \
--data-urlencode 'scope=<SCOPE>'
```

We created two filters for the Token Exchange to happen
- `KcTokenExchangeModifyRequestBody`: To rewrite the request body to meet the token exchange request body. 
- `KcTokenChangeRequestUri`: To rewrite the URI to meet Keycloak token exchange URI. 

**Notes:**
- Keycloak has to be configured to have token exchange enabled for your client.
- If you have custom identity provider SPI implemented as in [Keycloak: Brokering non-OIDC OAuth 2.0 identities](https://dev.to/mohammedalics/seamless-migration-to-keycloak-non-oidc-oauth-20-broker-58a6), you must override the related token exchange methods for the token exchange to work probably, especially if you will need to map claims from the old tokens to the new one. I have a full detailed article to guide you on how can you make it. See [Seamless Migration to Keycloak: Token Exchange](https://dev.to/mohammedalics/seamless-migration-to-keycloak-token-exchange-4b7p)

I hope you find it useful.