OpenID Connect (OIDC) and OAuth2 client and filters

You can use Quarkus extensions for OpenID Connect and OAuth 2.0 access token management, focusing on acquiring, refreshing, and propagating tokens.

This includes the following:

  • Using quarkus-oidc-client, quarkus-rest-client-oidc-filter and quarkus-resteasy-client-oidc-filter extensions to acquire and refresh access tokens from OpenID Connect and OAuth 2.0 compliant Authorization Servers such as Keycloak.

  • Using quarkus-rest-client-oidc-token-propagation and quarkus-resteasy-client-oidc-token-propagation extensions to propagate the current Bearer or Authorization Code Flow access tokens.

The access tokens managed by these extensions can be used as HTTP Authorization Bearer tokens to access the remote services.

OidcClient

Add the following dependency:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-client</artifactId>
</dependency>

The quarkus-oidc-client extension provides a reactive io.quarkus.oidc.client.OidcClient, which can be used to acquire and refresh tokens by using SmallRye Mutiny Uni and Vert.x WebClient.

OidcClient is initialized at build time with the IDP token endpoint URL, which can be auto-discovered or manually configured. OidcClient uses this endpoint to acquire access tokens with token grants such as client_credentials or password, and to refresh tokens with a refresh_token grant.

Token endpoint configuration

By default, the token endpoint address is discovered by adding a /.well-known/openid-configuration path to the configured quarkus.oidc-client.auth-server-url.

For example, given this Keycloak URL:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus

OidcClient will discover that the token endpoint URL is http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens.

If the OIDC provider requires authentication to access the discovery endpoint, you can configure OidcClient to send credentials with the discovery request, as described in Authenticated access to the discovery endpoint.

If the discovery endpoint is unavailable or you want to avoid an extra round-trip to it, you can disable discovery and configure the token endpoint address with a relative path.

For example:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus
quarkus.oidc-client.discovery-enabled=false
# Token endpoint: http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens
quarkus.oidc-client.token-path=/protocol/openid-connect/tokens

A more compact way to configure the token endpoint URL without the discovery is to set quarkus.oidc-client.token-path to an absolute URL:

quarkus.oidc-client.token-path=http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens

Setting quarkus.oidc-client.auth-server-url and quarkus.oidc-client.discovery-enabled is not required in this case.

Supported token grants

The main token grants that OidcClient can use to acquire the tokens are the client_credentials (default) and password grants.

Client credentials grant

Here is how OidcClient can be configured to use the client_credentials grant:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret

The client_credentials grant allows setting extra parameters for the token request by using quarkus.oidc-client.grant-options.client.<param-name>=<value>.

Here is how to set the intended token recipient by using the audience parameter:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
# 'client' is a shortcut for `client_credentials`
quarkus.oidc-client.grant.type=client
quarkus.oidc-client.grant-options.client.audience=https://example.com/api

Password grant

Here is how OidcClient can be configured to use the password grant:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=password
quarkus.oidc-client.grant-options.password.username=alice
quarkus.oidc-client.grant-options.password.password=alice

It can be further customized with the quarkus.oidc-client.grant-options.password configuration prefix, similar to the client credentials grant.

Other grants

OidcClient can also help acquire tokens with grants that require additional input parameters that cannot be captured in the configuration.

These grants are refresh_token (with the external refresh token), authorization_code, and two grants which can be used to exchange the current access token, namely:

  • urn:ietf:params:oauth:grant-type:token-exchange

  • urn:ietf:params:oauth:grant-type:jwt-bearer.

If you need to acquire an access token and have posted an existing refresh token to the current Quarkus endpoint, you must use the refresh_token grant. This grant uses an out-of-band refresh token to obtain a new token set.

In this case, configure OidcClient as follows:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=refresh

Then you can use the OidcClient.refreshTokens method with a provided refresh token to get the access token.

Using the urn:ietf:params:oauth:grant-type:token-exchange or urn:ietf:params:oauth:grant-type:jwt-bearer grants might be required if you are building a complex microservices application and want to avoid the same Bearer token being propagated to and used by more than one service.

Using OidcClient to support the authorization code grant might be required if you cannot use the Quarkus OIDC extension to support Authorization Code Flow.

If there is a very good reason for you to implement Authorization Code Flow, then you can configure OidcClient as follows:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=code

Then, you can use the OidcClient.accessTokens method to accept a Map of extra properties and pass the current code and redirect_uri parameters to exchange the authorization code for the tokens.

OidcClient also supports the urn:openid:params:grant-type:ciba grant:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=ciba

Then, you can use the OidcClient.accessTokens method to accept a Map of extra properties and pass the auth_req_id parameter to exchange the token authorization code.

Grant scopes

You might need to request that a specific set of scopes be associated with an issued access token. Use a dedicated quarkus.oidc-client.scopes list property, for example: quarkus.oidc-client.scopes=email,phone

Use OidcClient directly

You can use OidcClient directly to acquire access tokens and set them in an HTTP Authorization header as a Bearer scheme value.

For example, assume that the Quarkus endpoint must access a microservice that returns a user name.

  1. Create a REST client:

    package org.acme.security.openid.connect.client;
    
    import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
    
    import io.smallrye.mutiny.Uni;
    import jakarta.ws.rs.GET;
    import jakarta.ws.rs.HeaderParam;
    import jakarta.ws.rs.Path;
    import jakarta.ws.rs.Produces;
    
    @RegisterRestClient
    @Path("/")
    public interface RestClientWithTokenHeaderParam {
    
        @GET
        @Produces("text/plain")
        @Path("userName")
        Uni<String> getUserName(@HeaderParam("Authorization") String authorization);
    }
  2. Use OidcClient to acquire the tokens and propagate them:

    package org.acme.security.openid.connect.client;
    
    import org.eclipse.microprofile.rest.client.inject.RestClient;
    
    import io.quarkus.oidc.client.runtime.TokensHelper;
    import io.quarkus.oidc.client.OidcClient;
    
    import io.smallrye.mutiny.Uni;
    import jakarta.inject.Inject;
    import jakarta.ws.rs.GET;
    import jakarta.ws.rs.Path;
    import jakarta.ws.rs.Produces;
    
    @Path("/service")
    public class OidcClientResource {
    
        @Inject
        OidcClient client;
        TokensHelper tokenHelper = new TokensHelper(); (1)
    
        @Inject
        @RestClient
        RestClientWithTokenHeaderParam restClient;
    
        @GET
        @Path("user-name")
        @Produces("text/plain")
        public Uni<String> getUserName() {
        	return tokenHelper.getTokens(client).onItem()
            		.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
        }
    }
    1 io.quarkus.oidc.client.runtime.TokensHelper manages the access token acquisition and refresh.

Inject tokens

You can inject Tokens that use OidcClient internally. Tokens can be used to acquire the access tokens and refresh them if necessary:

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.quarkus.oidc.client.Tokens;

@Path("/service")
public class OidcClientResource {

    @Inject Tokens tokens;

    @GET
    public String getResponse() {
        //  Get the access token, which might have been refreshed.
        String accessToken = tokens.getAccessToken();
        // Use the access token to configure MP RestClient Authorization header/etc
    }
}

Use OidcClients

io.quarkus.oidc.client.OidcClients is a container of OidcClient instances.

It includes a default OidcClient and named clients that can be configured like this:

quarkus.oidc-client.client-enabled=false

quarkus.oidc-client.jwt-secret.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.jwt-secret.client-id=quarkus-app
quarkus.oidc-client.jwt-secret.credentials.jwt.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow

In this case, the default client is disabled with a client-enabled=false property. The jwt-secret client can be accessed like this:

import org.eclipse.microprofile.rest.client.inject.RestClient;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.oidc.client.runtime.TokensHelper;

@Path("/clients")
public class OidcClientResource {

    @Inject
    OidcClients clients;
    TokensHelper tokenHelper = new TokensHelper();

    @Inject
    @RestClient
    RestClientWithTokenHeaderParam restClient; (1)

    @GET
    @Path("user-name")
    @Produces("text/plain")
    public Uni<String> getUserName() {
    	OidcClient client = clients.getClient("jwt-secret");
    	return tokenHelper.getTokens(client).onItem()
        		.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
    }
}
1 See the RestClientWithTokenHeaderParam declaration in the Use OidcClient directly section.

If you also use OIDC multitenancy, and each OIDC tenant has its own associated OidcClient, you can use a Vert.x RoutingContext tenant-id attribute.

For example:

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;
import io.vertx.ext.web.RoutingContext;

@Path("/clients")
public class OidcClientResource {

    @Inject
    OidcClients clients;
    @Inject
    RoutingContext context;

    @GET
    public String getResponse() {
        String tenantId = context.get("tenant-id");
        // named OIDC tenant and client configurations use the same key:
        OidcClient client = clients.getClient(tenantId);
        //Use this client to get the token
    }
}

You can also create a new OidcClient programmatically. For example, assume that you must create it at startup time:

package org.acme.security.openid.connect.client;

import java.util.Map;

import org.eclipse.microprofile.config.inject.ConfigProperty;

import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.runtime.OidcClientConfig;
import io.quarkus.oidc.client.runtime.OidcClientConfig.Grant.Type;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.runtime.StartupEvent;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;

@ApplicationScoped
public class OidcClientCreator {

    @Inject
    OidcClients oidcClients;
    @ConfigProperty(name = "quarkus.oidc.auth-server-url")
    String oidcProviderAddress;

    private volatile OidcClient oidcClient;

    public void startup(@Observes StartupEvent event) {
    	createOidcClient().subscribe().with(client -> {oidcClient = client;});
    }

    public OidcClient getOidcClient() {
        return oidcClient;
    }

    private Uni<OidcClient> createOidcClient() {
        OidcClientConfig cfg = OidcClientConfig
            .authServerUrl(oidcProviderAddress)
            .id("myclient")
            .clientId("backend-service")
            .credentials("secret")
            .grant(Type.PASSWORD)
            .grantOptions("password", Map.of("username", "alice", "password", "alice"))
            .build();
        return oidcClients.newClient(cfg);
    }
}

Now, you can use this client like this:

import org.eclipse.microprofile.rest.client.inject.RestClient;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.runtime.TokensHelper;

@Path("/clients")
public class OidcClientResource {

    @Inject
    OidcClientCreator oidcClientCreator;
    TokensHelper tokenHelper = new TokensHelper();

    @Inject
    @RestClient
    RestClientWithTokenHeaderParam restClient; (1)

    @GET
    @Path("user-name")
    @Produces("text/plain")
    public Uni<String> getUserName() {
    	return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem()
        		.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
    }
}
1 See the RestClientWithTokenHeaderParam declaration in the Use OidcClient directly section.

Inject named OidcClient and tokens

If multiple OidcClient objects are configured, you can specify the OidcClient injection target with the @NamedOidcClient qualifier instead of working with OidcClients:

package org.acme.security.openid.connect.client;

import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.NamedOidcClient;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.runtime.TokensHelper;

@Path("/clients")
public class OidcClientResource {

    @Inject
    @NamedOidcClient("jwt-secret")
    OidcClient client;

    TokensHelper tokenHelper = new TokensHelper();

    @Inject
    @RestClient
    RestClientWithTokenHeaderParam restClient; (1)

    @GET
    @Path("user-name")
    @Produces("text/plain")
    public Uni<String> getUserName() {
    	return tokenHelper.getTokens(client).onItem()
        		.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
    }
}
1 See the RestClientWithTokenHeaderParam declaration in the Use OidcClient directly section.

The same qualifier can be used to specify the OidcClient used for a Tokens injection:

import java.io.IOException;

import jakarta.annotation.Priority;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.ext.Provider;

import io.quarkus.oidc.client.NamedOidcClient;
import io.quarkus.oidc.client.Tokens;

@Provider
@Priority(Priorities.AUTHENTICATION)
@RequestScoped
public class OidcClientRequestCustomFilter implements ClientRequestFilter {

    @Inject
    @NamedOidcClient("jwt-secret")
    Tokens tokens;

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken());
    }
}

Additionally, if you want to do a programmatic lookup of an OidcClient, use the NamedOidcClient.Literal qualifier to select it instead of relying on a static @NamedOidcClient injection point:

import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;

import io.quarkus.arc.Arc;
import io.quarkus.oidc.client.NamedOidcClient;
import io.quarkus.oidc.client.OidcClient;
import io.smallrye.mutiny.Uni;

@Path("/clients")
public class OidcClientResource {

    @Inject
    @Any
    Instance<OidcClient> clients; (1)

    @GET
    @Path("{name}/user-name")
    public Uni<String> getUserName(@PathParam("name") String name) {
        OidcClient client = clients.select(NamedOidcClient.Literal.of(name)).get();
        return client.getTokens().onItem().transform(tokens -> tokens.getAccessToken());
    }

    @GET
    @Path("{name}/access-token")
    public Uni<String> getAccessToken(@PathParam("name") String name) {
        // Alternatively, look the OidcClient up directly from the CDI container
        OidcClient client = Arc.container().instance(OidcClient.class, NamedOidcClient.Literal.of(name)).get(); (2)
        return client.getTokens().onItem().transform(tokens -> tokens.getAccessToken());
    }
}
1 Inject with @Any so that all the named OidcClient beans are available for selection by the NamedOidcClient.Literal qualifier.
2 Arc.container() resolves the bean directly without an injected Instance, passing the NamedOidcClient.Literal qualifier to select the wanted OidcClient.

Prefer the static @NamedOidcClient injection point whenever the client name is known in advance.

Use OidcClient in RestClient Reactive ClientFilter

Add the following Maven dependency:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-client-oidc-filter</artifactId>
</dependency>
It will also bring io.quarkus:quarkus-oidc-client.

quarkus-rest-client-oidc-filter extension provides io.quarkus.oidc.client.filter.OidcClientRequestReactiveFilter.

It works similarly to OidcClientRequestFilter. See Use OidcClient in MicroProfile RestClient client filter. It uses OidcClient to acquire the access token, refresh it if needed, and set it as an HTTP Authorization Bearer scheme value.

The difference is that it works with Reactive RestClient and implements a non-blocking client filter that does not block the current IO thread when acquiring or refreshing the tokens.

OidcClientRequestReactiveFilter delays an initial token acquisition until it is executed to avoid blocking an IO thread.

To add an OIDC client access token to requests from all methods, register OidcClientRequestReactiveFilter for the REST client interface with io.quarkus.oidc.client.filter.OidcClientFilter or org.eclipse.microprofile.rest.client.annotation.RegisterProvider:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@OidcClientFilter
@Path("/")
public interface ProtectedResourceService {

    @GET
    Uni<String> getUserName();
}

To add the token only to requests from selected methods, apply @OidcClientFilter to those methods instead of the REST client interface.

You cannot combine class-level and method-level @OidcClientFilter annotations on the same REST client, regardless of the OIDC client name. To use different OIDC clients for different methods, leave the interface unannotated and annotate every method that requires a token, specifying the client name where needed.

For example, a single REST client interface can call both public endpoints and endpoints that require an access token:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@Path("/")
public interface InformationService {

    @OidcClientFilter
    @GET
    Uni<String> getUserName();

    @Path("/public")
    @GET
    Uni<String> getPublicInformation();
}

or

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.reactive.filter.OidcClientRequestReactiveFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@RegisterProvider(OidcClientRequestReactiveFilter.class)
@Path("/")
public interface ProtectedResourceService {

    @GET
    Uni<String> getUserName();
}

OidcClientRequestReactiveFilter uses a default OidcClient by default. A named OidcClient can be selected with the quarkus.rest-client-oidc-filter.client-name configuration property. You can also select OidcClient by setting the value attribute of the @OidcClientFilter annotation.

The client name set through annotation has higher priority than the quarkus.rest-client-oidc-filter.client-name configuration property.

For example, given this jwt-secret named OIDC client declaration, you can refer to this client like this:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {

    @GET
    Uni<String> getUserName();
}

If you also want to refresh the token every time the ProtectedResourceService#getUserName call results in a 401 Unauthorized error, use the quarkus.rest-client-oidc-filter.refresh-on-unauthorized configuration property, as in the following example:

quarkus.rest-client-oidc-filter.refresh-on-unauthorized=true

Alternatively, if you only need to enable this feature for individual endpoints, create a custom filter, as in the following example:

package io.quarkus.it.keycloak;

import io.quarkus.oidc.client.reactive.filter.runtime.AbstractOidcClientRequestReactiveFilter;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;

@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestReactiveFilter {

    @Override
    protected boolean refreshOnUnauthorized() {
        return true;
    }
}

Use OidcClient in RestClient ClientFilter

Add the following Maven dependency:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-client-oidc-filter</artifactId>
</dependency>
It will also bring io.quarkus:quarkus-oidc-client.

quarkus-resteasy-client-oidc-filter extension provides io.quarkus.oidc.client.resteasy.filter.OidcClientRequestFilter, a Jakarta REST ClientRequestFilter that uses OidcClient to acquire the access token, refresh it if needed, and set it as an HTTP Authorization Bearer scheme value.

By default, this filter uses OidcClient to acquire the first pair of access and refresh tokens at initialization time. If the access tokens are short-lived and refresh tokens are unavailable, then the token acquisition should be delayed with quarkus.oidc-client.early-tokens-acquisition=false.

To add an OIDC client access token to requests from all methods, register OidcClientRequestFilter for the REST client interface with io.quarkus.oidc.client.filter.OidcClientFilter or org.eclipse.microprofile.rest.client.annotation.RegisterProvider:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@OidcClientFilter
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

To add the token only to requests from selected methods, apply @OidcClientFilter to those methods instead of the REST client interface.

You cannot combine class-level and method-level @OidcClientFilter annotations on the same REST client, regardless of the OIDC client name. To use different OIDC clients for different methods, leave the interface unannotated and annotate every method that requires a token, specifying the client name where needed.

For example, a single REST client interface can call both public endpoints and endpoints that require an access token:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@Path("/")
public interface InformationService {

    @OidcClientFilter
    @GET
    String getUserName();

    @Path("/public")
    @GET
    String getPublicInformation();
}

or

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.resteasy.filter.OidcClientRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@RegisterProvider(OidcClientRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

Alternatively, OidcClientRequestFilter can be registered automatically with all MP Rest or Jakarta REST clients if the quarkus.resteasy-client-oidc-filter.register-filter=true property is set.

OidcClientRequestFilter uses a default OidcClient by default. A named OidcClient can be selected with the quarkus.resteasy-client-oidc-filter.client-name configuration property. You can also select OidcClient by setting the value attribute of the @OidcClientFilter annotation.

The client name set through annotation has higher priority than the quarkus.resteasy-client-oidc-filter.client-name configuration property.

For example, given this jwt-secret named OIDC client declaration, you can refer to this client like this:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

If you also want to refresh the token every time the ProtectedResourceService#getUserName call results in a 401 Unauthorized error, use the quarkus.resteasy-client-oidc-filter.refresh-on-unauthorized configuration property, as in the following example:

quarkus.resteasy-client-oidc-filter.refresh-on-unauthorized=true

Alternatively, if you only need to enable this feature for individual endpoints, create a custom filter, as in the following example:

package io.quarkus.it.keycloak;

import io.quarkus.oidc.client.filter.runtime.AbstractOidcClientRequestFilter;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;

@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestFilter {

    @Override
    protected boolean refreshOnUnauthorized() {
        return true;
    }
}

Use a custom RestClient ClientFilter

If you prefer, you can use your own custom filter and inject Tokens:

import java.io.IOException;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.ext.Provider;
import io.quarkus.oidc.client.Tokens;

@Provider
@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter implements ClientRequestFilter {

    @Inject
    Tokens tokens;

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken());
    }
}

The Tokens producer will acquire and refresh the tokens, and the custom filter will decide how and when to use the token.

You can also inject named Tokens, see Inject named OidcClient and Tokens

Refreshing access tokens

OidcClientRequestReactiveFilter, OidcClientRequestFilter, and Tokens producers refresh the current expired access token if a refresh token is available.

Additionally, you can use the quarkus.oidc-client.refresh-token-time-skew property for preemptive access token refresh to avoid sending nearly expired access tokens that might cause HTTP 401 errors.

For example, if this property is set to 3S and the access token will expire in less than 3 seconds, the token is refreshed automatically.

By default, the OIDC client refreshes the token during the current request when it detects that the token has expired, or is nearly expired if the refresh token time skew is configured. Performance-critical applications might want to avoid waiting for a possible token refresh during incoming requests and configure an asynchronous token refresh instead.

For example:

quarkus.oidc-client.refresh-interval=1m (1)
1 Check every minute whether the current access token is expired and must be refreshed.

If the access token needs to be refreshed but no refresh token is available, an attempt is made to acquire a new token with a configured grant, such as client_credentials.

Some OpenID Connect providers do not return a refresh token in a client_credentials grant response. For example, starting with Keycloak 12, a refresh token is not returned by default for client_credentials. Providers might also restrict how many times a refresh token can be used.

Revoking access tokens

If your OpenID Connect provider, such as Keycloak, supports a token revocation endpoint, you can use OidcClient#revokeAccessToken to revoke the current access token. The revocation endpoint URL is discovered alongside the token request URI or can be configured with quarkus.oidc-client.revoke-path.

You might want to revoke the access token if a REST client call that uses this token fails with an HTTP 401 status code, or if the token has been used for a long time and you want to refresh it.

You can do this by requesting a token refresh with a refresh token. However, if the refresh token is unavailable, you can revoke the current access token first and then request a new access token.

OidcClient authentication

OidcClient must authenticate to the OpenID Connect provider for the client_credentials and other grant requests to succeed. All the OIDC Client Authentication options are supported.

For example:

client_secret_basic:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=mysecret

or

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.client-secret.value=mysecret

Or with the secret retrieved from a CredentialsProvider:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app

# This key is used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc-client.credentials.client-secret.provider.key=mysecret-key
# This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation
quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc-client.credentials.client-secret.provider.name=oidc-credentials-provider

client_secret_post:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.client-secret.value=mysecret
quarkus.oidc-client.credentials.client-secret.method=post

client_secret_jwt, signature algorithm is HS256:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow

Or with the secret retrieved from a CredentialsProvider, signature algorithm is HS256:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app

# This is a key that will be used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc-client.credentials.jwt.secret-provider.key=mysecret-key
# This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation
quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc-client.credentials.jwt.secret-provider.name=oidc-credentials-provider

private_key_jwt with the PEM key inlined in application.properties, where the signature algorithm is RS256:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key=Base64-encoded private key representation

private_key_jwt with the PEM key file, signature algorithm is RS256:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key-file=privateKey.pem

private_key_jwt with the keystore file, signature algorithm is RS256:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key-store-file=keystore.pkcs12
quarkus.oidc-client.credentials.jwt.key-store-password=mypassword
quarkus.oidc-client.credentials.jwt.key-password=mykeypassword

# Private key alias inside the keystore
quarkus.oidc-client.credentials.jwt.key-id=mykeyAlias

Using client_secret_jwt or private_key_jwt authentication methods ensures that no client secret goes over the wire.

Additional JWT authentication options

If either client_secret_jwt or private_key_jwt authentication method is used, you can customize the JWT signature algorithm, key identifier, audience, subject, and issuer.

For example:

# private_key_jwt client authentication

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key-file=privateKey.pem

# This is a token key identifier 'kid' header - set it if your OpenID Connect provider requires it.
# Note that if the key is represented in a JSON Web Key (JWK) format with a `kid` property, then
# using 'quarkus.oidc-client.credentials.jwt.token-key-id' is unnecessary.
quarkus.oidc-client.credentials.jwt.token-key-id=mykey

# Use the RS512 signature algorithm instead of the default RS256
quarkus.oidc-client.credentials.jwt.signature-algorithm=RS512

# The token endpoint URL is the default audience value; use the base address URL instead:
quarkus.oidc-client.credentials.jwt.audience=${quarkus.oidc-client.auth-server-url}

# custom subject instead of the client ID:
quarkus.oidc-client.credentials.jwt.subject=custom-subject

# custom issuer instead of the client ID:
quarkus.oidc-client.credentials.jwt.issuer=custom-issuer

JWT Bearer

RFC7523 explains how JWT Bearer tokens can be used to authenticate clients. For more information, see the Using JWTs for Client Authentication section.

Enable it as follows:

quarkus.oidc-client.auth-server-url=${auth-server-url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.source=bearer

Next, provide the JWT bearer token as a client_assertion parameter to the OIDC client.

Quarkus can load the JWT bearer token from the file system. For example, in Kubernetes, a service account token projection can be mounted to a /var/run/secrets/tokens path. Then configure the JWT bearer token path as follows:

quarkus.oidc-client.credentials.jwt.token-path=/var/run/secrets/tokens (1)
1 Path to a JWT bearer token. Quarkus loads a new token from the file system and reloads it when the token expires.

Your other option is to use OidcClient methods for acquiring or refreshing tokens that accept additional grant parameters, for example, oidcClient.getTokens(Map.of("client_assertion", "ey…​")).

If you use the OIDC client filters, you must register a custom filter that provides this assertion.

Here is an example of the Quarkus REST (formerly RESTEasy Reactive) custom filter:

package io.quarkus.it.keycloak;

import java.util.Map;

import io.quarkus.oidc.client.reactive.filter.runtime.AbstractOidcClientRequestReactiveFilter;
import io.quarkus.oidc.common.runtime.OidcConstants;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;

@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestReactiveFilter {

    @Override
    protected Map<String, String> additionalParameters() {
        return Map.of(OidcConstants.CLIENT_ASSERTION, "ey...");
    }
}

Here is an example of the RESTEasy Classic custom filter:

package io.quarkus.it.keycloak;

import java.util.Map;

import io.quarkus.oidc.client.filter.runtime.AbstractOidcClientRequestFilter;
import io.quarkus.oidc.common.runtime.OidcConstants;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;

@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestFilter {

    @Override
    protected Map<String, String> additionalParameters() {
        return Map.of(OidcConstants.CLIENT_ASSERTION, "ey...");
    }
}

SPIFFE JWT-SVID

SPIFFE (Secure Production Identity Framework for Everyone) JWT-SVID tokens can be used as client assertions for client authentication. When the JWT source is set to spiffe, the client_assertion_type parameter is set to urn:ietf:params:oauth:client-assertion-type:jwt-spiffe instead of the JWT bearer urn:ietf:params:oauth:client-assertion-type:jwt-bearer value.

Enable it as follows:

quarkus.oidc-client.auth-server-url=${auth-server-url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.source=spiffe-jwt

Quarkus can load the SPIFFE JWT-SVID token from the file system. Configure the token path as follows:

quarkus.oidc-client.credentials.jwt.token-path=/var/run/secrets/tokens (1)
1 Path to a SPIFFE JWT-SVID token. Quarkus loads a new token from the file system and reloads it when the token expires. The token’s sub claim is verified to start with the spiffe:// scheme.

Apple POST JWT

Apple OpenID Connect provider uses the client_secret_post method, where the secret is a JWT produced with the private_key_jwt authentication method, but with Apple account-specific issuer and subject properties.

quarkus-oidc-client supports a non-standard client_secret_post_jwt authentication method, which can be configured as follows:

quarkus.oidc-client.auth-server-url=${apple.url}
quarkus.oidc-client.client-id=${apple.client-id}
quarkus.oidc-client.credentials.client-secret.method=post-jwt

quarkus.oidc-client.credentials.jwt.key-file=ecPrivateKey.pem
quarkus.oidc-client.credentials.jwt.signature-algorithm=ES256
quarkus.oidc-client.credentials.jwt.subject=${apple.subject}
quarkus.oidc-client.credentials.jwt.issuer=${apple.issuer}

Mutual TLS

Some OpenID Connect providers require that a client be authenticated as part of the mutual TLS (mTLS) authentication process.

quarkus-oidc-client can be configured as follows to support mTLS:

quarkus.oidc-client.tls.tls-configuration-name=oidc-client

# configure hostname verification if necessary
#quarkus.tls.oidc-client.hostname-verification-algorithm=NONE

# Keystore configuration
quarkus.tls.oidc-client.key-store.p12.path=client-keystore.p12
quarkus.tls.oidc-client.key-store.p12.password=${key-store-password}

# Add more keystore properties if needed:
#quarkus.tls.oidc-client.key-store.p12.alias=keyAlias
#quarkus.tls.oidc-client.key-store.p12.alias-password=keyAliasPassword

# Truststore configuration
quarkus.tls.oidc-client.trust-store.p12.path=client-truststore.p12
quarkus.tls.oidc-client.trust-store.p12.password=${trust-store-password}
# Add more truststore properties if needed:
#quarkus.tls.oidc-client.trust-store.p12.alias=certAlias

Authenticated access to the discovery endpoint

The OIDC discovery endpoint is typically public. Some OIDC providers may enforce authenticated access to this endpoint. For example, the Kubernetes API server acts as an OIDC provider for service account tokens and requires a bearer access token to access its discovery endpoint.

To send client credentials to the discovery endpoint, set quarkus.oidc-client.credentials.for-all-endpoints=true. Since the discovery endpoint is accessed with an HTTP GET request, only authentication methods that use the HTTP Authorization header are effective, such as client_secret_basic and JWT bearer. Alternatively, you can use OidcRequestFilter to customize the authentication.

OIDC Client SPI

When your custom extension must acquire OIDC tokens by using one of the token grants supported by the OIDC client, the extension can depend only on the OIDC Client SPI and let the OIDC client itself acquire and refresh access tokens as needed.

  1. Add the following dependency:

    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-oidc-client-spi</artifactId>
    </dependency>
  2. Update your extension to use the io.quarkus.oidc.client.spi.TokenProvider CDI bean as required. For example:

    package org.acme.extension;
    
    import jakarta.inject.Inject;
    import io.quarkus.oidc.client.spi.TokenProvider;
    
    public class ExtensionOAuth2Support {
    
       @Inject
       TokenProvider tokenProvider;
    
       public Uni<String> getAccessToken() {
           return tokenProvider.getAccessToken();
       }
    }

    Currently, io.quarkus.oidc.client.spi.TokenProvider is available only for default OIDC clients because custom extensions are unlikely to be aware of multiple named OIDC clients.

Testing

Start by adding the following dependencies to your test project:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <scope>test</scope>
</dependency>

Wiremock

  1. Add the following dependencies to your test project:

    <dependency>
        <groupId>org.wiremock</groupId>
        <artifactId>wiremock</artifactId>
        <scope>test</scope>
        <version>${wiremock.version}</version> (1)
    </dependency>
    1 Use a proper Wiremock version. View all available WireMock versions on Maven Central.
  2. Write a Wiremock-based QuarkusTestResourceLifecycleManager, for example:

    package io.quarkus.it.keycloak;
    
    import static com.github.tomakehurst.wiremock.client.WireMock.matching;
    import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import com.github.tomakehurst.wiremock.WireMockServer;
    import com.github.tomakehurst.wiremock.client.WireMock;
    import com.github.tomakehurst.wiremock.core.Options.ChunkedEncodingPolicy;
    
    import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
    
    public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager {
        private WireMockServer server;
    
        @Override
        public Map<String, String> start() {
    
            server = new WireMockServer(wireMockConfig().dynamicPort().useChunkedTransferEncoding(ChunkedEncodingPolicy.NEVER));
            server.start();
    
            server.stubFor(WireMock.post("/tokens")
                    .withRequestBody(matching("grant_type=password&username=alice&password=alice"))
                    .willReturn(WireMock
                            .aResponse()
                            .withHeader("Content-Type", "application/json")
                            .withBody(
                                    "{\"access_token\":\"access_token_1\", \"expires_in\":4, \"refresh_token\":\"refresh_token_1\"}")));
            server.stubFor(WireMock.post("/tokens")
                    .withRequestBody(matching("grant_type=refresh_token&refresh_token=refresh_token_1"))
                    .willReturn(WireMock
                            .aResponse()
                            .withHeader("Content-Type", "application/json")
                            .withBody(
                                    "{\"access_token\":\"access_token_2\", \"expires_in\":4, \"refresh_token\":\"refresh_token_1\"}")));
    
    
            Map<String, String> conf = new HashMap<>();
            conf.put("keycloak.url", server.baseUrl());
            return conf;
        }
    
        @Override
        public synchronized void stop() {
            if (server != null) {
                server.stop();
                server = null;
            }
        }
    }
  3. Prepare the REST test endpoints.

    You can have the test front-end endpoint, which uses the injected MP REST client with a registered OidcClient filter, call the downstream endpoint. This endpoint echoes the token back. For example, see the integration-tests/oidc-client-wiremock in the main Quarkus repository.

  4. Set application.properties, for example:

    # Use the 'keycloak.url' property set by the test KeycloakRealmResourceManager
    quarkus.oidc-client.auth-server-url=${keycloak.url:replaced-by-test-resource}
    quarkus.oidc-client.discovery-enabled=false
    quarkus.oidc-client.token-path=/tokens
    quarkus.oidc-client.client-id=quarkus-service-app
    quarkus.oidc-client.credentials.secret=secret
    quarkus.oidc-client.grant.type=password
    quarkus.oidc-client.grant-options.password.username=alice
    quarkus.oidc-client.grant-options.password.password=alice
  5. Write the test code.

    Given the Wiremock-based resource above, the first test invocation should return the access_token_1 access token, which will expire in 4 seconds. Use awaitility to wait for about 5 seconds. The next test invocation should return the access_token_2 access token, which confirms the expired access_token_1 access token has been refreshed.

Keycloak

If you work with Keycloak, you can use the same approach described in the OpenID Connect Bearer Token Integration testing Keycloak section.

How to check the errors in the logs

  1. Enable io.quarkus.oidc.client.runtime.OidcClientImpl TRACE level logging to see more details about the token acquisition and refresh errors:

    quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".level=TRACE
    quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE
  2. Enable io.quarkus.oidc.client.runtime.OidcClientRecorder TRACE level logging to see more details about the OidcClient initialization errors:

    quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".level=TRACE
    quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".min-level=TRACE

OIDC request filters

You can filter OIDC requests made by the OIDC client to the OIDC provider by registering one or more OidcRequestFilter implementations, which can update or add new request headers, customize or analyze the request body.

You can have a single filter intercept requests to all OIDC provider endpoints, or use an @OidcEndpoint annotation to apply this filter to requests to specific endpoints only.

For example:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.smallrye.mutiny.Uni;
import io.vertx.core.http.HttpMethod;

@ApplicationScoped
@OidcEndpoint(value = Type.TOKEN)
@Unremovable
public class OidcRequestCustomizer implements OidcRequestFilter {

    @Override
    public Uni<Void> filter(OidcRequestFilterContext requestContext) {
        HttpMethod method = requestContext.request().method();
        String uri = requestContext.request().uri();
        if (method == HttpMethod.POST && uri.endsWith("/token") && requestContext.requestBody() != null) {
            requestContext.request().putHeader("Digest", calculateDigest(requestContext.requestBody().toString()));
        }
        return Uni.createFrom().voidItem();
    }

    private String calculateDigest(String bodyString) {
        // Apply the required digest algorithm to the body string.
        // Implementation details are omitted for brevity.
    }
}

OidcRequestContextProperties can be used to access request properties. Currently, you can use a client_id key to access the client tenant ID and a grant_type key to access the grant type that the OIDC client uses to acquire tokens.

OidcRequestFilter can customize the request body by updating it with a String or by preparing an instance of io.vertx.mutiny.core.buffer.Buffer and setting it on a request context, for example:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.TOKEN)
public class TokenRequestFilter implements OidcRequestFilter {

    @Override
    public Uni<Void> filter(OidcRequestFilterContext rc) {
        // Add more required properties to the token request
        rc.requestBody(rc.requestBody().toString() + "&opaque_token_param=opaque_token_value"));
        return Uni.createFrom().voidItem();
    }
}

OIDC response filters

You can filter responses to OIDC client requests by registering one or more OidcResponseFilter implementations that can check the response status, headers, and body to log them or perform other actions.

You can have a single filter intercept responses to all OIDC client requests, or use an @OidcEndpoint annotation to apply this filter to responses to specific OIDC client requests only.

For example:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;

import org.jboss.logging.Logger;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcResponseFilter;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.common.runtime.OidcConstants;

@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.TOKEN) (1)
public class TokenEndpointResponseFilter implements OidcResponseFilter {
    private static final Logger LOG = Logger.getLogger(TokenEndpointResponseFilter.class);

    @Override
    public Uni<Void> filter(OidcResponseFilterContext rc) {
        String contentType = rc.responseHeaders().get("Content-Type"); (2)
        if (contentType.equals("application/json")
                && "refresh_token".equals(rc.requestProperties().get(OidcConstants.GRANT_TYPE)) (3)
                && rc.responseBody().toJsonObject().containsKey("refresh_token")) { (4)
            LOG.debug("Tokens have been refreshed");
        }
        return Uni.createFrom().voidItem();
    }

}
1 Restrict this filter to requests targeting the OIDC token endpoint only.
2 Check the response Content-Type header.
3 Use OidcRequestContextProperties request properties to confirm it is a refresh_grant token grant response.
4 Confirm the response JSON contains a refresh_token property.

OidcResponseFilter can customize the response body by preparing an instance of io.vertx.mutiny.core.buffer.Buffer and setting it as a property on a response context, for example:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcResponseFilter;
import io.smallrye.mutiny.Uni;
import io.vertx.core.json.JsonObject;
import io.vertx.mutiny.core.buffer.Buffer;

@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.TOKEN)
public class TokenResponseFilter implements OidcResponseFilter {

    @Override
    public Uni<Void> filter(OidcResponseFilterContext rc) {
        JsonObject body = rc.responseBody().toJsonObject();
        // JSON `scope` property has multiple values separated by a comma character.
        // It must be replaced with a space character.
        String scope = body.getString("scope");
        body.put("scope", scope.replace(",", " "));
        rc.responseBody(Buffer.buffer(body.toString()));
        return Uni.createFrom().voidItem();
    }
}

Token Propagation for Quarkus REST

The quarkus-rest-client-oidc-token-propagation extension provides a REST Client filter, io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter, that simplifies the propagation of authentication information.

This filter propagates the bearer token present in the currently active request or the token acquired from the authorization code flow mechanism as the HTTP Authorization header’s Bearer scheme value.

You can selectively register AccessTokenRequestReactiveFilter by using either the io.quarkus.oidc.token.propagation.common.AccessToken annotation or the org.eclipse.microprofile.rest.client.annotation.RegisterProvider annotation, for example:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@AccessToken
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

Alternatively, apply @AccessToken to individual methods to propagate access tokens for requests made by those methods. This lets a single REST client interface call both public endpoints and endpoints that require an access token:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@Path("/")
public interface InformationService {

    @AccessToken
    @GET
    String getUserName();

    @Path("/public")
    @GET
    String getPublicInformation();
}

or

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@RegisterProvider(AccessTokenRequestReactiveFilter.class)
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

You can combine class-level and method-level @AccessToken annotations on the same REST client. When you do, Quarkus uses the class-level annotation for unannotated methods and the method-level annotation, including its exchangeTokenClient attribute, for annotated methods.

Additionally, AccessTokenRequestReactiveFilter can support a complex application that needs to exchange tokens before propagating them.

If you work with Keycloak or another OIDC provider that supports a Token Exchange token grant, then you can configure AccessTokenRequestReactiveFilter to exchange the token like this:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=exchange
quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange

quarkus.rest-client-oidc-token-propagation.exchange-token=true (1)
1 Please note that the exchange-token configuration property is ignored when the OidcClient name is set with the io.quarkus.oidc.token.propagation.common.AccessToken#exchangeTokenClient annotation attribute.
AccessTokenRequestReactiveFilter will use OidcClient to exchange the current token, and you can use quarkus.oidc-client.grant-options.exchange to set the additional exchange properties expected by your OpenID Connect provider.

If you work with providers such as Azure that require using JWT bearer token grant to exchange the current token, then you can configure AccessTokenRequestReactiveFilter to exchange the token like this:

quarkus.oidc-client.auth-server-url=${azure.provider.url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret

quarkus.oidc-client.grant.type=jwt
quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of
quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access

quarkus.resteasy-client-oidc-token-propagation.exchange-token=true

AccessTokenRequestReactiveFilter uses a default OidcClient by default. A named OidcClient can be selected with a quarkus.rest-client-oidc-token-propagation.client-name configuration property or with the io.quarkus.oidc.token.propagation.common.AccessToken#exchangeTokenClient annotation attribute.

Token Propagation for RESTEasy Classic

The quarkus-resteasy-client-oidc-token-propagation extension provides two Jakarta REST jakarta.ws.rs.client.ClientRequestFilter class implementations that simplify the propagation of authentication information. io.quarkus.oidc.token.propagation.AccessTokenRequestFilter propagates the Bearer token present in the current active request or the token acquired from the Authorization code flow mechanism, as the HTTP Authorization header’s Bearer scheme value.

The io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter provides the same functionality and, in addition, supports JWT tokens.

When you need to propagate the current Authorization Code Flow access token, immediate token propagation works well because the code flow access tokens, as opposed to ID tokens, are meant to be propagated for the current Quarkus endpoint to access the remote services on behalf of the currently authenticated user.

However, direct end-to-end Bearer token propagation should be avoided. For example, Client → Service A → Service B, where Service B receives a token sent by Client to Service A. In such cases, Service B cannot distinguish whether the token came from Service A or directly from Client. For Service B to verify that the token originated from Service A, it should be able to assert new issuer and audience claims.

Additionally, a complex application might need to exchange or update the tokens before propagating them. For example, the access context might differ when Service A accesses Service B. In this case, Service A might be granted a narrow or completely different set of scopes to access Service B.

The following sections show how AccessTokenRequestFilter and JsonWebTokenRequestFilter can help.

RestClient AccessTokenRequestFilter

AccessTokenRequestFilter treats all tokens as strings and, as such, can work with both JWT and opaque tokens.

You can selectively register AccessTokenRequestFilter by using either io.quarkus.oidc.token.propagation.common.AccessToken or org.eclipse.microprofile.rest.client.annotation.RegisterProvider, for example:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@AccessToken
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

Alternatively, apply @AccessToken to individual methods to propagate access tokens for requests made by those methods. This lets a single REST client interface call both public endpoints and endpoints that require an access token:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@Path("/")
public interface InformationService {

    @AccessToken
    @GET
    String getUserName();

    @Path("/public")
    @GET
    String getPublicInformation();
}

or

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@RegisterProvider(AccessTokenRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

You can combine class-level and method-level @AccessToken annotations on the same REST client. When you do, Quarkus uses the class-level annotation for unannotated methods and the method-level annotation, including its exchangeTokenClient attribute, for annotated methods.

Alternatively, AccessTokenRequestFilter can be registered automatically with all MP Rest or Jakarta REST clients if the quarkus.resteasy-client-oidc-token-propagation.register-filter property is set to true and the quarkus.resteasy-client-oidc-token-propagation.json-web-token property is set to false, which is the default value.

Exchange token before propagation

If the current access token needs to be exchanged before propagation and you work with Keycloak or another OpenID Connect provider that supports a Token Exchange token grant, you can configure AccessTokenRequestFilter like this:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=exchange
quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange

quarkus.resteasy-client-oidc-token-propagation.exchange-token=true

If you work with providers such as Azure that require using JWT bearer token grant to exchange the current token, you can configure AccessTokenRequestFilter to exchange the token like this:

quarkus.oidc-client.auth-server-url=${azure.provider.url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret

quarkus.oidc-client.grant.type=jwt
quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of
quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access

quarkus.resteasy-client-oidc-token-propagation.exchange-token=true
AccessTokenRequestFilter will use OidcClient to exchange the current token, and you can use quarkus.oidc-client.grant-options.exchange to set the additional exchange properties expected by your OpenID Connect provider.

AccessTokenRequestFilter uses a default OidcClient by default. A named OidcClient can be selected with the quarkus.resteasy-client-oidc-token-propagation.client-name configuration property.

RestClient JsonWebTokenRequestFilter

Using JsonWebTokenRequestFilter is recommended if you work with Bearer JWT tokens whose claims, such as issuer and audience, can be modified and the updated tokens re-secured, for example, re-signed.

It expects an injected org.eclipse.microprofile.jwt.JsonWebToken and therefore will not work with opaque tokens. Also, if your OpenID Connect provider supports a Token Exchange protocol, it is recommended to use AccessTokenRequestFilter instead, because both JWT and opaque bearer tokens can be securely exchanged with AccessTokenRequestFilter.

JsonWebTokenRequestFilter makes it easy for Service A implementations to update the injected org.eclipse.microprofile.jwt.JsonWebToken with the new issuer and audience claim values and secure the updated token again with a new signature. The only difficult step is ensuring that Service A has a signing key, which should be provisioned from a secure file system or remote secure storage such as Vault.

You can selectively register JsonWebTokenRequestFilter by using either io.quarkus.oidc.token.propagation.JsonWebToken or org.eclipse.microprofile.rest.client.annotation.RegisterProvider, for example:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@JsonWebToken
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

or

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@RegisterProvider(JsonWebTokenRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

Alternatively, JsonWebTokenRequestFilter can be registered automatically with all MicroProfile REST or Jakarta REST clients if both the quarkus.resteasy-client-oidc-token-propagation.register-filter and quarkus.resteasy-client-oidc-token-propagation.json-web-token properties are set to true.

Update token before propagation

If the injected token needs to have its iss (issuer) or aud (audience) claims updated and secured again with a new signature, then you can configure JsonWebTokenRequestFilter like this:

quarkus.resteasy-client-oidc-token-propagation.secure-json-web-token=true
smallrye.jwt.sign.key.location=/privateKey.pem
# Set a new issuer
smallrye.jwt.new-token.issuer=http://frontend-resource
# Set a new audience
smallrye.jwt.new-token.audience=http://downstream-resource
# Override the existing token issuer and audience claims if they are already set
smallrye.jwt.new-token.override-matching-claims=true

As mentioned, use AccessTokenRequestFilter if you work with Keycloak or an OpenID Connect provider that supports a Token Exchange protocol.

Testing

Typically, you must prepare two REST test endpoints. The first endpoint uses the injected MP REST client with a registered token propagation filter to call the second endpoint.

To learn how it can be done, follow the OpenID Connect client and token propagation quickstart, and its Testing section in particular.

GraphQL client integration

The quarkus-oidc-client-graphql extension provides a way to integrate an OIDC client into GraphQL clients, paralleling the approach used with REST clients.

When this extension is active, any GraphQL client configured through properties, rather than programmatically by the builder, uses the OIDC client to acquire an access token, which it then sets as the Authorization header value. The OIDC client also refreshes expired access tokens.

To configure which OIDC client the GraphQL client should use, select one of the configured OIDC clients with the quarkus.oidc-client-graphql.client-name property, for example:

quarkus.oidc-client-graphql.client-name=oidc-client-for-graphql

# example declaration of the OIDC client itself
quarkus.oidc-client.oidc-client-for-graphql.auth-server-url=${keycloak.url}
quarkus.oidc-client.oidc-client-for-graphql.grant.type=password
quarkus.oidc-client.oidc-client-for-graphql.grant-options.password.username=${username}
quarkus.oidc-client.oidc-client-for-graphql.grant-options.password.password=${password}
quarkus.oidc-client.oidc-client-for-graphql.client-id=${quarkus.oidc.client-id}
quarkus.oidc-client.oidc-client-for-graphql.credentials.client-secret.value=${keycloak.credentials.secret}
quarkus.oidc-client.oidc-client-for-graphql.credentials.client-secret.method=POST

If you do not specify the quarkus.oidc-client-graphql.client-name property, GraphQL clients will use the default OIDC client without an explicit name.

Specifically for type-safe GraphQL clients, you can override this on a per-client basis by annotating the GraphQLClientApi interface with @io.quarkus.oidc.client.filter.OidcClientFilter.

For example:

@GraphQLClientApi(configKey = "order-client")
@OidcClientFilter("oidc-client-for-graphql")
public interface OrdersGraphQLClient {
    // Queries, mutations, and subscriptions go here.
}

To use this with a programmatically created GraphQL client, both builders, VertxDynamicGraphQLClientBuilder and VertxTypesafeGraphQLClientBuilder, contain a dynamicHeader(String, Uni<String>) method that lets you plug in a header that might change for every request.

To plug an OIDC client into it, use:

@Inject
OidcClients oidcClients;

VertxDynamicGraphQLClientBuilder builder = ....;
Uni<String> tokenUni = oidcClients.getClient("OIDC_CLIENT_NAME")
    .getTokens().map(t -> "Bearer " + t.getAccessToken());
builder.dynamicHeader("Authorization", tokenUni);
VertxDynamicGraphQLClient client = builder.build();

For dynamic GraphQL clients, you can override the quarkus.oidc-client-graphql.client-name configuration property on a per-client basis with the quarkus.oidc-client-graphql."graphql-client".client-name configuration property.

Example application.properties
quarkus.oidc-client-graphql."dynamic-graphql-client-2".client-name=oidc-client-for-graphql-2

# example declaration of the OIDC client itself
quarkus.oidc-client.oidc-client-for-graphql-2.auth-server-url=${keycloak.url}
quarkus.oidc-client.oidc-client-for-graphql-2.grant.type=password
quarkus.oidc-client.oidc-client-for-graphql-2.grant-options.password.username=${username}
quarkus.oidc-client.oidc-client-for-graphql-2.grant-options.password.password=${password}
quarkus.oidc-client.oidc-client-for-graphql-2.client-id=${quarkus.oidc.client-id}
quarkus.oidc-client.oidc-client-for-graphql-2.credentials.client-secret.value=${keycloak.credentials.secret}
quarkus.oidc-client.oidc-client-for-graphql-2.credentials.client-secret.method=POST
An example of dynamic GraphQL client
@Inject
@GraphQLClient("dynamic-graphql-client-2")
DynamicGraphQLClient dynamicClient;

String dynamicClientAdmin() throws ExecutionException, InterruptedException {
    return dynamicClient.executeSync("query { principalName }").getData().getString("principalName");   (1)
}
1 The dynamic-graphql-client-2 GraphQL client calls have the Authorization header with an access token acquired by the oidc-client-for-graphql-2 OIDC client.

Health Check

When you use the quarkus-smallrye-health extension, Quarkus can expose health checks for OIDC clients.

The health check uses OIDC endpoint discovery to validate the connection. Therefore, discovery must be enabled.

To activate the health check, set quarkus.oidc-client.health.enabled to true and ensure discovery is not disabled:

quarkus.oidc-client.health.enabled=true

Configuration reference

OIDC client

OIDC token propagation