nullpointerexceptionkeycloaktoken-exchange

NPE in Keycloak internal token-exchange


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:

  1. I have a client "original" who has his own client-id/client-secret
  2. I created another client "target" and configured the policy for token exchange, assuming the "original" client in that policy.

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.


Solution

  • client configuration

    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.