이 콘텐츠는 선택한 언어로 제공되지 않습니다.

OpenID Connect (OIDC) client and token propagation


Red Hat build of Quarkus 3.8

Red Hat Customer Content Services

Abstract

This guide details how to configure and use OpenID Connect (OIDC) clients and token propagation, including token management, request filtering, and application setup.

Providing feedback on Red Hat build of Quarkus documentation

To report an error or to improve our documentation, log in to your Red Hat Jira account and submit an issue. If you do not have a Red Hat Jira account, then you will be prompted to create an account.

Procedure

  1. Click the following link to create a ticket.
  2. Enter a brief description of the issue in the Summary.
  3. Provide a detailed description of the issue or enhancement in the Description. Include a URL to where the issue occurs in the documentation.
  4. Clicking Submit creates and routes the issue to the appropriate documentation team.

Making open source more inclusive

Red Hat is committed to replacing problematic language in our code, documentation, and web properties. We are beginning with these four terms: master, slave, blacklist, and whitelist. Because of the enormity of this endeavor, these changes will be implemented gradually over several upcoming releases. For more details, see our CTO Chris Wright’s message.

Chapter 1. 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-oidc-client-reactive-filter and quarkus-oidc-client-filter extensions to acquire and refresh access tokens from OpenID Connect and OAuth 2.0 compliant Authorization Servers such as Keycloak.
  • Using quarkus-oidc-token-propagation-reactive and quarkus-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.

Also see OpenID Connect client and token propagation quickstart.

1.1. OidcClient

Add the following dependency:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-client</artifactId>
</dependency>
Copy to Clipboard Toggle word wrap

The 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 build time with the IDP token endpoint URL, which can be auto-discovered or manually configured. OidcClient uses this endpoint to acquire access tokens by using token grants such as client_credentials or password and refresh the tokens by using a refresh_token grant.

1.1.1. 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
Copy to Clipboard Toggle word wrap

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 unavailable 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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

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

1.1.2. Supported token grants

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

1.1.2.1. 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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap
1.1.2.2. 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
Copy to Clipboard Toggle word wrap

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

1.1.2.3. Other grants

OidcClient can also help acquire the tokens by using grants that require some extra 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 and 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 employs 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
Copy to Clipboard Toggle word wrap

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 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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

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.

1.1.2.4. 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

1.1.3. 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
    }
}
Copy to Clipboard Toggle word wrap

1.1.4. 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.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
    }
}
Copy to Clipboard Toggle word wrap

1.1.5. Use OidcClients

io.quarkus.oidc.client.OidcClients is a container of OidcClients - 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
Copy to Clipboard Toggle word wrap

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
    }
}
Copy to Clipboard Toggle word wrap
Note

If you also use OIDC multitenancy, and each OIDC tenant has its own associated OidcClient, you can use a Vert.x RoutingContext tenantId 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("tenantId");
        // named OIDC tenant and client configurations use the same key:
        OidcClient client = clients.getClient(tenantId);
        //Use this client to get the token
    }
}
Copy to Clipboard Toggle word wrap

If you need, you can also create a 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
    }
}
Copy to Clipboard Toggle word wrap

1.1.6. Inject named OidcClient and tokens

In case of multiple configured OidcClient objects, 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 the client to get the token
    }
}
Copy to Clipboard Toggle word wrap

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());
    }
}
Copy to Clipboard Toggle word wrap

1.1.7. Use OidcClient in RestClient Reactive ClientFilter

Add the following Maven Dependency:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-client-reactive-filter</artifactId>
</dependency>
Copy to Clipboard Toggle word wrap

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 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.

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();
}
Copy to Clipboard Toggle word wrap

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();
}
Copy to Clipboard Toggle word wrap

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 the 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();
}
Copy to Clipboard Toggle word wrap

1.1.8. Use OidcClient in RestClient ClientFilter

Add the following Maven Dependency:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-client-filter</artifactId>
</dependency>
Copy to Clipboard Toggle word wrap

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 unavailable, 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();
}
Copy to Clipboard Toggle word wrap

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();
}
Copy to Clipboard Toggle word wrap

Alternatively, OidcClientRequestFilter can be registered automatically with all MP Rest or Jakarta REST clients if the 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 the 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();
}
Copy to Clipboard Toggle word wrap

1.1.9. Use a 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());
    }
}
Copy to Clipboard Toggle word wrap

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

1.1.10. Refreshing access tokens

OidcClientRequestReactiveFilter, OidcClientRequestFilter and Tokens producers will refresh the current expired access token if the refresh token is available. Additionally, the 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 is made to acquire a new token by using a configured grant, such as client_credentials.

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.

1.1.11. 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 an HTTP 401 status code or if the access token has already been used for a long time and you would like to refresh it.

This can be achieved by requesting a token refresh by using a refresh token. However, if the refresh token is unavailable, you can refresh it by revoking it first and then requesting a new access token.

1.1.12. 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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

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
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc-client.credentials.client-secret.provider.name=oidc-credentials-provider
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

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
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc-client.credentials.jwt.secret-provider.name=oidc-credentials-provider
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

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

1.1.12.1. 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 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
Copy to Clipboard Toggle word wrap
1.1.12.2. 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}
Copy to Clipboard Toggle word wrap
1.1.12.3. 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-client.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
Copy to Clipboard Toggle word wrap

1.1.13. 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>
Copy to Clipboard Toggle word wrap
1.1.13.1. Wiremock

Add the following dependencies to your test project:

<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock</artifactId>
    <scope>test</scope>
</dependency>
Copy to Clipboard Toggle word wrap

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;
        }
    }
}
Copy to Clipboard Toggle word wrap

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 the '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
Copy to Clipboard Toggle word wrap

And finally, 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, and now the next test invocation should return the access_token_2 access token, which confirms the expired access_token_1 access token has been refreshed.

1.1.13.2. Keycloak

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

1.1.14. 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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

1.2. OIDC request filters

You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more OidcRequestFilter 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
    }
}
Copy to Clipboard Toggle word wrap

1.3. Token Propagation Reactive

The quarkus-oidc-token-propagation-reactive extension provides a RestEasy Reactive Client, io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter, that simplifies the propagation of authentication information. This client 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 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();
}
Copy to Clipboard Toggle word wrap

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();
}
Copy to Clipboard Toggle word wrap

Additionally, AccessTokenRequestReactiveFilter can support a complex application that needs to exchange the 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.oidc-token-propagation.exchange-token=true 
1
Copy to Clipboard Toggle word wrap
1
Please note that the exchange-token configuration property is ignored when the OidcClient name is set with the io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient annotation attribute.

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
Copy to Clipboard Toggle word wrap

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

1.4. 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. 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 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 completely different set of scopes to access Service B.

The following sections show how AccessTokenRequestFilter and JsonWebTokenRequestFilter can help.

1.4.1. 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();
}
Copy to Clipboard Toggle word wrap

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();
}
Copy to Clipboard Toggle word wrap

Alternatively, AccessTokenRequestFilter can be registered automatically with all MP Rest or Jakarta REST clients if the 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).

1.4.1.1. 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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

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.

1.4.2. 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 ensuring that Service A has a signing key; it 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;

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

    @GET
    String getUserName();
}
Copy to Clipboard Toggle word wrap

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();
}
Copy to Clipboard Toggle word wrap

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.

1.4.2.1. 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.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
Copy to Clipboard Toggle word wrap

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

1.4.3. 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.

1.5. Token Propagation Reactive

Add the following Maven Dependency:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-token-propagation-reactive</artifactId>
</dependency>
Copy to Clipboard Toggle word wrap

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 of the tokens before the propagation. However, these features might be added in the future.

1.6. References

Chapter 2. OpenID Connect client and token propagation quickstart

Learn how to use OpenID Connect (OIDC) and OAuth2 clients with filters to get, refresh, and propagate access tokens in your applications.

For more information about OIDC Client and Token Propagation support in Quarkus, see the OpenID Connect (OIDC) and OAuth2 client and filters reference guide.

To protect your applications by using Bearer Token Authorization, see the OpenID Connect (OIDC) Bearer token authentication guide.

2.1. Prerequisites

To complete this guide, you need:

  • Roughly 15 minutes
  • An IDE
  • JDK 17+ installed with JAVA_HOME configured appropriately
  • Apache Maven 3.9.6
  • A working container runtime (Docker or Podman)
  • Optionally the Quarkus CLI if you want to use it
  • Optionally Mandrel or GraalVM installed and configured appropriately if you want to build a native executable (or Docker if you use a native container build)
  • jq tool

2.2. Architecture

In this example, an application is built with two Jakarta REST resources, FrontendResource and ProtectedResource. Here, FrontendResource uses one of two methods to propagate access tokens to ProtectedResource:

  • It can get a token by using an OIDC token propagation Reactive filter before propagating it.
  • It can use an OIDC token propagation Reactive filter to propagate the incoming access token.

FrontendResource has four endpoints:

  • /frontend/user-name-with-oidc-client-token
  • /frontend/admin-name-with-oidc-client-token
  • /frontend/user-name-with-propagated-token
  • /frontend/admin-name-with-propagated-token

FrontendResource uses a REST Client with an OIDC token propagation Reactive filter to get and propagate an access token to ProtectedResource when either /frontend/user-name-with-oidc-client-token or /frontend/admin-name-with-oidc-client-token is called. Also, FrontendResource uses a REST Client with OpenID Connect Token Propagation Reactive Filter to propagate the current incoming access token to ProtectedResource when either /frontend/user-name-with-propagated-token or /frontend/admin-name-with-propagated-token is called.

ProtectedResource has two endpoints:

  • /protected/user-name
  • /protected/admin-name

Both endpoints return the username extracted from the incoming access token, which was propagated to ProtectedResource from FrontendResource. The only difference between these endpoints is that calling /protected/user-name is only allowed if the current access token has a user role, and calling /protected/admin-name is only allowed if the current access token has an admin role.

2.3. Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git -b 3.8, or download an archive.

The solution is in the security-openid-connect-client-quickstart directory.

2.4. Creating the Maven project

First, you need a new project. Create a new project with the following command:

  • Using the Quarkus CLI:

    quarkus create app org.acme:security-openid-connect-client-quickstart \
        --extension='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive' \
        --no-code
    cd security-openid-connect-client-quickstart
    Copy to Clipboard Toggle word wrap

    To create a Gradle project, add the --gradle or --gradle-kotlin-dsl option.

    For more information about how to install and use the Quarkus CLI, see the Quarkus CLI guide.

  • Using Maven:

    mvn io.quarkus.platform:quarkus-maven-plugin:3.8.5:create \
        -DprojectGroupId=org.acme \
        -DprojectArtifactId=security-openid-connect-client-quickstart \
        -Dextensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive' \
        -DnoCode
    cd security-openid-connect-client-quickstart
    Copy to Clipboard Toggle word wrap

    To create a Gradle project, add the -DbuildTool=gradle or -DbuildTool=gradle-kotlin-dsl option.

For Windows users:

  • If using cmd, (don’t use backward slash \ and put everything on the same line)
  • If using Powershell, wrap -D parameters in double quotes e.g. "-DprojectArtifactId=security-openid-connect-client-quickstart"

This command generates a Maven project, importing the oidc, oidc-client-reactive-filter, oidc-token-propagation-reactive-filter, and resteasy-reactive extensions.

If you already have your Quarkus project configured, you can add these extensions to your project by running the following command in your project base directory:

  • Using the Quarkus CLI:

    quarkus extension add oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive
    Copy to Clipboard Toggle word wrap
  • Using Maven:

    ./mvnw quarkus:add-extension -Dextensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'
    Copy to Clipboard Toggle word wrap
  • Using Gradle:

    ./gradlew addExtension --extensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'
    Copy to Clipboard Toggle word wrap

This command adds the following extensions to your build file:

  • Using Maven:

    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-oidc</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-oidc-client-reactive-filter</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-oidc-token-propagation-reactive</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-reactive</artifactId>
    </dependency>
    Copy to Clipboard Toggle word wrap
  • Using Gradle:

    implementation("io.quarkus:quarkus-oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive")
    Copy to Clipboard Toggle word wrap

2.5. Writing the application

Start by implementing ProtectedResource:

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

import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;

import org.eclipse.microprofile.jwt.JsonWebToken;

@Path("/protected")
@Authenticated
public class ProtectedResource {

    @Inject
    JsonWebToken principal;

    @GET
    @RolesAllowed("user")
    @Produces("text/plain")
    @Path("userName")
    public Uni<String> userName() {
        return Uni.createFrom().item(principal.getName());
    }

    @GET
    @RolesAllowed("admin")
    @Produces("text/plain")
    @Path("adminName")
    public Uni<String> adminName() {
        return Uni.createFrom().item(principal.getName());
    }
}
Copy to Clipboard Toggle word wrap

ProtectedResource returns a name from both userName() and adminName() methods. The name is extracted from the current JsonWebToken.

Next, add two REST clients, OidcClientRequestReactiveFilter and AccessTokenRequestReactiveFilter, which FrontendResource uses to call ProtectedResource.

Add the OidcClientRequestReactiveFilter REST Client:

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

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

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 RestClientWithOidcClientFilter {

    @GET
    @Produces("text/plain")
    @Path("userName")
    Uni<String> getUserName();

    @GET
    @Produces("text/plain")
    @Path("adminName")
    Uni<String> getAdminName();
}
Copy to Clipboard Toggle word wrap

The RestClientWithOidcClientFilter interface depends on OidcClientRequestReactiveFilter to get and propagate the tokens.

Add the AccessTokenRequestReactiveFilter REST Client:

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

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

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 io.smallrye.mutiny.Uni;

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

    @GET
    @Produces("text/plain")
    @Path("userName")
    Uni<String> getUserName();

    @GET
    @Produces("text/plain")
    @Path("adminName")
    Uni<String> getAdminName();
}
Copy to Clipboard Toggle word wrap

The RestClientWithTokenPropagationFilter interface depends on AccessTokenRequestReactiveFilter to propagate the incoming already-existing tokens.

Note that both RestClientWithOidcClientFilter and RestClientWithTokenPropagationFilter interfaces are the same. This is because combining OidcClientRequestReactiveFilter and AccessTokenRequestReactiveFilter on the same REST Client causes side effects because both filters can interfere with each other. For example, OidcClientRequestReactiveFilter can override the token propagated by AccessTokenRequestReactiveFilter, or AccessTokenRequestReactiveFilter can fail if it is called when no token is available to propagate and OidcClientRequestReactiveFilter is expected to get a new token instead.

Now, finish creating the application by adding FrontendResource:

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

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

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

import io.smallrye.mutiny.Uni;

@Path("/frontend")
public class FrontendResource {
    @Inject
    @RestClient
    RestClientWithOidcClientFilter restClientWithOidcClientFilter;

    @Inject
    @RestClient
    RestClientWithTokenPropagationFilter restClientWithTokenPropagationFilter;

    @GET
    @Path("user-name-with-oidc-client-token")
    @Produces("text/plain")
    public Uni<String> getUserNameWithOidcClientToken() {
        return restClientWithOidcClientFilter.getUserName();
    }

    @GET
    @Path("admin-name-with-oidc-client-token")
    @Produces("text/plain")
    public Uni<String> getAdminNameWithOidcClientToken() {
	    return restClientWithOidcClientFilter.getAdminName();
    }

    @GET
    @Path("user-name-with-propagated-token")
    @Produces("text/plain")
    public Uni<String> getUserNameWithPropagatedToken() {
        return restClientWithTokenPropagationFilter.getUserName();
    }

    @GET
    @Path("admin-name-with-propagated-token")
    @Produces("text/plain")
    public Uni<String> getAdminNameWithPropagatedToken() {
        return restClientWithTokenPropagationFilter.getAdminName();
    }
}
Copy to Clipboard Toggle word wrap

FrontendResource uses REST Client with an OIDC token propagation Reactive filter to get and propagate an access token to ProtectedResource when either /frontend/user-name-with-oidc-client-token or /frontend/admin-name-with-oidc-client-token is called. Also, FrontendResource uses REST Client with OpenID Connect Token Propagation Reactive Filter to propagate the current incoming access token to ProtectedResource when either /frontend/user-name-with-propagated-token or /frontend/admin-name-with-propagated-token is called.

Finally, add a Jakarta REST ExceptionMapper:

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

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

import org.jboss.resteasy.reactive.ClientWebApplicationException;

@Provider
public class FrontendExceptionMapper implements ExceptionMapper<ClientWebApplicationException> {

	@Override
	public Response toResponse(ClientWebApplicationException t) {
		return Response.status(t.getResponse().getStatus()).build();
	}

}
Copy to Clipboard Toggle word wrap

This exception mapper is only added to verify during the tests that ProtectedResource returns 403 when the token has no expected role. Without this mapper, RESTEasy Reactive would correctly convert the exceptions that escape from REST Client calls to 500 to avoid leaking the information from the downstream resources such as ProtectedResource. However, in the tests, it would not be possible to assert that 500 is caused by an authorization exception instead of some internal error.

2.6. Configuring the application

Having prepared the code, you configure the application:

# Configure OIDC

%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=backend-service
quarkus.oidc.credentials.secret=secret

# Tell Dev Services for Keycloak to import the realm file
# This property is ineffective when running the application in JVM or Native modes but only in dev and test modes.

quarkus.keycloak.devservices.realm-path=quarkus-realm.json

# Configure OIDC Client

quarkus.oidc-client.auth-server-url=${quarkus.oidc.auth-server-url}
quarkus.oidc-client.client-id=${quarkus.oidc.client-id}
quarkus.oidc-client.credentials.secret=${quarkus.oidc.credentials.secret}
quarkus.oidc-client.grant.type=password
quarkus.oidc-client.grant-options.password.username=alice
quarkus.oidc-client.grant-options.password.password=alice

# Configure REST clients

%prod.port=8080
%dev.port=8080
%test.port=8081

org.acme.security.openid.connect.client.RestClientWithOidcClientFilter/mp-rest/url=http://localhost:${port}/protected
org.acme.security.openid.connect.client.RestClientWithTokenPropagationFilter/mp-rest/url=http://localhost:${port}/protected
Copy to Clipboard Toggle word wrap

This configuration references Keycloak, which is used by ProtectedResource to verify the incoming access tokens and by OidcClient to get the tokens for a user alice by using a password grant. Both REST clients point to `ProtectedResource’s HTTP address.

Note

Adding a %prod. profile prefix to quarkus.oidc.auth-server-url ensures that Dev Services for Keycloak launches a container for you when the application is run in dev or test modes. For more information, see the Running the application in dev mode section.

2.7. Starting and configuring the Keycloak server

Note

Do not start the Keycloak server when you run the application in dev or test modes; Dev Services for Keycloak launches a container. For more information, see the Running the application in dev mode section. Ensure you put the realm configuration file on the classpath, in the target/classes directory. This placement ensures that the file is automatically imported in dev mode. However, if you have already built a complete solution, you do not need to add the realm file to the classpath because the build process has already done so.

To start a Keycloak Server, you can use Docker and just run the following command:

docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev
Copy to Clipboard Toggle word wrap

Set {keycloak.version} to 24.0.0 or later.

You can access your Keycloak Server at localhost:8180.

Log in as the admin user to access the Keycloak Administration Console. The password is admin.

Import the realm configuration file to create a new realm. For more details, see the Keycloak documentation about how to create a new realm.

This quarkus realm file adds a frontend client, and alice and admin users. alice has a user role. admin has both user and admin roles.

2.8. Running the application in dev mode

To run the application in a dev mode, use:

  • Using the Quarkus CLI:

    quarkus dev
    Copy to Clipboard Toggle word wrap
  • Using Maven:

    ./mvnw quarkus:dev
    Copy to Clipboard Toggle word wrap
  • Using Gradle:

    ./gradlew --console=plain quarkusDev
    Copy to Clipboard Toggle word wrap

Dev Services for Keycloak launches a Keycloak container and imports quarkus-realm.json.

Open a Dev UI available at /q/dev-ui and click a Provider: Keycloak link in the OpenID Connect Dev UI card.

When asked, log in to a Single Page Application provided by the OpenID Connect Dev UI:

  • Log in as alice, with the password, alice. This user has a user role.

    • Access /frontend/user-name-with-propagated-token, which returns 200.
    • Access /frontend/admin-name-with-propagated-token, which returns 403.
  • Log out and back in as admin with the password, admin. This user has both admin and user roles.

    • Access /frontend/user-name-with-propagated-token, which returns 200.
    • Access /frontend/admin-name-with-propagated-token, which returns 200.

In this case, you are testing that FrontendResource can propagate the access tokens from the OpenID Connect Dev UI.

2.9. Running the application in JVM mode

After exploring the application in dev mode, you can run it as a standard Java application.

First, compile it:

  • Using the Quarkus CLI:

    quarkus build
    Copy to Clipboard Toggle word wrap
  • Using Maven:

    ./mvnw install
    Copy to Clipboard Toggle word wrap
  • Using Gradle:

    ./gradlew build
    Copy to Clipboard Toggle word wrap

Then, run it:

java -jar target/quarkus-app/quarkus-run.jar
Copy to Clipboard Toggle word wrap

2.10. Running the application in native mode

You can compile this demo into native code; no modifications are required.

This implies that you no longer need to install a JVM on your production environment, as the runtime technology is included in the produced binary and optimized to run with minimal resources.

Compilation takes longer, so this step is turned off by default. To build again, enable the native profile:

  • Using the Quarkus CLI:

    quarkus build --native
    Copy to Clipboard Toggle word wrap
  • Using Maven:

    ./mvnw install -Dnative
    Copy to Clipboard Toggle word wrap
  • Using Gradle:

    ./gradlew build -Dquarkus.package.type=native
    Copy to Clipboard Toggle word wrap

After a little while, when the build finishes, you can run the native binary directly:

./target/security-openid-connect-quickstart-1.0.0-SNAPSHOT-runner
Copy to Clipboard Toggle word wrap

2.11. Testing the application

For more information about testing your application in dev mode, see the preceding Running the application in dev mode section.

You can test the application launched in JVM or Native modes with curl.

Obtain an access token for alice:

export access_token=$(\
    curl --insecure -X POST http://localhost:8180/realms/quarkus/protocol/openid-connect/token \
    --user backend-service:secret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=alice&password=alice&grant_type=password' | jq --raw-output '.access_token' \
 )
Copy to Clipboard Toggle word wrap

Now, use this token to call /frontend/user-name-with-propagated-token and /frontend/admin-name-with-propagated-token:

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token
Copy to Clipboard Toggle word wrap

This command returns the 200 status code and the name alice.

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token
Copy to Clipboard Toggle word wrap

In contrast, this command returns 403. Recall that alice only has a user role.

Next, obtain an access token for admin:

export access_token=$(\
    curl --insecure -X POST http://localhost:8180/realms/quarkus/protocol/openid-connect/token \
    --user backend-service:secret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=admin&password=admin&grant_type=password' | jq --raw-output '.access_token' \
 )
Copy to Clipboard Toggle word wrap

Use this token to call /frontend/user-name-with-propagated-token:

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token
Copy to Clipboard Toggle word wrap

This command returns a 200 status code and the name admin.

Now, use this token to call /frontend/admin-name-with-propagated-token:

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token
Copy to Clipboard Toggle word wrap

This command also returns the 200 status code and the name admin because admin has both user and admin roles.

Now, check the FrontendResource methods, which do not propagate the existing tokens but use OidcClient to get and propagate the tokens. As already shown, OidcClient is configured to get the tokens for the alice user, so:

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-oidc-client-token
Copy to Clipboard Toggle word wrap

This command returns the 200 status code and the name alice.

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-oidc-client-token
Copy to Clipboard Toggle word wrap

In contrast with the preceding command, this command returns a 403 status code.

2.12. References

Legal Notice

Copyright © 2025 Red Hat, Inc.
The text of and illustrations in this document are licensed by Red Hat under a Creative Commons Attribution–Share Alike 3.0 Unported license ("CC-BY-SA"). An explanation of CC-BY-SA is available at http://creativecommons.org/licenses/by-sa/3.0/. In accordance with CC-BY-SA, if you distribute this document or an adaptation of it, you must provide the URL for the original version.
Red Hat, as the licensor of this document, waives the right to enforce, and agrees not to assert, Section 4d of CC-BY-SA to the fullest extent permitted by applicable law.
Red Hat, Red Hat Enterprise Linux, the Shadowman logo, the Red Hat logo, JBoss, OpenShift, Fedora, the Infinity logo, and RHCE are trademarks of Red Hat, Inc., registered in the United States and other countries.
Linux® is the registered trademark of Linus Torvalds in the United States and other countries.
Java® is a registered trademark of Oracle and/or its affiliates.
XFS® is a trademark of Silicon Graphics International Corp. or its subsidiaries in the United States and/or other countries.
MySQL® is a registered trademark of MySQL AB in the United States, the European Union and other countries.
Node.js® is an official trademark of Joyent. Red Hat is not formally related to or endorsed by the official Joyent Node.js open source or commercial project.
The OpenStack® Word Mark and OpenStack logo are either registered trademarks/service marks or trademarks/service marks of the OpenStack Foundation, in the United States and other countries and are used with the OpenStack Foundation's permission. We are not affiliated with, endorsed or sponsored by the OpenStack Foundation, or the OpenStack community.
All other trademarks are the property of their respective owners.
맨 위로 이동
Red Hat logoGithubredditYoutubeTwitter

자세한 정보

평가판, 구매 및 판매

커뮤니티

Red Hat 문서 정보

Red Hat을 사용하는 고객은 신뢰할 수 있는 콘텐츠가 포함된 제품과 서비스를 통해 혁신하고 목표를 달성할 수 있습니다. 최신 업데이트를 확인하세요.

보다 포괄적 수용을 위한 오픈 소스 용어 교체

Red Hat은 코드, 문서, 웹 속성에서 문제가 있는 언어를 교체하기 위해 최선을 다하고 있습니다. 자세한 내용은 다음을 참조하세요.Red Hat 블로그.

Red Hat 소개

Red Hat은 기업이 핵심 데이터 센터에서 네트워크 에지에 이르기까지 플랫폼과 환경 전반에서 더 쉽게 작업할 수 있도록 강화된 솔루션을 제공합니다.

Theme

© 2026 Red Hat