이 콘텐츠는 선택한 언어로 제공되지 않습니다.
OpenID Connect (OIDC) client and token propagation
Abstract
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
- Click the following link to create a ticket.
- Enter a brief description of the issue in the Summary.
- Provide a detailed description of the issue or enhancement in the Description. Include a URL to where the issue occurs in the documentation.
- Clicking Submit creates and routes the issue to the appropriate documentation team.
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-rest-client-oidc-filter
andquarkus-resteasy-client-oidc-filter
extensions to acquire and refresh access tokens from OpenID Connect and OAuth 2.0 compliant Authorization Servers such as Keycloak. -
Using
quarkus-rest-client-oidc-token-propagation
andquarkus-resteasy-client-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.
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>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client</artifactId>
</dependency>
Copy to clipboardCopied
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
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus
Copy to clipboardCopied
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
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 clipboardCopied
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
quarkus.oidc-client.token-path=http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens
Copy to clipboardCopied
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
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 clipboardCopied
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
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 clipboardCopied1.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
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 clipboardCopied
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
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 clipboardCopied
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 for Quarkus REST and Token Propagation for RESTEasy Classic 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
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 clipboardCopied
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
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 clipboardCopied
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
You can use OidcClient
directly to acquire access tokens and set them in an HTTP Authorization
header as a Bearer
scheme value.
For example, let’s assume the Quarkus endpoint has to access a microservice that returns a user name. First, create a REST client:
package org.acme.security.openid.connect.client; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.smallrye.mutiny.Uni; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @RegisterRestClient @Path("/") public interface RestClientWithTokenHeaderParam { @GET @Produces("text/plain") @Path("userName") Uni<String> getUserName(@HeaderParam("Authorization") String authorization); }
package org.acme.security.openid.connect.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
@RegisterRestClient
@Path("/")
public interface RestClientWithTokenHeaderParam {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName(@HeaderParam("Authorization") String authorization);
}
Copy to clipboardCopied
Now, use OidcClient
to acquire the tokens and propagate them:
package org.acme.security.openid.connect.client; import org.eclipse.microprofile.rest.client.inject.RestClient; import io.quarkus.oidc.client.runtime.TokensHelper; import io.quarkus.oidc.client.OidcClient; import io.smallrye.mutiny.Uni; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @Path("/service") public class OidcClientResource { @Inject OidcClient client; TokensHelper tokenHelper = new TokensHelper(); 1 @Inject @RestClient RestClientWithTokenHeaderParam restClient; @GET @Path("user-name") @Produces("text/plain") public Uni<String> getUserName() { return tokenHelper.getTokens(client).onItem() .transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken())); } }
package org.acme.security.openid.connect.client;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import io.quarkus.oidc.client.runtime.TokensHelper;
import io.quarkus.oidc.client.OidcClient;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
@Path("/service")
public class OidcClientResource {
@Inject
OidcClient client;
TokensHelper tokenHelper = new TokensHelper(); 1
@Inject
@RestClient
RestClientWithTokenHeaderParam restClient;
@GET
@Path("user-name")
@Produces("text/plain")
public Uni<String> getUserName() {
return tokenHelper.getTokens(client).onItem()
.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
}
}
Copy to clipboardCopied- 1
io.quarkus.oidc.client.runtime.TokensHelper
manages the access token acquisition and refresh.
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.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import io.quarkus.oidc.client.Tokens; @Path("/service") public class OidcClientResource { @Inject Tokens tokens; @GET public String getResponse() { // Get the access token, which might have been refreshed. String accessToken = tokens.getAccessToken(); // Use the access token to configure MP RestClient Authorization header/etc } }
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.quarkus.oidc.client.Tokens;
@Path("/service")
public class OidcClientResource {
@Inject Tokens tokens;
@GET
public String getResponse() {
// Get the access token, which might have been refreshed.
String accessToken = tokens.getAccessToken();
// Use the access token to configure MP RestClient Authorization header/etc
}
}
Copy to clipboardCopied1.1.5. 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
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 clipboardCopied
In this case, the default client is disabled with a client-enabled=false
property. The jwt-secret
client can be accessed like this:
import org.eclipse.microprofile.rest.client.inject.RestClient; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import io.smallrye.mutiny.Uni; import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.OidcClients; import io.quarkus.oidc.client.runtime.TokensHelper; @Path("/clients") public class OidcClientResource { @Inject OidcClients clients; TokensHelper tokenHelper = new TokensHelper(); @Inject @RestClient RestClientWithTokenHeaderParam restClient; 1 @GET @Path("user-name") @Produces("text/plain") public Uni<String> getUserName() { OidcClient client = clients.getClient("jwt-secret"); return tokenHelper.getTokens(client).onItem() .transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken())); } }
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.oidc.client.runtime.TokensHelper;
@Path("/clients")
public class OidcClientResource {
@Inject
OidcClients clients;
TokensHelper tokenHelper = new TokensHelper();
@Inject
@RestClient
RestClientWithTokenHeaderParam restClient; 1
@GET
@Path("user-name")
@Produces("text/plain")
public Uni<String> getUserName() {
OidcClient client = clients.getClient("jwt-secret");
return tokenHelper.getTokens(client).onItem()
.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
}
}
Copy to clipboardCopied- 1
- See the
RestClientWithTokenHeaderParam
declaration in the Use OidcClient directly section.
If you also use OIDC multitenancy, and each OIDC tenant has its own associated OidcClient
, you can use a Vert.x RoutingContext
tenant-id
attribute. For example:
import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.OidcClients; import io.vertx.ext.web.RoutingContext; @Path("/clients") public class OidcClientResource { @Inject OidcClients clients; @Inject RoutingContext context; @GET public String getResponse() { String tenantId = context.get("tenant-id"); // named OIDC tenant and client configurations use the same key: OidcClient client = clients.getClient(tenantId); //Use this client to get the token } }
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;
import io.vertx.ext.web.RoutingContext;
@Path("/clients")
public class OidcClientResource {
@Inject
OidcClients clients;
@Inject
RoutingContext context;
@GET
public String getResponse() {
String tenantId = context.get("tenant-id");
// named OIDC tenant and client configurations use the same key:
OidcClient client = clients.getClient(tenantId);
//Use this client to get the token
}
}
Copy to clipboardCopied
You can also create a new OidcClient
programmatically. For example, let’s assume you must create it at startup time:
package org.acme.security.openid.connect.client; import java.util.Map; import org.eclipse.microprofile.config.inject.ConfigProperty; import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.runtime.OidcClientConfig; import io.quarkus.oidc.client.runtime.OidcClientConfig.Grant.Type; import io.quarkus.oidc.client.OidcClients; import io.quarkus.runtime.StartupEvent; import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; @ApplicationScoped public class OidcClientCreator { @Inject OidcClients oidcClients; @ConfigProperty(name = "quarkus.oidc.auth-server-url") String oidcProviderAddress; private volatile OidcClient oidcClient; public void startup(@Observes StartupEvent event) { createOidcClient().subscribe().with(client -> {oidcClient = client;}); } public OidcClient getOidcClient() { return oidcClient; } private Uni<OidcClient> createOidcClient() { OidcClientConfig cfg = OidcClientConfig .authServerUrl(oidcProviderAddress) .id("myclient") .clientId("backend-service") .credentials("secret") .grant(Type.PASSWORD) .grantOptions("password", Map.of("username", "alice", "password", "alice")) .build(); return oidcClients.newClient(cfg); } }
package org.acme.security.openid.connect.client;
import java.util.Map;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.runtime.OidcClientConfig;
import io.quarkus.oidc.client.runtime.OidcClientConfig.Grant.Type;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.runtime.StartupEvent;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
@ApplicationScoped
public class OidcClientCreator {
@Inject
OidcClients oidcClients;
@ConfigProperty(name = "quarkus.oidc.auth-server-url")
String oidcProviderAddress;
private volatile OidcClient oidcClient;
public void startup(@Observes StartupEvent event) {
createOidcClient().subscribe().with(client -> {oidcClient = client;});
}
public OidcClient getOidcClient() {
return oidcClient;
}
private Uni<OidcClient> createOidcClient() {
OidcClientConfig cfg = OidcClientConfig
.authServerUrl(oidcProviderAddress)
.id("myclient")
.clientId("backend-service")
.credentials("secret")
.grant(Type.PASSWORD)
.grantOptions("password", Map.of("username", "alice", "password", "alice"))
.build();
return oidcClients.newClient(cfg);
}
}
Copy to clipboardCopiedNow, you can use this client like this:
import org.eclipse.microprofile.rest.client.inject.RestClient; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import io.smallrye.mutiny.Uni; import io.quarkus.oidc.client.runtime.TokensHelper; @Path("/clients") public class OidcClientResource { @Inject OidcClientCreator oidcClientCreator; TokensHelper tokenHelper = new TokensHelper(); @Inject @RestClient RestClientWithTokenHeaderParam restClient; 1 @GET @Path("user-name") @Produces("text/plain") public Uni<String> getUserName() { return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem() .transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken())); } }
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.runtime.TokensHelper;
@Path("/clients")
public class OidcClientResource {
@Inject
OidcClientCreator oidcClientCreator;
TokensHelper tokenHelper = new TokensHelper();
@Inject
@RestClient
RestClientWithTokenHeaderParam restClient; 1
@GET
@Path("user-name")
@Produces("text/plain")
public Uni<String> getUserName() {
return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem()
.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
}
}
Copy to clipboardCopied- 1
- See the
RestClientWithTokenHeaderParam
declaration in the Use OidcClient directly section.
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 org.acme.security.openid.connect.client; import org.eclipse.microprofile.rest.client.inject.RestClient; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import io.smallrye.mutiny.Uni; import io.quarkus.oidc.client.NamedOidcClient; import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.runtime.TokensHelper; @Path("/clients") public class OidcClientResource { @Inject @NamedOidcClient("jwt-secret") OidcClient client; TokensHelper tokenHelper = new TokensHelper(); @Inject @RestClient RestClientWithTokenHeaderParam restClient; 1 @GET @Path("user-name") @Produces("text/plain") public Uni<String> getUserName() { return tokenHelper.getTokens(client).onItem() .transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken())); } }
package org.acme.security.openid.connect.client;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.NamedOidcClient;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.runtime.TokensHelper;
@Path("/clients")
public class OidcClientResource {
@Inject
@NamedOidcClient("jwt-secret")
OidcClient client;
TokensHelper tokenHelper = new TokensHelper();
@Inject
@RestClient
RestClientWithTokenHeaderParam restClient; 1
@GET
@Path("user-name")
@Produces("text/plain")
public Uni<String> getUserName() {
return tokenHelper.getTokens(client).onItem()
.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
}
}
Copy to clipboardCopied- 1
- See the
RestClientWithTokenHeaderParam
declaration in the Use OidcClient directly section.
The same qualifier can be used to specify the OidcClient
used for a Tokens
injection:
import java.io.IOException; import jakarta.annotation.Priority; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.client.ClientRequestFilter; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.ext.Provider; import io.quarkus.oidc.client.NamedOidcClient; import io.quarkus.oidc.client.Tokens; @Provider @Priority(Priorities.AUTHENTICATION) @RequestScoped public class OidcClientRequestCustomFilter implements ClientRequestFilter { @Inject @NamedOidcClient("jwt-secret") Tokens tokens; @Override public void filter(ClientRequestContext requestContext) throws IOException { requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken()); } }
import java.io.IOException;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.ext.Provider;
import io.quarkus.oidc.client.NamedOidcClient;
import io.quarkus.oidc.client.Tokens;
@Provider
@Priority(Priorities.AUTHENTICATION)
@RequestScoped
public class OidcClientRequestCustomFilter implements ClientRequestFilter {
@Inject
@NamedOidcClient("jwt-secret")
Tokens tokens;
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken());
}
}
Copy to clipboardCopied1.1.7. Use OidcClient in RestClient Reactive ClientFilter
Add the following Maven Dependency:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-rest-client-oidc-filter</artifactId> </dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-oidc-filter</artifactId>
</dependency>
Copy to clipboardCopied
It will also bring io.quarkus:quarkus-oidc-client
.
quarkus-rest-client-oidc-filter
extension provides io.quarkus.oidc.client.filter.OidcClientRequestReactiveFilter
.
It works similarly to 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.inject.RegisterRestClient; import io.quarkus.oidc.client.filter.OidcClientFilter; import io.smallrye.mutiny.Uni; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @OidcClientFilter @Path("/") public interface ProtectedResourceService { @GET Uni<String> getUserName(); }
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter
@Path("/")
public interface ProtectedResourceService {
@GET
Uni<String> getUserName();
}
Copy to clipboardCopiedor
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.client.reactive.filter.OidcClientRequestReactiveFilter; import io.smallrye.mutiny.Uni; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @RegisterProvider(OidcClientRequestReactiveFilter.class) @Path("/") public interface ProtectedResourceService { @GET Uni<String> getUserName(); }
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.reactive.filter.OidcClientRequestReactiveFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(OidcClientRequestReactiveFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
Uni<String> getUserName();
}
Copy to clipboardCopied
OidcClientRequestReactiveFilter
uses a default OidcClient
by default. A named OidcClient
can be selected with a quarkus.rest-client-oidc-filter.client-name
configuration property. You can also select OidcClient
by setting the value
attribute of the @OidcClientFilter
annotation. The client name set through annotation has higher priority than the quarkus.rest-client-oidc-filter.client-name
configuration property. For example, given this jwt-secret
named OIDC client declaration, you can refer to this client like this:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.client.filter.OidcClientFilter; import io.smallrye.mutiny.Uni; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @OidcClientFilter("jwt-secret") @Path("/") public interface ProtectedResourceService { @GET Uni<String> getUserName(); }
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {
@GET
Uni<String> getUserName();
}
Copy to clipboardCopied1.1.8. Use OidcClient in RestClient ClientFilter
Add the following Maven Dependency:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-client-oidc-filter</artifactId> </dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-client-oidc-filter</artifactId>
</dependency>
Copy to clipboardCopied
It will also bring io.quarkus:quarkus-oidc-client
.
quarkus-resteasy-client-oidc-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; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @OidcClientFilter @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Copy to clipboardCopiedor
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.client.filter.OidcClientRequestFilter; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @RegisterProvider(OidcClientRequestFilter.class) @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(OidcClientRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Copy to clipboardCopied
Alternatively, OidcClientRequestFilter
can be registered automatically with all MP Rest or Jakarta REST clients if the quarkus.resteasy-client-oidc-filter.register-filter=true
property is set.
OidcClientRequestFilter
uses a default OidcClient
by default. A named OidcClient
can be selected with a quarkus.resteasy-client-oidc-filter.client-name
configuration property. You can also select OidcClient
by setting the value
attribute of the @OidcClientFilter
annotation. The client name set through annotation has higher priority than the quarkus.resteasy-client-oidc-filter.client-name
configuration property. For example, given this jwt-secret
named OIDC client declaration, you can refer to this client like this:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.client.filter.OidcClientFilter; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @OidcClientFilter("jwt-secret") @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Copy to clipboardCopied1.1.9. Use a custom RestClient ClientFilter
If you prefer, you can use your own custom filter and inject Tokens
:
import java.io.IOException; import jakarta.annotation.Priority; import jakarta.inject.Inject; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.client.ClientRequestFilter; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.ext.Provider; import io.quarkus.oidc.client.Tokens; @Provider @Priority(Priorities.AUTHENTICATION) public class OidcClientRequestCustomFilter implements ClientRequestFilter { @Inject Tokens tokens; @Override public void filter(ClientRequestContext requestContext) throws IOException { requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken()); } }
import java.io.IOException;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.ext.Provider;
import io.quarkus.oidc.client.Tokens;
@Provider
@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter implements ClientRequestFilter {
@Inject
Tokens tokens;
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken());
}
}
Copy to clipboardCopied
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
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 clipboardCopiedor
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.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 clipboardCopiedOr with the secret retrieved from a CredentialsProvider:
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/ quarkus.oidc-client.client-id=quarkus-app # This key is used to retrieve a secret from the map of credentials returned from CredentialsProvider quarkus.oidc-client.credentials.client-secret.provider.key=mysecret-key # This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc # Set it only if more than one CredentialsProvider can be registered quarkus.oidc-client.credentials.client-secret.provider.name=oidc-credentials-provider
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
# This key is used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc-client.credentials.client-secret.provider.key=mysecret-key
# This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation
quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc-client.credentials.client-secret.provider.name=oidc-credentials-provider
Copy to clipboardCopied
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
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 clipboardCopied
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
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 clipboardCopied
Or with the secret retrieved from a CredentialsProvider, signature algorithm is HS256
:
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/ quarkus.oidc-client.client-id=quarkus-app # This is a key that will be used to retrieve a secret from the map of credentials returned from CredentialsProvider quarkus.oidc-client.credentials.jwt.secret-provider.key=mysecret-key # This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc # Set it only if more than one CredentialsProvider can be registered quarkus.oidc-client.credentials.jwt.secret-provider.name=oidc-credentials-provider
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
# This is a key that will be used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc-client.credentials.jwt.secret-provider.key=mysecret-key
# This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation
quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc-client.credentials.jwt.secret-provider.name=oidc-credentials-provider
Copy to clipboardCopied
private_key_jwt
with the PEM key inlined in application.properties, and where the signature algorithm is RS256
:
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/ quarkus.oidc-client.client-id=quarkus-app quarkus.oidc-client.credentials.jwt.key=Base64-encoded private key representation
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key=Base64-encoded private key representation
Copy to clipboardCopied
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
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 clipboardCopied
private_key_jwt
with the keystore file, signature algorithm is RS256
:
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/ quarkus.oidc-client.client-id=quarkus-app quarkus.oidc-client.credentials.jwt.key-store-file=keystore.pkcs12 quarkus.oidc-client.credentials.jwt.key-store-password=mypassword quarkus.oidc-client.credentials.jwt.key-password=mykeypassword # Private key alias inside the keystore quarkus.oidc-client.credentials.jwt.key-id=mykeyAlias
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key-store-file=keystore.pkcs12
quarkus.oidc-client.credentials.jwt.key-store-password=mypassword
quarkus.oidc-client.credentials.jwt.key-password=mykeypassword
# Private key alias inside the keystore
quarkus.oidc-client.credentials.jwt.key-id=mykeyAlias
Copy to clipboardCopied
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 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. Use the RS512 signature algorithm instead of the default RS256 The token endpoint URL is the default audience value; use the base address URL instead: custom subject instead of the client ID: custom issuer instead of the client ID:
# 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 clipboardCopied1.1.12.2. JWT Bearer
RFC7523 explains how JWT Bearer tokens can be used to authenticate clients, see the Using JWTs for Client Authentication section for more information.
It can be enabled as follows:
quarkus.oidc-client.auth-server-url=${auth-server-url} quarkus.oidc-client.client-id=quarkus-app quarkus.oidc-client.credentials.jwt.source=bearer
quarkus.oidc-client.auth-server-url=${auth-server-url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.source=bearer
Copy to clipboardCopied
Next, the JWT bearer token must be provided as a client_assertion
parameter to the OIDC client.
Quarkus can load the JWT bearer token from a file system. For example, in Kubernetes, a service account token projection can be mounted to a /var/run/secrets/tokens
path. Then all you need to do is configure a JWT bearer token path as follows:
quarkus.oidc-client.credentials.jwt.token-path=/var/run/secrets/tokens 1
quarkus.oidc-client.credentials.jwt.token-path=/var/run/secrets/tokens 1
Copy to clipboardCopied- 1
- Path to a JWT bearer token. Quarkus loads a new token from a filesystem and reload it when the token has expired.
Your other option is to use OidcClient
methods for acquiring or refreshing tokens which accept additional grant parameters, for example, oidcClient.getTokens(Map.of("client_assertion", "ey…"))
.
If you work work with the OIDC client filters then you must register a custom filter which will provide this assertion.
Here is an example of the Quarkus REST (formerly RESTEasy Reactive) custom filter:
package io.quarkus.it.keycloak; import java.util.Map; import io.quarkus.oidc.client.reactive.filter.runtime.AbstractOidcClientRequestReactiveFilter; import io.quarkus.oidc.common.runtime.OidcConstants; import jakarta.annotation.Priority; import jakarta.ws.rs.Priorities; @Priority(Priorities.AUTHENTICATION) public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestReactiveFilter { @Override protected Map<String, String> additionalParameters() { return Map.of(OidcConstants.CLIENT_ASSERTION, "ey..."); } }
package io.quarkus.it.keycloak;
import java.util.Map;
import io.quarkus.oidc.client.reactive.filter.runtime.AbstractOidcClientRequestReactiveFilter;
import io.quarkus.oidc.common.runtime.OidcConstants;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestReactiveFilter {
@Override
protected Map<String, String> additionalParameters() {
return Map.of(OidcConstants.CLIENT_ASSERTION, "ey...");
}
}
Copy to clipboardCopiedHere is an example of the RESTEasy Classic custom filter:
package io.quarkus.it.keycloak; import java.util.Map; import io.quarkus.oidc.client.filter.runtime.AbstractOidcClientRequestFilter; import io.quarkus.oidc.common.runtime.OidcConstants; import jakarta.annotation.Priority; import jakarta.ws.rs.Priorities; @Priority(Priorities.AUTHENTICATION) public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestFilter { @Override protected Map<String, String> additionalParameters() { return Map.of(OidcConstants.CLIENT_ASSERTION, "ey..."); } }
package io.quarkus.it.keycloak;
import java.util.Map;
import io.quarkus.oidc.client.filter.runtime.AbstractOidcClientRequestFilter;
import io.quarkus.oidc.common.runtime.OidcConstants;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestFilter {
@Override
protected Map<String, String> additionalParameters() {
return Map.of(OidcConstants.CLIENT_ASSERTION, "ey...");
}
}
Copy to clipboardCopied1.1.12.3. 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}
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 clipboardCopied1.1.12.4. 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.tls-configuration-name=oidc-client # configure hostname verification if necessary #quarkus.tls.oidc-client.hostname-verification-algorithm=NONE # Keystore configuration quarkus.tls.oidc-client.key-store.p12.path=client-keystore.p12 quarkus.tls.oidc-client.key-store.p12.password=${key-store-password} # Add more keystore properties if needed: #quarkus.tls.oidc-client.key-store.p12.alias=keyAlias #quarkus.tls.oidc-client.key-store.p12.alias-password=keyAliasPassword # Truststore configuration quarkus.tls.oidc-client.trust-store.p12.path=client-truststore.p12 quarkus.tls.oidc-client.trust-store.p12.password=${trust-store-password} # Add more truststore properties if needed: #quarkus.tls.oidc-client.trust-store.p12.alias=certAlias
quarkus.oidc-client.tls.tls-configuration-name=oidc-client
# configure hostname verification if necessary
#quarkus.tls.oidc-client.hostname-verification-algorithm=NONE
# Keystore configuration
quarkus.tls.oidc-client.key-store.p12.path=client-keystore.p12
quarkus.tls.oidc-client.key-store.p12.password=${key-store-password}
# Add more keystore properties if needed:
#quarkus.tls.oidc-client.key-store.p12.alias=keyAlias
#quarkus.tls.oidc-client.key-store.p12.alias-password=keyAliasPassword
# Truststore configuration
quarkus.tls.oidc-client.trust-store.p12.path=client-truststore.p12
quarkus.tls.oidc-client.trust-store.p12.password=${trust-store-password}
# Add more truststore properties if needed:
#quarkus.tls.oidc-client.trust-store.p12.alias=certAlias
Copy to clipboardCopied1.1.13. OIDC Client SPI
When your custom extension must acquire OIDC tokens using one of the OIDC token grants supported by OIDC client, this extension can depend on the OIDC Client SPI only and let OIDC client itself acquire and refresh access tokens as necessary.
Add the following dependency:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-oidc-client-spi</artifactId> </dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-spi</artifactId>
</dependency>
Copy to clipboardCopied
Next update your extension to use io.quarkus.oidc.client.spi.TokenProvider
CDI bean as required, for example:
package org.acme.extension; import jakarta.inject.Inject; import io.quarkus.oidc.client.spi.TokenProvider; public class ExtensionOAuth2Support { @Inject TokenProvider tokenProvider; public Uni<String> getAccessToken() { return tokenProvider.getAccessToken(); } }
package org.acme.extension;
import jakarta.inject.Inject;
import io.quarkus.oidc.client.spi.TokenProvider;
public class ExtensionOAuth2Support {
@Inject
TokenProvider tokenProvider;
public Uni<String> getAccessToken() {
return tokenProvider.getAccessToken();
}
}
Copy to clipboardCopied
Currently, io.quarkus.oidc.client.spi.TokenProvider
is only available for default OIDC clients, since custom extensions are unlikely to be aware of multiple named OIDC clients.
1.1.14. 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>
<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 clipboardCopied1.1.14.1. Wiremock
Add the following dependencies to your test project:
<dependency> <groupId>org.wiremock</groupId> <artifactId>wiremock</artifactId> <scope>test</scope> <version>${wiremock.version}</version> 1 </dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
<version>${wiremock.version}</version> 1
</dependency>
Copy to clipboardCopied
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; } } }
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 clipboardCopied
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
# Use the 'keycloak.url' property set by the test KeycloakRealmResourceManager
quarkus.oidc-client.auth-server-url=${keycloak.url:replaced-by-test-resource}
quarkus.oidc-client.discovery-enabled=false
quarkus.oidc-client.token-path=/tokens
quarkus.oidc-client.client-id=quarkus-service-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=password
quarkus.oidc-client.grant-options.password.username=alice
quarkus.oidc-client.grant-options.password.password=alice
Copy to clipboardCopied
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.14.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.15. 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
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 clipboardCopied
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
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 clipboardCopied1.2. OIDC request filters
You can filter OIDC requests made by OIDC client to the OIDC provider by registering one or more OidcRequestFilter
implementations, which can update or add new request headers, or analyze the request body.
You can have a single filter intercepting requests to all OIDC provider endpoints, or use an @OidcEndpoint
annotation to apply this filter to requests to specific endpoints only. For example:
package io.quarkus.it.keycloak; import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; import io.quarkus.oidc.common.OidcEndpoint; import io.quarkus.oidc.common.OidcRequestFilter; import io.vertx.core.http.HttpMethod; @ApplicationScoped @OidcEndpoint(value = Type.TOKEN) @Unremovable public class OidcRequestCustomizer implements OidcRequestFilter { @Override public void filter(OidcRequestContext requestContext) { HttpMethod method = requestContext.request().method(); String uri = requestContext.request().uri(); if (method == HttpMethod.POST && uri.endsWith("/token") && requestContext.requestBody() != null) { requestContext.request().putHeader("Digest", calculateDigest(requestContext.requestBody().toString())); } } private String calculateDigest(String bodyString) { // Apply the required digest algorithm to the body string } }
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.core.http.HttpMethod;
@ApplicationScoped
@OidcEndpoint(value = Type.TOKEN)
@Unremovable
public class OidcRequestCustomizer implements OidcRequestFilter {
@Override
public void filter(OidcRequestContext requestContext) {
HttpMethod method = requestContext.request().method();
String uri = requestContext.request().uri();
if (method == HttpMethod.POST && uri.endsWith("/token") && requestContext.requestBody() != null) {
requestContext.request().putHeader("Digest", calculateDigest(requestContext.requestBody().toString()));
}
}
private String calculateDigest(String bodyString) {
// Apply the required digest algorithm to the body string
}
}
Copy to clipboardCopied
OidcRequestContextProperties
can be used to access request properties. Currently, you can use a client_id
key to access the client tenant id and a grant_type
key to access the grant type which the OIDC client uses to acquire tokens.
1.3. OIDC response filters
You can filter responses to the OIDC client requests by registering one or more OidcResponseFilter
implementations, which can check the response status, headers and body, in order to log them or perform other actions.
You can have a single filter intercepting responses to all OIDC client requests, or use an @OidcEndpoint
annotation to apply this filter to the responses to the specific OIDC client requests only. For example:
package io.quarkus.it.keycloak; import jakarta.enterprise.context.ApplicationScoped; import org.jboss.logging.Logger; import io.quarkus.arc.Unremovable; import io.quarkus.oidc.common.OidcEndpoint; import io.quarkus.oidc.common.OidcEndpoint.Type; import io.quarkus.oidc.common.OidcResponseFilter; import io.quarkus.oidc.common.runtime.OidcConstants; @ApplicationScoped @Unremovable @OidcEndpoint(value = Type.TOKEN) 1 public class TokenEndpointResponseFilter implements OidcResponseFilter { private static final Logger LOG = Logger.getLogger(TokenEndpointResponseFilter.class); @Override public void filter(OidcResponseContext rc) { String contentType = rc.responseHeaders().get("Content-Type"); 2 if (contentType.equals("application/json") && "refresh_token".equals(rc.requestProperties().get(OidcConstants.GRANT_TYPE)) 3 && rc.responseBody().toJsonObject().containsKey("refresh_token")) { 4 LOG.debug("Tokens have been refreshed"); } } }
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcResponseFilter;
import io.quarkus.oidc.common.runtime.OidcConstants;
@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.TOKEN) 1
public class TokenEndpointResponseFilter implements OidcResponseFilter {
private static final Logger LOG = Logger.getLogger(TokenEndpointResponseFilter.class);
@Override
public void filter(OidcResponseContext rc) {
String contentType = rc.responseHeaders().get("Content-Type"); 2
if (contentType.equals("application/json")
&& "refresh_token".equals(rc.requestProperties().get(OidcConstants.GRANT_TYPE)) 3
&& rc.responseBody().toJsonObject().containsKey("refresh_token")) { 4
LOG.debug("Tokens have been refreshed");
}
}
}
Copy to clipboardCopied1.4. Token Propagation for Quarkus REST
The quarkus-rest-client-oidc-token-propagation
extension provides a REST Client filter, io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter
, that simplifies the propagation of authentication information. This 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.common.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.common.AccessToken; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @AccessToken @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@AccessToken
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Copy to clipboardCopiedor
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @RegisterProvider(AccessTokenRequestReactiveFilter.class) @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(AccessTokenRequestReactiveFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Copy to clipboardCopied
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.resteasy-client-oidc-token-propagation.exchange-token=true 1
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=exchange
quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange
quarkus.resteasy-client-oidc-token-propagation.exchange-token=true 1
Copy to clipboardCopied- 1
- Please note that the
exchange-token
configuration property is ignored when the OidcClient name is set with theio.quarkus.oidc.token.propagation.common.AccessToken#exchangeTokenClient
annotation attribute.
AccessTokenRequestReactiveFilter
will use OidcClient
to exchange the current token, and you can use quarkus.oidc-client.grant-options.exchange
to set the additional exchange properties expected by your OpenID Connect Provider.
If you work with providers such as Azure
that require using JWT bearer token grant to exchange the current token, then you can configure AccessTokenRequestReactiveFilter
to exchange the token like this:
quarkus.oidc-client.auth-server-url=${azure.provider.url} quarkus.oidc-client.client-id=quarkus-app quarkus.oidc-client.credentials.secret=secret quarkus.oidc-client.grant.type=jwt quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access quarkus.resteasy-client-oidc-token-propagation.exchange-token=true
quarkus.oidc-client.auth-server-url=${azure.provider.url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=jwt
quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of
quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access
quarkus.resteasy-client-oidc-token-propagation.exchange-token=true
Copy to clipboardCopied
AccessTokenRequestReactiveFilter
uses a default OidcClient
by default. A named OidcClient
can be selected with a quarkus.rest-client-oidc-token-propagation.client-name
configuration property or with the io.quarkus.oidc.token.propagation.common.AccessToken#exchangeTokenClient
annotation attribute.
1.5. Token Propagation for RESTEasy Classic
The quarkus-resteasy-client-oidc-token-propagation
extension provides two Jakarta REST jakarta.ws.rs.client.ClientRequestFilter
class implementations that simplify the propagation of authentication information. io.quarkus.oidc.token.propagation.AccessTokenRequestFilter
propagates the Bearer token present in the current active request or the token acquired from the Authorization code flow mechanism, as the HTTP Authorization
header’s Bearer
scheme value. The io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter
provides the same functionality 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.5.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.common.AccessToken
or org.eclipse.microprofile.rest.client.annotation.RegisterProvider
, for example:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.token.propagation.common.AccessToken; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @AccessToken @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@AccessToken
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Copy to clipboardCopiedor
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @RegisterProvider(AccessTokenRequestFilter.class) @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(AccessTokenRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Copy to clipboardCopied
Alternatively, AccessTokenRequestFilter
can be registered automatically with all MP Rest or Jakarta REST clients if the quarkus.resteasy-client-oidc-token-propagation.register-filter
property is set to true
and quarkus.resteasy-client-oidc-token-propagation.json-web-token
property is set to false
(which is a default value).
1.5.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.resteasy-client-oidc-token-propagation.exchange-token=true
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=exchange
quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange
quarkus.resteasy-client-oidc-token-propagation.exchange-token=true
Copy to clipboardCopied
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.resteasy-client-oidc-token-propagation.exchange-token=true
quarkus.oidc-client.auth-server-url=${azure.provider.url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=jwt
quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of
quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access
quarkus.resteasy-client-oidc-token-propagation.exchange-token=true
Copy to clipboardCopied
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.resteasy-client-oidc-token-propagation.client-name
configuration property.
1.5.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 which should be provisioned from a secure file system or remote secure storage such as Vault.
You can selectively register JsonWebTokenRequestFilter
by using either io.quarkus.oidc.token.propagation.JsonWebToken
or org.eclipse.microprofile.rest.client.annotation.RegisterProvider
, for example:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.token.propagation.JsonWebToken; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @JsonWebToken @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@JsonWebToken
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Copy to clipboardCopiedor
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @RegisterRestClient @RegisterProvider(JsonWebTokenRequestFilter.class) @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(JsonWebTokenRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
Copy to clipboardCopied
Alternatively, JsonWebTokenRequestFilter
can be registered automatically with all MicroProfile REST or Jakarta REST clients if both quarkus.resteasy-client-oidc-token-propagation.register-filter
and quarkus.resteasy-client-oidc-token-propagation.json-web-token
properties are set to true
.
1.5.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.resteasy-client-oidc-token-propagation.secure-json-web-token=true smallrye.jwt.sign.key.location=/privateKey.pem # Set a new issuer smallrye.jwt.new-token.issuer=http://frontend-resource # Set a new audience smallrye.jwt.new-token.audience=http://downstream-resource # Override the existing token issuer and audience claims if they are already set smallrye.jwt.new-token.override-matching-claims=true
quarkus.resteasy-client-oidc-token-propagation.secure-json-web-token=true
smallrye.jwt.sign.key.location=/privateKey.pem
# Set a new issuer
smallrye.jwt.new-token.issuer=http://frontend-resource
# Set a new audience
smallrye.jwt.new-token.audience=http://downstream-resource
# Override the existing token issuer and audience claims if they are already set
smallrye.jwt.new-token.override-matching-claims=true
Copy to clipboardCopied
As mentioned, use AccessTokenRequestFilter
if you work with Keycloak or an OpenID Connect Provider that supports a Token Exchange protocol.
1.5.3. Testing
Typically, you must prepare two REST test endpoints. The first endpoint uses the injected MP REST client with a registered token propagation filter to call the second endpoint.
To learn how it can be done, please follow the OpenID Connect client and token propagation quickstart, and its Testing section in particular.
1.6. Configuration reference
1.6.1. OIDC client
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property | Type | Default |
If the OIDC client extension is enabled.
Environment variable: | boolean |
|
The base URL of the OpenID Connect (OIDC) server, for example,
Environment variable: | string | |
Discovery of the OIDC endpoints. If not enabled, you must configure the OIDC endpoint URLs individually.
Environment variable: | boolean |
|
The relative path or absolute URL of the OIDC dynamic client registration endpoint. Set if
Environment variable: | string | |
The duration to attempt the initial connection to an OIDC server. For example, setting the duration to
Environment variable: | ||
The number of times to retry re-establishing an existing OIDC connection if it is temporarily lost. Different from
Environment variable: | int |
|
The number of seconds after which the current OIDC connection request times out.
Environment variable: |
| |
Whether DNS lookup should be performed on the worker thread. Use this option when you can see logged warnings about blocked Vert.x event loop by HTTP requests to OIDC server.
Environment variable: | boolean |
|
The maximum size of the connection pool used by the WebClient.
Environment variable: | int | |
Follow redirects automatically when WebClient gets HTTP 302. When this property is disabled only a single redirect to exactly the same original URI is allowed but only if one or more cookies were set during the redirect request.
Environment variable: | boolean |
|
The OIDC token endpoint that issues access and refresh tokens; specified as a relative path or absolute URL. Set if
Environment variable: | string | |
The relative path or absolute URL of the OIDC token revocation endpoint.
Environment variable: | string | |
The client id of the application. Each application has a client id that is used to identify the application. Setting the client id is not required if
Environment variable: | string | |
The client name of the application. It is meant to represent a human readable description of the application which you may provide when an application (client) is registered in an OpenId Connect provider’s dashboard. For example, you can set this property to have more informative log messages which record an activity of the given client.
Environment variable: | string | |
A unique OIDC client identifier. It must be set when OIDC clients are created dynamically and is optional in all other cases.
Environment variable: | string | |
If this client configuration is enabled.
Environment variable: | boolean |
|
List of access token scopes
Environment variable: | list of string | |
Refresh token time skew. If this property is enabled then the configured duration is converted to seconds and is added to the current time when checking whether the access token should be refreshed. If the sum is greater than this access token’s expiration time then a refresh is going to happen.
Environment variable: | ||
Access token expiration period relative to the current time. This property is only checked when an access token grant response does not include an access token expiration property.
Environment variable: | ||
Access token expiry time skew that can be added to the calculated token expiry time.
Environment variable: | ||
If the access token 'expires_in' property should be checked as an absolute time value as opposed to a duration relative to the current time.
Environment variable: | boolean |
|
Grant type
Environment variable: | client: 'client_credentials' grant requiring an OIDC client authentication only password: 'password' grant requiring both OIDC client and user ('username' and 'password') authentications code: 'authorization_code' grant requiring an OIDC client authentication as well as at least 'code' and 'redirect_uri' parameters which must be passed to OidcClient at the token request time. exchange: 'urn:ietf:params:oauth:grant-type:token-exchange' grant requiring an OIDC client authentication as well as at least 'subject_token' parameter which must be passed to OidcClient at the token request time. jwt: 'urn:ietf:params:oauth:grant-type:jwt-bearer' grant requiring an OIDC client authentication as well as at least an 'assertion' parameter which must be passed to OidcClient at the token request time.
refresh: 'refresh_token' grant requiring an OIDC client authentication and a refresh token. Note, OidcClient supports this grant by default if an access token acquisition response contained a refresh token. However, in some cases, the refresh token is provided out of band, for example, it can be shared between several of the confidential client’s services, etc. If 'quarkus.oidc-client.grant-type' is set to 'refresh' then ciba: 'urn:openid:params:grant-type:ciba' grant requiring an OIDC client authentication as well as 'auth_req_id' parameter which must be passed to OidcClient at the token request time. device: 'urn:ietf:params:oauth:grant-type:device_code' grant requiring an OIDC client authentication as well as 'device_code' parameter which must be passed to OidcClient at the token request time. | client |
Access token property name in a token grant response
Environment variable: | string |
|
Refresh token property name in a token grant response
Environment variable: | string |
|
Access token expiry property name in a token grant response
Environment variable: | string |
|
Refresh token expiry property name in a token grant response
Environment variable: | string |
|
Grant options
Environment variable: | Map<String,Map<String,String>> | |
Requires that all filters which use 'OidcClient' acquire the tokens at the post-construct initialization time, possibly long before these tokens are used. This property should be disabled if the access token may expire before it is used for the first time and no refresh token is available.
Environment variable: | boolean |
|
Custom HTTP headers which have to be sent to the token endpoint
Environment variable: | Map<String,String> | |
Type | Default | |
The host name or IP address of the Proxy.
Environment variable: | string | |
The port number of the Proxy. The default value is
Environment variable: | int |
|
The username, if the Proxy needs authentication.
Environment variable: | string | |
The password, if the Proxy needs authentication.
Environment variable: | string | |
Type | Default | |
The name of the TLS configuration to use.
If a name is configured, it uses the configuration from The default TLS configuration is not used by default.
Environment variable: | string | |
Different authentication options for OIDC client to access OIDC token and other secured endpoints | Type | Default |
The client secret used by the
Environment variable: | string | |
The client secret value. This value is ignored if
Environment variable: | string | |
The CredentialsProvider bean name, which should only be set if more than one CredentialsProvider is registered
Environment variable: | string | |
The CredentialsProvider keyring name. The keyring name is only required when the CredentialsProvider being used requires the keyring name to look up the secret, which is often the case when a CredentialsProvider is shared by multiple extensions to retrieve credentials from a more dynamic source like a vault instance or secret manager
Environment variable: | string | |
The CredentialsProvider client secret key
Environment variable: | string | |
The authentication method. If the
Environment variable: |
basic:
post:
post-jwt: query: client id and secret are submitted as HTTP query parameters. This option is only supported by the OIDC extension. | |
JWT token source: OIDC provider client or an existing JWT bearer token.
Environment variable: |
client: JWT token is generated by the OIDC provider client to support bearer: JWT bearer token is used as a client assertion: https://www.rfc-editor.org/rfc/rfc7523#section-2.2. | client |
Path to a file with a JWT bearer token that should be used as a client assertion. This path can only be set when JWT source (
Environment variable: | path | |
If provided, indicates that JWT is signed using a secret key. It is mutually exclusive with
Environment variable: | string | |
The CredentialsProvider bean name, which should only be set if more than one CredentialsProvider is registered
Environment variable: | string | |
The CredentialsProvider keyring name. The keyring name is only required when the CredentialsProvider being used requires the keyring name to look up the secret, which is often the case when a CredentialsProvider is shared by multiple extensions to retrieve credentials from a more dynamic source like a vault instance or secret manager
Environment variable: | string | |
The CredentialsProvider client secret key
Environment variable: | string | |
String representation of a private key. If provided, indicates that JWT is signed using a private key in PEM or JWK format. It is mutually exclusive with
Environment variable: | string | |
If provided, indicates that JWT is signed using a private key in PEM or JWK format. It is mutually exclusive with
Environment variable: | string | |
If provided, indicates that JWT is signed using a private key from a keystore. It is mutually exclusive with
Environment variable: | string | |
A parameter to specify the password of the keystore file.
Environment variable: | string | |
The private key id or alias.
Environment variable: | string | |
The private key password.
Environment variable: | string | |
The JWT audience (
Environment variable: | string | |
The key identifier of the signing key added as a JWT
Environment variable: | string | |
The issuer of the signing key added as a JWT
Environment variable: | string | |
Subject of the signing key added as a JWT
Environment variable: | string | |
Additional claims.
Environment variable: | Map<String,String> | |
The signature algorithm used for the
Environment variable: | string | |
The JWT lifespan in seconds. This value is added to the time at which the JWT was issued to calculate the expiration time.
Environment variable: | int |
|
If true then the client authentication token is a JWT bearer grant assertion. Instead of producing 'client_assertion' and 'client_assertion_type' form properties, only 'assertion' is produced. This option is only supported by the OIDC client extension.
Environment variable: | boolean |
|
To write duration values, use the standard java.time.Duration
format. See the Duration#parse() Java API documentation for more information.
You can also use a simplified format, starting with a number:
- If the value is only a number, it represents time in seconds.
-
If the value is a number followed by
ms
, it represents time in milliseconds.
In other cases, the simplified format is translated to the java.time.Duration
format for parsing:
-
If the value is a number followed by
h
,m
, ors
, it is prefixed withPT
. -
If the value is a number followed by
d
, it is prefixed withP
.
1.6.2. OIDC token propagation
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property | Type | Default |
If the OIDC Token Reactive Propagation is enabled.
Environment variable: | boolean |
|
Whether the token propagation is enabled during the
For example, you may need to use a REST client from Note, this feature relies on a duplicated context. More information about Vert.x duplicated context can be found in this guide.
Environment variable: | boolean |
|
Exchange the current token with OpenId Connect Provider for a new token using either "urn:ietf:params:oauth:grant-type:token-exchange" or "urn:ietf:params:oauth:grant-type:jwt-bearer" token grant before propagating it.
Environment variable: | boolean |
|
Name of the configured OidcClient. Note this property is only used if the
Environment variable: | string |
1.7. 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.8.6 or later
- 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 three methods to propagate access tokens to ProtectedResource
:
- It can get a token by using an OIDC client filter before propagating it.
-
It can get a token by using a programmatically created OIDC client and propagate it by passing it to a REST client method as an HTTP
Authorization
header value. - It can use an OIDC token propagation filter to propagate the incoming access token.
FrontendResource
has eight endpoints:
-
/frontend/user-name-with-oidc-client-token
-
/frontend/admin-name-with-oidc-client-token
-
/frontend/user-name-with-oidc-client-token-header-param
-
/frontend/admin-name-with-oidc-client-token-header-param
-
/frontend/user-name-with-oidc-client-token-header-param-blocking
-
/frontend/admin-name-with-oidc-client-token-header-param-blocking
-
/frontend/user-name-with-propagated-token
-
/frontend/admin-name-with-propagated-token
When either /frontend/user-name-with-oidc-client-token
or /frontend/admin-name-with-oidc-client-token
endpoint is called, FrontendResource
uses a REST client with an OIDC client filter to get and propagate an access token to ProtectedResource
. When either /frontend/user-name-with-oidc-client-token-header-param
or /frontend/admin-name-with-oidc-client-token-header-param
endpoint is called, FrontendResource
uses a programmatically created OIDC client to get and propagate an access token to ProtectedResource
by passing it to a REST client method as an HTTP Authorization
header value. When either /frontend/user-name-with-propagated-token
or /frontend/admin-name-with-propagated-token
endpoint is called, FrontendResource
uses a REST client with OIDC Token Propagation Filter
to propagate the current incoming access token to ProtectedResource
.
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.20
, 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,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest' \ --no-code cd security-openid-connect-client-quickstart
Copy to clipboardCopiedquarkus create app org.acme:security-openid-connect-client-quickstart \ --extension='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest' \ --no-code cd security-openid-connect-client-quickstart
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 com.redhat.quarkus.platform:quarkus-maven-plugin:3.20.1:create \ -DprojectGroupId=org.acme \ -DprojectArtifactId=security-openid-connect-client-quickstart \ -Dextensions='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest' \ -DnoCode cd security-openid-connect-client-quickstart
Copy to clipboardCopiedmvn com.redhat.quarkus.platform:quarkus-maven-plugin:3.20.1:create \ -DprojectGroupId=org.acme \ -DprojectArtifactId=security-openid-connect-client-quickstart \ -Dextensions='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest' \ -DnoCode cd security-openid-connect-client-quickstart
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"
It generates a Maven project, importing the oidc
, rest-client-oidc-filter
, rest-client-oidc-token-propagation
, and rest
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,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest
Copy to clipboardCopiedquarkus extension add oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest
Using Maven:
./mvnw quarkus:add-extension -Dextensions='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest'
Copy to clipboardCopied./mvnw quarkus:add-extension -Dextensions='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest'
Using Gradle:
./gradlew addExtension --extensions='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest'
Copy to clipboardCopied./gradlew addExtension --extensions='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest'
It 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-rest-client-oidc-filter</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-rest-client-oidc-token-propagation</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-rest</artifactId> </dependency>
Copy to clipboardCopied<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-oidc</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-rest-client-oidc-filter</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-rest-client-oidc-token-propagation</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-rest</artifactId> </dependency>
Using Gradle:
implementation("io.quarkus:quarkus-oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest")
Copy to clipboardCopiedimplementation("io.quarkus:quarkus-oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest")
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()); } }
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 clipboardCopied
ProtectedResource
returns a name from both userName()
and adminName()
methods. The name is extracted from the current JsonWebToken
.
Next, add the following REST clients:
-
RestClientWithOidcClientFilter
, which uses an OIDC client filter provided by thequarkus-rest-client-oidc-filter
extension to get and propagate an access token. -
RestClientWithTokenHeaderParam
, which accepts a token already acquired by the programmatically created OidcClient as an HTTPAuthorization
header value. -
RestClientWithTokenPropagationFilter
, which uses an OIDC token propagation filter provided by thequarkus-rest-client-oidc-token-propagation
extension to get and propagate an access token.
Add the RestClientWithOidcClientFilter
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.inject.RegisterRestClient; import io.quarkus.oidc.client.filter.OidcClientFilter; import io.smallrye.mutiny.Uni; @RegisterRestClient @OidcClientFilter 1 @Path("/") public interface RestClientWithOidcClientFilter { @GET @Produces("text/plain") @Path("userName") Uni<String> getUserName(); @GET @Produces("text/plain") @Path("adminName") Uni<String> getAdminName(); }
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.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
@RegisterRestClient
@OidcClientFilter 1
@Path("/")
public interface RestClientWithOidcClientFilter {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName();
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName();
}
Copy to clipboardCopied- 1
- Register an OIDC client filter with the REST client to get and propagate the tokens.
Add the RestClientWithTokenHeaderParam
REST client:
package org.acme.security.openid.connect.client; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.smallrye.mutiny.Uni; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @RegisterRestClient @Path("/") public interface RestClientWithTokenHeaderParam { @GET @Produces("text/plain") @Path("userName") Uni<String> getUserName(@HeaderParam("Authorization") String authorization); 1 @GET @Produces("text/plain") @Path("adminName") Uni<String> getAdminName(@HeaderParam("Authorization") String authorization); 2 }
package org.acme.security.openid.connect.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
@RegisterRestClient
@Path("/")
public interface RestClientWithTokenHeaderParam {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName(@HeaderParam("Authorization") String authorization); 1
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName(@HeaderParam("Authorization") String authorization); 2
}
Copy to clipboardCopied
Add the RestClientWithTokenPropagationFilter
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.inject.RegisterRestClient; import io.quarkus.oidc.token.propagation.common.AccessToken; import io.smallrye.mutiny.Uni; @RegisterRestClient @AccessToken 1 @Path("/") public interface RestClientWithTokenPropagationFilter { @GET @Produces("text/plain") @Path("userName") Uni<String> getUserName(); @GET @Produces("text/plain") @Path("adminName") Uni<String> getAdminName(); }
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.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import io.smallrye.mutiny.Uni;
@RegisterRestClient
@AccessToken 1
@Path("/")
public interface RestClientWithTokenPropagationFilter {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName();
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName();
}
Copy to clipboardCopied- 1
- Register an OIDC token propagation filter with the REST client to propagate the incoming already-existing tokens.
Do not use the RestClientWithOidcClientFilter
and RestClientWithTokenPropagationFilter
interfaces in the same REST client because they can conflict, leading to issues. For example, the OIDC client filter can override the token from the OIDC token propagation filter, or the propagation filter might not work correctly if it attempts to propagate a token when none is available, expecting the OIDC client filter to obtain a new token instead.
Also, add OidcClientCreator
to create an OIDC client programmatically at startup. OidcClientCreator
supports RestClientWithTokenHeaderParam
REST client calls:
package org.acme.security.openid.connect.client; import java.util.Map; import org.eclipse.microprofile.config.inject.ConfigProperty; import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.OidcClients; import io.quarkus.oidc.client.runtime.OidcClientConfig; import io.quarkus.oidc.client.runtime.OidcClientConfig.Grant.Type; import io.quarkus.runtime.StartupEvent; import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; @ApplicationScoped public class OidcClientCreator { @Inject OidcClients oidcClients; 1 @ConfigProperty(name = "quarkus.oidc.auth-server-url") String oidcProviderAddress; private volatile OidcClient oidcClient; public void startup(@Observes StartupEvent event) { createOidcClient().subscribe().with(client -> {oidcClient = client;}); } public OidcClient getOidcClient() { return oidcClient; } private Uni<OidcClient> createOidcClient() { OidcClientConfig cfg = OidcClientConfig .authServerUrl(oidcProviderAddress) .id("myclient") .clientId("backend-service") .credentials("secret") .grant(Type.PASSWORD) .grantOptions("password", Map.of("username", "alice", "password", "alice")) .build(); return oidcClients.newClient(cfg); } }
package org.acme.security.openid.connect.client;
import java.util.Map;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.oidc.client.runtime.OidcClientConfig;
import io.quarkus.oidc.client.runtime.OidcClientConfig.Grant.Type;
import io.quarkus.runtime.StartupEvent;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
@ApplicationScoped
public class OidcClientCreator {
@Inject
OidcClients oidcClients; 1
@ConfigProperty(name = "quarkus.oidc.auth-server-url")
String oidcProviderAddress;
private volatile OidcClient oidcClient;
public void startup(@Observes StartupEvent event) {
createOidcClient().subscribe().with(client -> {oidcClient = client;});
}
public OidcClient getOidcClient() {
return oidcClient;
}
private Uni<OidcClient> createOidcClient() {
OidcClientConfig cfg = OidcClientConfig
.authServerUrl(oidcProviderAddress)
.id("myclient")
.clientId("backend-service")
.credentials("secret")
.grant(Type.PASSWORD)
.grantOptions("password", Map.of("username", "alice", "password", "alice"))
.build();
return oidcClients.newClient(cfg);
}
}
Copy to clipboardCopied- 1
OidcClients
can be used to retrieve the already initialized, named OIDC clients and create new OIDC clients on demand.
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 io.quarkus.oidc.client.Tokens; import io.quarkus.oidc.client.runtime.TokensHelper; import org.eclipse.microprofile.rest.client.inject.RestClient; import io.smallrye.mutiny.Uni; @Path("/frontend") public class FrontendResource { @Inject @RestClient RestClientWithOidcClientFilter restClientWithOidcClientFilter; 1 @Inject @RestClient RestClientWithTokenPropagationFilter restClientWithTokenPropagationFilter; 2 @Inject OidcClientCreator oidcClientCreator; TokensHelper tokenHelper = new TokensHelper(); 3 @Inject @RestClient RestClientWithTokenHeaderParam restClientWithTokenHeaderParam; 4 @GET @Path("user-name-with-oidc-client-token") @Produces("text/plain") public Uni<String> getUserNameWithOidcClientToken() { 5 return restClientWithOidcClientFilter.getUserName(); } @GET @Path("admin-name-with-oidc-client-token") @Produces("text/plain") public Uni<String> getAdminNameWithOidcClientToken() { 6 return restClientWithOidcClientFilter.getAdminName(); } @GET @Path("user-name-with-propagated-token") @Produces("text/plain") public Uni<String> getUserNameWithPropagatedToken() { 7 return restClientWithTokenPropagationFilter.getUserName(); } @GET @Path("admin-name-with-propagated-token") @Produces("text/plain") public Uni<String> getAdminNameWithPropagatedToken() { 8 return restClientWithTokenPropagationFilter.getAdminName(); } @GET @Path("user-name-with-oidc-client-token-header-param") @Produces("text/plain") public Uni<String> getUserNameWithOidcClientTokenHeaderParam() { 9 return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem() .transformToUni(tokens -> restClientWithTokenHeaderParam.getUserName("Bearer " + tokens.getAccessToken())); } @GET @Path("admin-name-with-oidc-client-token-header-param") @Produces("text/plain") public Uni<String> getAdminNameWithOidcClientTokenHeaderParam() { 10 return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem() .transformToUni(tokens -> restClientWithTokenHeaderParam.getAdminName("Bearer " + tokens.getAccessToken())); } @GET @Path("user-name-with-oidc-client-token-header-param-blocking") @Produces("text/plain") public String getUserNameWithOidcClientTokenHeaderParamBlocking() { 11 Tokens tokens = tokenHelper.getTokens(oidcClientCreator.getOidcClient()).await().indefinitely(); return restClientWithTokenHeaderParam.getUserName("Bearer " + tokens.getAccessToken()).await().indefinitely(); } @GET @Path("admin-name-with-oidc-client-token-header-param-blocking") @Produces("text/plain") public String getAdminNameWithOidcClientTokenHeaderParamBlocking() { 12 Tokens tokens = tokenHelper.getTokens(oidcClientCreator.getOidcClient()).await().indefinitely(); return restClientWithTokenHeaderParam.getAdminName("Bearer " + tokens.getAccessToken()).await().indefinitely(); } }
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 io.quarkus.oidc.client.Tokens;
import io.quarkus.oidc.client.runtime.TokensHelper;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import io.smallrye.mutiny.Uni;
@Path("/frontend")
public class FrontendResource {
@Inject
@RestClient
RestClientWithOidcClientFilter restClientWithOidcClientFilter; 1
@Inject
@RestClient
RestClientWithTokenPropagationFilter restClientWithTokenPropagationFilter; 2
@Inject
OidcClientCreator oidcClientCreator;
TokensHelper tokenHelper = new TokensHelper(); 3
@Inject
@RestClient
RestClientWithTokenHeaderParam restClientWithTokenHeaderParam; 4
@GET
@Path("user-name-with-oidc-client-token")
@Produces("text/plain")
public Uni<String> getUserNameWithOidcClientToken() { 5
return restClientWithOidcClientFilter.getUserName();
}
@GET
@Path("admin-name-with-oidc-client-token")
@Produces("text/plain")
public Uni<String> getAdminNameWithOidcClientToken() { 6
return restClientWithOidcClientFilter.getAdminName();
}
@GET
@Path("user-name-with-propagated-token")
@Produces("text/plain")
public Uni<String> getUserNameWithPropagatedToken() { 7
return restClientWithTokenPropagationFilter.getUserName();
}
@GET
@Path("admin-name-with-propagated-token")
@Produces("text/plain")
public Uni<String> getAdminNameWithPropagatedToken() { 8
return restClientWithTokenPropagationFilter.getAdminName();
}
@GET
@Path("user-name-with-oidc-client-token-header-param")
@Produces("text/plain")
public Uni<String> getUserNameWithOidcClientTokenHeaderParam() { 9
return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem()
.transformToUni(tokens -> restClientWithTokenHeaderParam.getUserName("Bearer " + tokens.getAccessToken()));
}
@GET
@Path("admin-name-with-oidc-client-token-header-param")
@Produces("text/plain")
public Uni<String> getAdminNameWithOidcClientTokenHeaderParam() { 10
return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem()
.transformToUni(tokens -> restClientWithTokenHeaderParam.getAdminName("Bearer " + tokens.getAccessToken()));
}
@GET
@Path("user-name-with-oidc-client-token-header-param-blocking")
@Produces("text/plain")
public String getUserNameWithOidcClientTokenHeaderParamBlocking() { 11
Tokens tokens = tokenHelper.getTokens(oidcClientCreator.getOidcClient()).await().indefinitely();
return restClientWithTokenHeaderParam.getUserName("Bearer " + tokens.getAccessToken()).await().indefinitely();
}
@GET
@Path("admin-name-with-oidc-client-token-header-param-blocking")
@Produces("text/plain")
public String getAdminNameWithOidcClientTokenHeaderParamBlocking() { 12
Tokens tokens = tokenHelper.getTokens(oidcClientCreator.getOidcClient()).await().indefinitely();
return restClientWithTokenHeaderParam.getAdminName("Bearer " + tokens.getAccessToken()).await().indefinitely();
}
}
Copy to clipboardCopied- 1 5 6
FrontendResource
uses the injectedRestClientWithOidcClientFilter
REST client with the OIDC client filter to get and propagate an access token toProtectedResource
when either/frontend/user-name-with-oidc-client-token
or/frontend/admin-name-with-oidc-client-token
is called.- 2 7 8
FrontendResource
uses the injectedRestClientWithTokenPropagationFilter
REST client with the OIDC token propagation filter to propagate the current incoming access token toProtectedResource
when either/frontend/user-name-with-propagated-token
or/frontend/admin-name-with-propagated-token
is called.- 4 9 10
FrontendResource
uses the programmatically created OIDC client to get and propagate an access token toProtectedResource
by passing it directly to the injectedRestClientWithTokenHeaderParam
REST client’s method as an HTTPAuthorization
header value, when either/frontend/user-name-with-oidc-client-token-header-param
or/frontend/admin-name-with-oidc-client-token-header-param
is called.- 11 12
- Sometimes, one may have to acquire tokens in a blocking manner before propagating them with the REST client. This example shows how to acquire the tokens in such cases.
- 3
io.quarkus.oidc.client.runtime.TokensHelper
is a useful tool when OIDC client is used directly, without the OIDC client filter. To useTokensHelper
, pass OIDC Client to it to get the tokens andTokensHelper
acquires the tokens and refreshes them if necessary in a thread-safe way.
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(); } }
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 clipboardCopied
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, Quarkus REST (formerly 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 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. Configure OIDC Client Configure REST clients
# 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.RestClientWithTokenHeaderParam/mp-rest/url=http://localhost:${port}/protected
org.acme.security.openid.connect.client.RestClientWithTokenPropagationFilter/mp-rest/url=http://localhost:${port}/protected
Copy to clipboardCopied
The preceding 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.
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
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 KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev
docker run --name keycloak -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev
Copy to clipboardCopied
Set {keycloak.version}
to 26.1.3
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 clipboardCopiedquarkus dev
Using Maven:
./mvnw quarkus:dev
Copy to clipboardCopied./mvnw quarkus:dev
Using Gradle:
./gradlew --console=plain quarkusDev
Copy to clipboardCopied./gradlew --console=plain quarkusDev
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 Keycloak provider
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
admin
, with the password,admin
. This user has bothadmin
anduser
roles.-
Access
/frontend/user-name-with-propagated-token
, which returns200
. -
Access
/frontend/admin-name-with-propagated-token
, which returns200
.
-
Access
Log out and back in as
alice
with the password,alice
. This user has auser
role.-
Access
/frontend/user-name-with-propagated-token
, which returns200
. -
Access
/frontend/admin-name-with-propagated-token
, which returns403
.
-
Access
You have tested 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 clipboardCopiedquarkus build
Using Maven:
./mvnw install
Copy to clipboardCopied./mvnw install
Using Gradle:
./gradlew build
Copy to clipboardCopied./gradlew build
Then, run it:
java -jar target/quarkus-app/quarkus-run.jar
java -jar target/quarkus-app/quarkus-run.jar
Copy to clipboardCopied2.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 clipboardCopiedquarkus build --native
Using Maven:
./mvnw install -Dnative
Copy to clipboardCopied./mvnw install -Dnative
Using Gradle:
./gradlew build -Dquarkus.native.enabled=true
Copy to clipboardCopied./gradlew build -Dquarkus.native.enabled=true
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
./target/security-openid-connect-quickstart-1.0.0-SNAPSHOT-runner
Copy to clipboardCopied2.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' \ )
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 clipboardCopied
Use this token to call /frontend/user-name-with-propagated-token
. This command returns the 200
status code and the name alice
:
curl -i -X GET \ http://localhost:8080/frontend/user-name-with-propagated-token \ -H "Authorization: Bearer "$access_token
curl -i -X GET \
http://localhost:8080/frontend/user-name-with-propagated-token \
-H "Authorization: Bearer "$access_token
Copy to clipboardCopied
Use the same token to call /frontend/admin-name-with-propagated-token
. In contrast to the preceding command, this command returns 403
because alice
has only a user
role:
curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-propagated-token \ -H "Authorization: Bearer "$access_token
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-propagated-token \
-H "Authorization: Bearer "$access_token
Copy to clipboardCopied
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' \ )
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 clipboardCopied
Use this token to call /frontend/user-name-with-propagated-token
. This command returns a 200
status code and the name admin
:
curl -i -X GET \ http://localhost:8080/frontend/user-name-with-propagated-token \ -H "Authorization: Bearer "$access_token
curl -i -X GET \
http://localhost:8080/frontend/user-name-with-propagated-token \
-H "Authorization: Bearer "$access_token
Copy to clipboardCopied
Use the same token to call /frontend/admin-name-with-propagated-token
. This command also returns the 200
status code and the name admin
because admin
has both user
and admin
roles:
curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-propagated-token \ -H "Authorization: Bearer "$access_token
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-propagated-token \
-H "Authorization: Bearer "$access_token
Copy to clipboardCopied
Next, 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.
curl -i -X GET \ http://localhost:8080/frontend/user-name-with-oidc-client-token
curl -i -X GET \
http://localhost:8080/frontend/user-name-with-oidc-client-token
Copy to clipboardCopied
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
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-oidc-client-token
Copy to clipboardCopied
In contrast with the preceding command, this command returns a 403
status code.
Next, test that the programmatically created OIDC client correctly acquires and propagates the token with RestClientWithTokenHeaderParam
both in reactive and imperative (blocking) modes.
Call the /user-name-with-oidc-client-token-header-param
. This command returns the 200
status code and the name alice
:
curl -i -X GET \ http://localhost:8080/frontend/user-name-with-oidc-client-token-header-param
curl -i -X GET \
http://localhost:8080/frontend/user-name-with-oidc-client-token-header-param
Copy to clipboardCopied
Call the /admin-name-with-oidc-client-token-header-param
. In contrast with the preceding command, this command returns a 403
status code:
curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-oidc-client-token-header-param
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-oidc-client-token-header-param
Copy to clipboardCopiedNext, test the endpoints which use OIDC client in in the blocking mode.
Call the /user-name-with-oidc-client-token-header-param-blocking
. This command returns the 200
status code and the name alice
:
curl -i -X GET \ http://localhost:8080/frontend/user-name-with-oidc-client-token-header-param-blocking
curl -i -X GET \
http://localhost:8080/frontend/user-name-with-oidc-client-token-header-param-blocking
Copy to clipboardCopied
Call the /admin-name-with-oidc-client-token-header-param-blocking
. In contrast with the preceding command, this command returns a 403
status code:
curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-oidc-client-token-header-param-blocking
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-oidc-client-token-header-param-blocking
Copy to clipboardCopied