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.
Requisitos previos
To complete this guide, you need:
-
Roughly 15 minutes
-
An IDE
-
JDK 17+ installed with
JAVA_HOMEconfigured appropriately -
Apache Maven 3.9.15
-
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)
Arquitectura
In this example, an application is built with two Jakarta REST resources, FrontendResource and ProtectedResource.
-
FrontendResourceuses one of three methods to propagate access tokens toProtectedResource:-
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
Authorizationheader value. -
It can use an OIDC token propagation filter to propagate the incoming access token.
-
-
FrontendResourcehas 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-tokenor/frontend/admin-name-with-oidc-client-tokenendpoint is called,FrontendResourceuses a REST client with an OIDC client filter to get and propagate an access token toProtectedResource. -
When either
/frontend/user-name-with-oidc-client-token-header-paramor/frontend/admin-name-with-oidc-client-token-header-paramendpoint is called,FrontendResourceuses a programmatically created OIDC client to get and propagate an access token toProtectedResourceby passing it to a REST client method as an HTTPAuthorizationheader value. -
When either
/frontend/user-name-with-propagated-tokenor/frontend/admin-name-with-propagated-tokenendpoint is called,FrontendResourceuses a REST client withOIDC Token Propagation Filterto propagate the current incoming access token toProtectedResource. -
ProtectedResourcehas two endpoints:-
/protected/user-name -
/protected/admin-nameBoth endpoints return the username extracted from the incoming access token, which was propagated to
ProtectedResourcefromFrontendResource. The only difference between these endpoints is that calling/protected/user-nameis only allowed if the current access token has auserrole, and calling/protected/admin-nameis only allowed if the current access token has anadminrole.
-
Solución
Recomendamos que siga las instrucciones de las siguientes secciones y cree la aplicación paso a paso. Sin embargo, también puede ir directamente al ejemplo completo.
Clone el repositorio Git: git clone https://github.com/quarkusio/quarkus-quickstarts.git o descargue un archivo.
The solution is in the security-openid-connect-client-quickstart directory.
Creación del proyecto Maven
Create a new project with the following command:
For Windows users:
-
If using cmd, (don’t use backward slash
\and put everything on the same line) -
If using Powershell, wrap
-Dparameters 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:
quarkus extension add oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest
./mvnw quarkus:add-extension -Dextensions='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest'
./gradlew addExtension --extensions='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest'
It adds the following extensions to your build file:
<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>
implementation("io.quarkus:quarkus-oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest")
Writing the application
-
Implement
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()); } }ProtectedResourcereturns a name from bothuserName()andadminName()methods. The name is extracted from the currentJsonWebToken. -
Add the following REST clients:
-
RestClientWithOidcClientFilter, which uses an OIDC client filter provided by thequarkus-rest-client-oidc-filterextension to get and propagate an access token. -
RestClientWithTokenHeaderParam, which accepts a token already acquired by the programmatically created OidcClient as an HTTPAuthorizationheader value. -
RestClientWithTokenPropagationFilter, which uses an OIDC token propagation filter provided by thequarkus-rest-client-oidc-token-propagationextension to get and propagate an access token.
-
-
Add the
RestClientWithOidcClientFilterREST 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(); }1 Register an OIDC client filter with the REST client to get and propagate the tokens. -
Add the
RestClientWithTokenHeaderParamREST 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); (1) }1 RestClientWithTokenHeaderParamREST client expects that the tokens will be passed to it as HTTPAuthorizationheader values. -
Add the
RestClientWithTokenPropagationFilterREST 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(); }1 Register an OIDC token propagation filter with the REST client to propagate the incoming already-existing tokens. Do not use the
RestClientWithOidcClientFilterandRestClientWithTokenPropagationFilterinterfaces 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.
-
Add
OidcClientCreatorto create an OIDC client programmatically at startup.OidcClientCreatorsupportsRestClientWithTokenHeaderParamREST 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); } }1 OidcClientscan be used to retrieve the already initialized, named OIDC clients and create new OIDC clients on demand. -
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() { (1) return restClientWithOidcClientFilter.getUserName(); } @GET @Path("admin-name-with-oidc-client-token") @Produces("text/plain") public Uni<String> getAdminNameWithOidcClientToken() { (1) return restClientWithOidcClientFilter.getAdminName(); } @GET @Path("user-name-with-propagated-token") @Produces("text/plain") public Uni<String> getUserNameWithPropagatedToken() { (2) return restClientWithTokenPropagationFilter.getUserName(); } @GET @Path("admin-name-with-propagated-token") @Produces("text/plain") public Uni<String> getAdminNameWithPropagatedToken() { (2) return restClientWithTokenPropagationFilter.getAdminName(); } @GET @Path("user-name-with-oidc-client-token-header-param") @Produces("text/plain") public Uni<String> getUserNameWithOidcClientTokenHeaderParam() { (4) 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() { (4) 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() { (5) 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() { (5) Tokens tokens = tokenHelper.getTokens(oidcClientCreator.getOidcClient()).await().indefinitely(); return restClientWithTokenHeaderParam.getAdminName("Bearer " + tokens.getAccessToken()).await().indefinitely(); } }1 FrontendResourceuses the injectedRestClientWithOidcClientFilterREST client with the OIDC client filter to get and propagate an access token toProtectedResourcewhen either/frontend/user-name-with-oidc-client-tokenor/frontend/admin-name-with-oidc-client-tokenis called.2 FrontendResourceuses the injectedRestClientWithTokenPropagationFilterREST client with the OIDC token propagation filter to propagate the current incoming access token toProtectedResourcewhen either/frontend/user-name-with-propagated-tokenor/frontend/admin-name-with-propagated-tokenis called.3 io.quarkus.oidc.client.runtime.TokensHelperis useful when the OIDC client is used directly, without the OIDC client filter. Pass the OIDC client toTokensHelperto get the tokens.TokensHelperacquires the tokens and refreshes them if necessary in a thread-safe way.4 FrontendResourceuses the programmatically created OIDC client to get and propagate an access token toProtectedResourceby passing it directly to the injectedRestClientWithTokenHeaderParamREST client’s method as an HTTPAuthorizationheader value when either/frontend/user-name-with-oidc-client-token-header-paramor/frontend/admin-name-with-oidc-client-token-header-paramis called.5 Sometimes, an application needs 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. -
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(); } }This exception mapper is only added to verify during the tests that
ProtectedResourcereturns403when 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
500to avoid leaking the information from the downstream resources such asProtectedResource. However, in the tests, it would not be possible to assert that500is caused by an authorization exception instead of some internal error.
Configuración de la aplicación
Having prepared the code, you configure the application:
# Configure OIDC
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=backend-service
quarkus.oidc.credentials.secret=secret
# Tell Dev Services for Keycloak to import the realm file
# This property is ineffective when running the application in JVM or Native modes but only in dev and test modes.
quarkus.keycloak.devservices.realm-path=quarkus-realm.json
# Configure OIDC Client
quarkus.oidc-client.auth-server-url=${quarkus.oidc.auth-server-url}
quarkus.oidc-client.client-id=${quarkus.oidc.client-id}
quarkus.oidc-client.credentials.secret=${quarkus.oidc.credentials.secret}
quarkus.oidc-client.grant.type=password
quarkus.oidc-client.grant-options.password.username=alice
quarkus.oidc-client.grant-options.password.password=alice
# Configure REST clients
%prod.port=8080
%dev.port=8080
%test.port=8081
org.acme.security.openid.connect.client.RestClientWithOidcClientFilter/mp-rest/url=http://localhost:${port}/protected
org.acme.security.openid.connect.client.RestClientWithTokenHeaderParam/mp-rest/url=http://localhost:${port}/protected
org.acme.security.openid.connect.client.RestClientWithTokenPropagationFilter/mp-rest/url=http://localhost:${port}/protected
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 For more information, see the Running the application in dev mode section. |
Starting and configuring the Keycloak server
|
Do not start the Keycloak server when you run the application in dev or test modes; For more information, see the Running the application in dev mode section. Ensure you put the realm configuration file on the classpath, in the |
-
Start a Keycloak Server by using Docker:
docker run --name keycloak -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:26.5.7 start-devYou can access your Keycloak Server at localhost:8180.
-
Log in as the
adminuser to access the Keycloak Administration Console. The password isadmin. -
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
quarkusrealm file adds afrontendclient, andaliceandadminusers.alicehas auserrole.adminhas bothuserandadminroles.
Running the application in dev mode
-
Run the application in a dev mode:
CLIquarkus devMaven./mvnw quarkus:devGradle./gradlew --console=plain quarkusDevDev 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 providerlink in the OpenID Connect Dev UI card. -
When asked, log in to a
Single Page Applicationprovided by the OpenID Connect Dev UI, log in asadmin, with the password,admin.This user has both
adminanduserroles.-
Access
/frontend/user-name-with-propagated-token, which returns200. -
Access
/frontend/admin-name-with-propagated-token, which returns200.
-
-
Log out and back in as
alicewith the password,alice.This user has a
userrole.-
Access
/frontend/user-name-with-propagated-token, which returns200. -
Access
/frontend/admin-name-with-propagated-token, which returns403.You have tested that
FrontendResourcecan propagate the access tokens from the OpenID Connect Dev UI.
-
Running the application in JVM mode
-
After exploring the application in dev mode, run it as a standard Java application by compiling it:
CLIquarkus buildMaven./mvnw installGradle./gradlew build -
Run it:
java -jar target/quarkus-app/quarkus-run.jar
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:
quarkus build --native
./mvnw install -Dnative
./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
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' \ ) -
Use this token to call
/frontend/user-name-with-propagated-token. This command returns the200status code and the namealice:curl -i -X GET \ http://localhost:8080/frontend/user-name-with-propagated-token \ -H "Authorization: Bearer "$access_token -
Use the same token to call
/frontend/admin-name-with-propagated-token. In contrast to the preceding command, this command returns403becausealicehas only auserrole:curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-propagated-token \ -H "Authorization: Bearer "$access_token -
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' \ ) -
Use this token to call
/frontend/user-name-with-propagated-token. This command returns a200status code and the nameadmin:curl -i -X GET \ http://localhost:8080/frontend/user-name-with-propagated-token \ -H "Authorization: Bearer "$access_token -
Use the same token to call
/frontend/admin-name-with-propagated-token. This command also returns the200status code and the nameadminbecauseadminhas bothuserandadminroles:curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-propagated-token \ -H "Authorization: Bearer "$access_token -
Check the
FrontendResourcemethods, which do not propagate the existing tokens but useOidcClientto get and propagate the tokens.As already shown,
OidcClientis configured to get the tokens for thealiceuser.curl -i -X GET \ http://localhost:8080/frontend/user-name-with-oidc-client-tokenThis command returns the
200status code and the namealice.curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-oidc-client-tokenIn contrast with the preceding command, this command returns a
403status code. -
Test that the programmatically created OIDC client correctly acquires and propagates the token with
RestClientWithTokenHeaderParamboth in reactive and imperative (blocking) modes.-
Call the
/user-name-with-oidc-client-token-header-param. This command returns the200status code and the namealice:curl -i -X GET \ http://localhost:8080/frontend/user-name-with-oidc-client-token-header-param -
Call the
/admin-name-with-oidc-client-token-header-param. In contrast with the preceding command, this command returns a403status code:curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-oidc-client-token-header-param
-
-
Test the endpoints that use OIDC client in the blocking mode.
-
Call the
/user-name-with-oidc-client-token-header-param-blocking. This command returns the200status code and the namealice:curl -i -X GET \ http://localhost:8080/frontend/user-name-with-oidc-client-token-header-param-blocking -
Call the
/admin-name-with-oidc-client-token-header-param-blocking. In contrast with the preceding command, this command returns a403status code:curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-oidc-client-token-header-param-blocking
-