DEV Community

Cover image for Seamless Migration to Keycloak: Refresh token
Mohammed Ammer
Mohammed Ammer

Posted on

Seamless Migration to Keycloak: Refresh token

In the previous post, 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, 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:

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

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

Below sequence diagram is to describe in more details the flow:
sequence diagram

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

Gateway Configuration

The gateway configuration should look like below:

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

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

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

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, 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

I hope you find it useful.

Top comments (0)