I'm trying to achieve an internal token exchange in Keycloak 17.0.1, however, the server returns an unknown error (NullPointerException).
My scenario is: I have three microservices, A, B, and C. A calls B, which is an intermediate service that needs to call service C. So, I don't want to propagate the original token (A) to call (C). Instead, I want to exchange the token, so B makes a token-exchange request to Keycloak to get a new token and then calls service C.
What I have done:
And finally the cURL call:
curl -L -X POST 'http://localhost:8080/realms/myrealm/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=target' \
--data-urlencode 'client_secret=<< TARGET SECRET >>' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
--data-urlencode 'subject_token=<< ORIGINAL CLIENT TOKEN >>' \
--data-urlencode 'requested_token_type=urn:ietf:params:oauth:token-type:refresh_token' \
--data-urlencode 'audience=original'
Response:
2022-04-19 16:05:16,154 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-37) Uncaught server error: java.lang.NullPointerException
at org.keycloak.protocol.oidc.TokenManager.attachAuthenticationSession(TokenManager.java:539)
at org.keycloak.protocol.oidc.DefaultTokenExchangeProvider.exchangeClientToOIDCClient(DefaultTokenExchangeProvider.java:336)
at org.keycloak.protocol.oidc.DefaultTokenExchangeProvider.exchangeClientToClient(DefaultTokenExchangeProvider.java:315)
at org.keycloak.protocol.oidc.DefaultTokenExchangeProvider.tokenExchange(DefaultTokenExchangeProvider.java:233)
at org.keycloak.protocol.oidc.DefaultTokenExchangeProvider.exchange(DefaultTokenExchangeProvider.java:123)
at org.keycloak.protocol.oidc.endpoints.TokenEndpoint.tokenExchange(TokenEndpoint.java:789)
at org.keycloak.protocol.oidc.endpoints.TokenEndpoint.processGrantRequest(TokenEndpoint.java:204)
at jdk.internal.reflect.GeneratedMethodAccessor344.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:170)
at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:130)
at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:660)
at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:524)
at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:474)
at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364)
at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:476)
at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:434)
at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:192)
at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:152)
Am I missing something?
UPDATE The only way I've managed it to work was to "force" a session to be created in Keycloak by using a "password" grant type in the request of client A. So, I created a user foo and got a token in this way:
POST http://localhost:{{keycloak_port}}/realms/{{keycloak_realm}}/protocol/openid-connect/token
Authorization: Basic original:12345
Content-Type: application/x-www-form-urlencoded
grant_type=password
&username=foo
&password=bar
This way, a session was created for the client original and the token exchange request for the target client did work.
I'm wondering if it is a correct approach, though.
As I'm using client_credentials OAuth2.0 flow, I had to enable the "Use Refresh Tokens For Client Credentials Grant" in Keycloak (clients/settings/OpenID Connect Compatibility Modes) and then toggle the option mentioned earlier. Although OAuth2.0 states that refresh_tokens should not be used in this flow, I could not find another solution to this. See attached image for more details.