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>'
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:
Below sequence diagram is to describe in more details the flow:
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
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>'
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>'
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)