'How do i pass a Bearer Token dynamically with Quarkus Microprofile Rest Client?

I'm trying to call a Keycloak Admin REST endpoint to create a keycloak user in my code and it requires the request to have a Bearer token added to the Authorization header. I need a way to pass the token dynamically to the rest client.

I have to call another service to generate the token.

Here's what I've tried and the error that i got. What could be wrong or is there a better way to do this?

SignUpService.java

@ApplicationScoped
public class SignUpService {
    private static final Logger LOG = Logger.getLogger(SignUpService.class);

    @Inject
    @RestClient
    KeycloakClientAdmin keycloakClientAdmin;

    public Response create(KeycloakUserDTO keyCloakUser) {
    ...
    try {
            keycloakClientAdmin.createKeycloakUser(keyCloakUser);
        } catch (Exception e) {
            LOG.info("Unable to create new user");
            e.printStackTrace();
        }
    }
    return Response.ok().build();
}

KeycloakClientAdmin.java

@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RegisterRestClient(configKey = "config.api.keycloak.admin")
@RegisterClientHeaders(KeycloakClientHeader.class)
public interface KeycloakClientAdmin {
    
    @POST
    @Path("/admin/realms/{realm}/users")
    public void createKeycloakUser(keycloakUserDTO user);
}

KeycloakClient.java

@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@RegisterRestClient(configKey = "config.api.keycloak.token")
public interface KeycloakClient {
    

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Path("/realms/master/protocol/openid-connect/token")
    public TokenRepresentation createToken(MultivaluedMap<String, String> formMap);
}

KeycloakClientHeader.java

The token was succesfully created here,I was able to log it, but from the error(status code 401) i got it looks like it wasn't added to the header. The stack trace is at the end.

@ApplicationScoped
public class KeycloakClientHeader implements ClientHeadersFactory{
    private static final Logger LOG = Logger.getLogger(KeycloakClientHeader.class);

    @Inject
    @RestClient
    KeycloakClient keycloakClient;

    @Override
    public MultivaluedMap<String, String> update(
                          MultivaluedMap<String, String> mm1,MultivaluedMap<String, String> mm2) {
        MultivaluedMap<String, String> result = new MultivaluedMapImpl<>();
        MultivaluedMap<String, String> tokenRequest = new MultivaluedMapImpl<>();

        tokenRequest.add("client_id", "admin-cli");
        tokenRequest.add("username", admin);
        tokenRequest.add("password", password);
        tokenRequest.add("grant_type", "password");
        
        String token = null;   
        try {
            token = keycloakClient.createToken(tokenRequest).getAccess_token();
            LOG.info("Token: " + token);
        } catch (Exception e) {
            LOG.info("Unable to create token");
            e.printStackTrace();
        }
        result.add("Authorization", "Bearer  " + token);
        return result;
    }
}

application.properties file

config.api.keycloak.token/mp-rest/url=http://localhost:9000
config.api.keycloak.token/mp-rest/scope=javax.inject.Singleton

config.api.keycloak.admin/mp-rest/url=http://localhost:9000
config.api.keycloak.admin/mp-rest/scope=javax.inject.Singleton

The error:

Unable to create new user
--
org.jboss.resteasy.client.exception.ResteasyWebApplicationException: Unknown error, status code 401
--
        at org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper.wrap(WebApplicationExceptionWrapper.java:107)
--
        at org.jboss.resteasy.microprofile.client.DefaultResponseExceptionMapper.toThrowable(DefaultResponseExceptionMapper.java:21)
--
        at org.jboss.resteasy.microprofile.client.ExceptionMapping$HandlerException.mapException(ExceptionMapping.java:41)
--
        at org.jboss.resteasy.microprofile.client.ProxyInvocationHandler.invoke(ProxyInvocationHandler.java:153)
--
        at com.sun.proxy.$Proxy157.createKeycloakUser(Unknown Source)
--
        at farm.everyfarmer.mobile.useraccount.service.SignUpService.create(SignUpService.java:94)
--
        at farm.everyfarmer.mobile.useraccount.service.SignUpService_Subclass.create$$superforward1(SignUpService_Subclass.zig:137)
--
        at farm.everyfarmer.mobile.useraccount.service.SignUpService_Subclass$$function$$2.apply(SignUpService_Subclass$$function$$2.zig:33)
--
        at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:54)
--
        at io.quarkus.arc.runtime.devconsole.InvocationInterceptor.proceed(InvocationInterceptor.java:62)
--
        at io.quarkus.arc.runtime.devconsole.InvocationInterceptor.monitor(InvocationInterceptor.java:49
--
        at io.quarkus.arc.runtime.devconsole.InvocationInterceptor_Bean.intercept(InvocationInterceptor_Bean.zig:521)
--
        at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:41)
--
        at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:41)
--
        at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:32)
--
        at farm.everyfarmer.mobile.useraccount.service.SignUpService_Subclass.create(SignUpService_Subclass.zig:293)
--
        at farm.everyfarmer.mobile.useraccount.service.SignUpService_ClientProxy.create(SignUpService_ClientProxy.zig:157)
--
        at farm.everyfarmer.mobile.useraccount.resource.SignUpResource.createAcccount(SignUpResource.java:45)
--
        at farm.everyfarmer.mobile.useraccount.resource.SignUpResource_Subclass.createAcccount$$superforward1(SignUpResource_Subclass.zig:94)
--
        at farm.everyfarmer.mobile.useraccount.resource.SignUpResource_Subclass$$function$$1.apply(SignUpResource_Subclass$$function$$1.zig:33)
--
        at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:54)
--
        at io.quarkus.arc.runtime.devconsole.InvocationInterceptor.proceed(InvocationInterceptor.java:62)
--
        at io.quarkus.arc.runtime.devconsole.InvocationInterceptor.monitor(InvocationInterceptor.java:49)
--
        at io.quarkus.arc.runtime.devconsole.InvocationInterceptor_Bean.intercept(InvocationInterceptor_Bean.zig:521)
--
        at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:41)
--
        at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:41)
--
        at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:32)
--
        at farm.everyfarmer.mobile.useraccount.resource.SignUpResource_Subclass.createAcccount(SignUpResource_Subclass.zig:158)
--
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
--
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
--
        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.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:408)
--
        at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:69)
--
        at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:492)
--
        at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:261)
--
        at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:161)
--
        at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364)
--
        at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:164)
--
        at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:247)
--
        at io.quarkus.resteasy.runtime.standalone.RequestDispatcher.service(RequestDispatcher.java:73)
--
        at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.dispatch(VertxRequestHandler.java:138)
--
        at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler$1.run(VertxRequestHandler.java:93)
--
        at io.quarkus.vertx.core.runtime.VertxCoreRecorder$13.runWith(VertxCoreRecorder.java:536)
--
        at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449)
--
        at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478)
--
        at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
--
        at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
--
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
--
        at java.base/java.lang.Thread.run(Thread.java:829)


Solution 1:[1]

Your mistake is having double space, instead of 1 in bearer:

 HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken());

vs

 result.add("Authorization", "Bearer  " + token); <-- Has double space

Fix: result.add("Authorization", "Bearer " + token);

The specification is clear that it should be 1 Single Space:


2.1. Authorization Request Header Field

When sending the access token in the "Authorization" request header field defined by HTTP/1.1 [RFC2617], the client uses the "Bearer"
authentication scheme to transmit the access token.

For example:

 GET /resource HTTP/1.1
 Host: server.example.com
 Authorization: Bearer mF_9.B5f-4.1JqM

The syntax of the "Authorization" header field for this scheme
follows the usage of the Basic scheme defined in Section 2 of
[RFC2617]. Note that, as with Basic, it does not conform to the
generic syntax defined in Section 1.2 of [RFC2617] but is compatible
with the general authentication framework being developed for HTTP 1.1 [HTTP-AUTH], although it does not follow the preferred practice outlined therein in order to reflect existing deployments. The syntax for Bearer credentials is as follows:

 **b64token    = 1*( ALPHA / DIGIT /
                   "-" / "." / "_" / "~" / "+" / "/" ) *"="
 credentials = "Bearer" 1*SP b64token**

https://datatracker.ietf.org/doc/html/rfc6750#section-2.1

Solution 2:[2]

Alternatively, you could just add the keycloak-admin-client module dependency to your project and you don't have to worry about managing most of what you are doing yourself. It is done for you.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 JCompetence
Solution 2 Arthur