OpenID Connect (OIDC) and OAuth2 Client and Filters Reference Guide
This reference guide explains how to use:
-
quarkus-oidc-client
,quarkus-oidc-client-reactive-filter
andquarkus-oidc-client-filter
extensions to acquire and refresh access tokens from OpenID Connect and OAuth 2.0 compliant Authorization Servers such as Keycloak -
quarkus-oidc-token-propagation-reactive
andquarkus-oidc-token-propagation
extensions to propagate the currentBearer
orAuthorization 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>
quarkus-oidc-client
extension provides a reactive
io.quarkus.oidc.client.OidcClient
which can be used to acquire and refresh
tokens using SmallRye Mutiny Uni
and Vert.x WebClient
.
OidcClient
is initialized at the build time with the IDP token endpoint
URL which can be auto-discovered or manually configured and uses this
endpoint to acquire access tokens using the token grants such as
client_credentials
or password
and refresh the tokens using 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
.
Alternatively, if the discovery endpoint is not available or you want to save on the discovery endpoint round-trip, you can disable the discovery and configure the token endpoint address with a relative path value, 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 which 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 to set extra parameters to the token
request via
quarkus.oidc-client.grant-options.client.<param-name>=<value>
. Here is how
to set the intended token recipient via 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 using a
quarkus.oidc-client.grant-options.password
configuration prefix, similarly
to how the client credentials grant can be customized.
Other Grants
OidcClient
can also help with acquiring the tokens using the grants which
require some extra input parameters which cannot be captured in the
configuration. These grants are refresh_token
(with the external refresh
token), authorization_code
, as well as two grants which can be used to
exchange the current access token,
urn:ietf:params:oauth:grant-type:token-exchange
and
urn:ietf:params:oauth:grant-type:jwt-bearer
.
Using the refresh_token
grant which uses an out-of-band refresh token to
acquire a new set of tokens will be required if the existing refresh token
has been posted to the current Quarkus endpoint for it to acquire the access
token. In this case OidcClient
needs to be configured 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
and then you can use 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 be propagated to and used by more than one service. See
Token Propagation in MicroProfile RestClient
Reactive filter and Token Propagation in
MicroProfile RestClient filter for more details.
Using OidcClient
to support the authorization code
grant might be
required if for some reason 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
and then you can use OidcClient.accessTokens
method accepting 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
and then you can use OidcClient.accessTokens
method accepting a Map of
extra properties and pass auth_req_id
parameter to exchange the
authorization code for the tokens.
Use OidcClient directly
One can use OidcClient
directly as follows:
import jakarta.inject.PostConstruct;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.Tokens;
@Path("/service")
public class OidcClientResource {
@Inject
OidcClient client;
volatile Tokens currentTokens;
@PostConstruct
public void init() {
currentTokens = client.getTokens().await().indefinitely();
}
@GET
public String getResponse() {
Tokens tokens = currentTokens;
if (tokens.isAccessTokenExpired()) {
// Add @Blocking method annotation if this code is used with Reactive RestClient
tokens = client.refreshTokens(tokens.getRefreshToken()).await().indefinitely();
currentTokens = tokens;
}
// Use tokens.getAccessToken() to configure MP RestClient Authorization header/etc
}
}
Inject Tokens
You can inject Tokens
which uses OidcClient
internally. Tokens
can be
used to acquire the access tokens and refresh them if necessary:
import jakarta.inject.PostConstruct;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
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
s - it
includes a default OidcClient
and named clients which 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
Note in this case the default client is disabled with a
client-enabled=false
property. The jwt-secret
client can be accessed
like this:
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;
@Path("/clients")
public class OidcClientResource {
@Inject
OidcClients clients;
@GET
public String getResponse() {
OidcClient client = clients.getClient("jwt-secret");
// use this client to get the token
}
}
If you also use OIDC
multitenancy and each OIDC tenant has its own associated
|
If you need you can also create new OidcClient
programmatically like this:
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.quarkus.oidc.client.OidcClientConfig;
import io.smallrye.mutiny.Uni;
@Path("/clients")
public class OidcClientResource {
@Inject
OidcClients clients;
@GET
public String getResponse() {
OidcClientConfig cfg = new OidcClientConfig();
cfg.setId("myclient");
cfg.setAuthServerUrl("http://localhost:8081/auth/realms/quarkus/");
cfg.setClientId("quarkus");
cfg.getCredentials().setSecret("secret");
Uni<OidcClient> client = clients.newClient(cfg);
// use this client to get the token
}
}
Inject named OidcClient and Tokens
In case of multiple configured OidcClient
s you can specify the
OidcClient
injection target by the extra qualifier @NamedOidcClient
instead of working with OidcClients
:
package io.quarkus.oidc.client;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/clients")
public class OidcClientResource {
@Inject
@NamedOidcClient("jwt-secret")
OidcClient client;
@GET
public String getResponse() {
// use client to get the token
}
}
The same qualifier can be used to specify the OidcClient
used for a
Tokens
injection:
@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());
}
}
Use OidcClient in RestClient Reactive ClientFilter
Add the following Maven Dependency:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-reactive-filter</artifactId>
</dependency>
Note it will also bring io.quarkus:quarkus-oidc-client
.
quarkus-oidc-client-reactive-filter
extension provides
io.quarkus.oidc.client.filter.OidcClientRequestReactiveFilter
.
It works similarly to the way OidcClientRequestFilter
does (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 which 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.
You can selectively register OidcClientRequestReactiveFilter
by using
either io.quarkus.oidc.client.reactive.filter.OidcClientFilter
or
org.eclipse.microprofile.rest.client.annotation.RegisterProvider
annotations:
import 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;
@RegisterRestClient
@OidcClientFilter
@Path("/")
public interface ProtectedResourceService {
@GET
Uni<String> getUserName();
}
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;
@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 a
quarkus.oidc-client-reactive-filter.client-name
configuration property.
You can also select OidcClient
by setting value
attribute of the
@OidcClientFilter
annotation. The client name set through annotation has
higher priority than the quarkus.oidc-client-reactive-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.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {
@GET
Uni<String> getUserName();
}
Use OidcClient in RestClient ClientFilter
Add the following Maven Dependency:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-filter</artifactId>
</dependency>
Note it will also bring io.quarkus:quarkus-oidc-client
.
quarkus-oidc-client-filter
extension provides
io.quarkus.oidc.client.filter.OidcClientRequestFilter
Jakarta REST
ClientRequestFilter which 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 will get OidcClient
to acquire the first pair of
access and refresh tokens at its initialization time. If the access tokens
are short-lived and refresh tokens are not available then the token
acquisition should be delayed with
quarkus.oidc-client.early-tokens-acquisition=false
.
You can selectively register OidcClientRequestFilter
by using either
io.quarkus.oidc.client.filter.OidcClientFilter
or
org.eclipse.microprofile.rest.client.annotation.RegisterProvider
annotations:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
@RegisterRestClient
@OidcClientFilter
@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.client.filter.OidcClientRequestFilter;
@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
quarkus.oidc-client-filter.register-filter=true
property is set.
OidcClientRequestFilter
uses a default OidcClient
by default. A named
OidcClient
can be selected with a quarkus.oidc-client-filter.client-name
configuration property. You can also select OidcClient
by setting value
attribute of the @OidcClientFilter
annotation. The client name set through
annotation has higher priority than the
quarkus.oidc-client-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;
@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Use Custom RestClient ClientFilter
If you prefer you can use your own custom filter and inject Tokens
:
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 will refresh the current expired access token if the refresh token
is available. Additionally, quarkus.oidc-client.refresh-token-time-skew
property can be used for a preemptive access token refreshment 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 then this token will be auto-refreshed.
If the access token needs to be refreshed but no refresh token is available
then an attempt will be made to acquire a new token using the configured
grant such as client_credentials
.
Note that some OpenID Connect Providers will not return a refresh token in a
client_credentials
grant response. For example, starting from Keycloak 12
a refresh token will not be returned by default for
client_credentials
. The providers might also restrict the number of times
a refresh token can be used.
Revoking Access Tokens
If your OpenId Connect provider such as Keycloak supports a token revocation
endpoint then OidcClient#revokeAccessToken
can be used to revoke the
current access token. The revocation endpoint URL will be discovered
alongside the token request URI or can be configured with
quarkus.oidc-client.revoke-path
.
You might want to have the access token revoked if using this token with a
REST client fails with HTTP 401
or the access token has already been used
for a long time and you’d like to refresh it.
This can be achieved by requesting a token refresh using a refresh token. However, if the refresh token is not available then you can refresh it by revoking it first and then request a new access token.
OidcClient Authentication
OidcClient
has to 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 is a key which will be used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc-client.credentials.client-secret.provider.key=mysecret-key
# 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 which 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
# 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 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.jks
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 methods
are used then the JWT signature algorithm, key identifier, audience, subject
and issuer can be customized, 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 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 not necessary.
quarkus.oidc-client.credentials.jwt.token-key-id=mykey
# Use 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
Apple POST JWT
Apple OpenID Connect Provider uses a client_secret_post
method where a
secret is a JWT produced with a 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 is authenticated as part
of the mutual TLS (mTLS
) authentication process.
quarkus-oidc-client
can be configured as follows to support mTLS
:
quarkus.oidc.tls.verification=certificate-validation
# Keystore configuration
quarkus.oidc.client.tls.key-store-file=client-keystore.jks
quarkus.oidc.client.tls.key-store-password=${key-store-password}
# Add more keystore properties if needed:
#quarkus.oidc.client.tls.key-store-alias=keyAlias
#quarkus.oidc.client.tls.key-store-alias-password=keyAliasPassword
# Truststore configuration
quarkus.oidc-client.tls.trust-store-file=client-truststore.jks
quarkus.oidc-client.tls.trust-store-password=${trust-store-password}
# Add more truststore properties if needed:
#quarkus.oidc.client.tls.trust-store-alias=certAlias
Testing
Start by adding the following dependencies to your test project:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
Wiremock
Add the following dependencies to your test project:
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
</dependency>
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;
}
}
}
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.
Set application.properties
, for example:
# Use 'keycloak.url' property set by the test KeycloakRealmResourceManager
quarkus.oidc-client.auth-server-url=${keycloak.url}
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
and finally write the test code. Given the Wiremock-based resource above,
the first test invocation should return access_token_1
access token which
will expire in 4 seconds. Use awaitility
to wait for about 5 seconds, and
now the next test invocation should return access_token_2
access token
which confirms the expired access_token_1
access token has been refreshed.
Keycloak
If you work with Keycloak then you can use the same approach as described in the OpenID Connect Bearer Token Integration testing Keycloak section.
How to check the errors in the logs
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
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 customization
You can customize OIDC requests made by Quarkus to the OIDC provider by
registering one or more OidcRequestFiler
implementations which can update
or add new request headers, for example, a filter can analyze the request
body and add its digest as a new header value:
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.core.http.HttpMethod;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpRequest;
@ApplicationScoped
@Unremovable
public class OidcRequestCustomizer implements OidcRequestFilter {
@Override
public void filter(HttpRequest<Buffer> request, Buffer buffer, OidcRequestContextProperties contextProperties) {
HttpMethod method = request.method();
String uri = request.uri();
if (method == HttpMethod.POST && uri.endsWith("/service") && buffer != null) {
request.putHeader("Digest", calculateDigest(buffer.toString()));
}
}
private String calculateDigest(String bodyString) {
// Apply the required digest algorithm to the body string
}
}
Token Propagation Reactive
The quarkus-oidc-token-propagation-reactive
extension provides RestEasy
Reactive Client
io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter
that simplifies the propagation of authentication information by propagating
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.
You can selectively register AccessTokenRequestReactiveFilter
by using
either io.quarkus.oidc.token.propagation.AccessToken
or
org.eclipse.microprofile.rest.client.annotation.RegisterProvider
annotation, for example:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.AccessToken;
@RegisterRestClient
@AccessToken
@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.reactive.AccessTokenRequestReactiveFilter;
@RegisterRestClient
@RegisterProvider(AccessTokenRequestReactiveFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Additionally, AccessTokenRequestReactiveFilter
can support a complex
application that needs to exchange the tokens before propagating them.
If you work with
Keycloak
or other OpenID Connect Providers which support 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.oidc-token-propagation.exchange-token=true
Note 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.oidc-token-propagation-reactive.exchange-token=true
AccessTokenRequestReactiveFilter
uses a default OidcClient
by default. A
named OidcClient
can be selected with a
quarkus.oidc-token-propagation-reactive.client-name
configuration
property.
Token Propagation
The quarkus-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, but in addition provides support for JWT tokens.
When you need to propagate the current Authorization Code Flow access token then the immediate token propagation will work well - as 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, the direct end to end Bearer token propagation should be avoided if
possible. For example, Client → Service A → Service B
where Service B
receives a token sent by Client
to Service A
. In such cases Service B
will not be able to distinguish if the token came from Service A
or from
Client
directly. For Service B
to verify the token came from Service A
it should be able to assert a 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 be
different when Service A
is accessing Service B
. In this case, Service
A
might be granted a narrow or a 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 it can
work with both JWT and opaque tokens.
You can selectively register AccessTokenRequestFilter
by using either
io.quarkus.oidc.token.propagation.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.AccessToken;
@RegisterRestClient
@AccessToken
@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.AccessTokenRequestFilter;
@RegisterRestClient
@RegisterProvider(AccessTokenRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Alternatively, AccessTokenRequestFilter
can be registered automatically
with all MP Rest or Jakarta REST clients if
quarkus.oidc-token-propagation.register-filter
property is set to true
and quarkus.oidc-token-propagation.json-web-token
property is set to
false
(which is a default value).
Exchange Token Before Propagation
If the current access token needs to be exchanged before propagation and you
work with
Keycloak
or other OpenID Connect Provider which supports a
Token Exchange token grant then
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.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 then 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.oidc-token-propagation.exchange-token=true
Note 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 a
quarkus.oidc-token-propagation.client-name
configuration property.
RestClient JsonWebTokenRequestFilter
Using JsonWebTokenRequestFilter
is recommended if you work with Bearer JWT
tokens where these tokens can have their claims such as issuer
and
audience
modified and the updated tokens secured (for example, re-signed)
again. It expects an injected org.eclipse.microprofile.jwt.JsonWebToken
and therefore will not work with the opaque tokens. Also, if your OpenID
Connect Provider supports a Token Exchange protocol then it is recommended
to use AccessTokenRequestFilter
instead - as 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 to ensure Service A
has a
signing key - it should be provisioned from a secure file system or from the
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;
@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;
@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
quarkus.oidc-token-propagation.register-filter
and
quarkus.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) and/or aud
(audience) claims updated and secured again with a new signature then you
can configure JsonWebTokenRequestFilter
like this:
quarkus.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 already noted above, use AccessTokenRequestFilter
if you work with
Keycloak or OpenID Connect Provider which supports a Token Exchange
protocol.
Testing
You can generate the tokens as described in
OpenID
Connect Bearer Token Integration testing section. Prepare the REST test
endpoints. You can have the test front-end endpoint, which uses the injected
MP REST client with a registered token propagation filter, call the
downstream endpoint. For example, see the
integration-tests/oidc-token-propagation
in the main
Quarkus repository.
Token Propagation Reactive
Add the following Maven Dependency:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-token-propagation-reactive</artifactId>
</dependency>
The quarkus-oidc-token-propagation-reactive
extension provides
io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter
which can be used to propagate the current Bearer
or Authorization Code
Flow
access tokens.
The quarkus-oidc-token-propagation-reactive
extension (as opposed to the
non-reactive quarkus-oidc-token-propagation
extension) does not currently
support the exchanging or resigning the tokens before the propagation.
However, these features might be added in the future.
GraphQL client integration
The quarkus-oidc-client-graphql
extension provides a way to integrate an
OIDC client into GraphQL clients. This
works similarly as with REST clients. When this extension is present, any
configured (that means NOT created programmatically via the builder, but via
configuration properties) GraphQL client will attempt to use the OIDC client
to obtain an access token and set it as an Authorization
header value.
OIDC client will also refresh expired access tokens.
To configure which OIDC client should be used by GraphQL client, 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 don’t 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 be able to use this with a programmatically created GraphQL client, both
builders (VertxDynamicGraphQLClientBuilder
and
VertxTypesafeGraphQLClientBuilder
) contain a method dynamicHeader(String,
Uni<String>
) that allows you to plug in a header that might change for
every request. To plug an OIDC client into it, use
@Inject
OidcClients oidcClients;
VertxTypesafeGraphQLClientBuilder builder = ....;
Uni<String> tokenUni = oidcClients.getClient("OIDC_CLIENT_NAME")
.getTokens().map(t -> "Bearer " + t.getAccessToken());
builder.dynamicHeader("Authorization", tokenUni);
VertxDynamicGraphQLClient client = builder.build();