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 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.15 , 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 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.15.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 Using Maven: ./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' 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> Using Gradle: implementation("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()); } } ProtectedResource returns a name from both userName() and adminName() methods. The name is extracted from the current JsonWebToken . , add the following REST clients: RestClientWithOidcClientFilter , which uses an OIDC client filter provided by the quarkus-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 HTTP Authorization header value. RestClientWithTokenPropagationFilter , which uses an OIDC token propagation filter provided by the quarkus-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(); } 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 } 1 2 RestClientWithTokenHeaderParam REST client expects that the tokens will be passed to it as HTTP Authorization header values. 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.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. Important 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.OidcClientConfig; import io.quarkus.oidc.client.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; 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 = new OidcClientConfig(); cfg.setId("myclient"); cfg.setAuthServerUrl(oidcProviderAddress); cfg.setClientId("backend-service"); cfg.getCredentials().setSecret("secret"); cfg.getGrant().setType(Type.PASSWORD); cfg.setGrantOptions(Map.of("password", Map.of("username", "alice", "password", "alice"))); return oidcClients.newClient(cfg); } } 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(); } } 1 5 6 FrontendResource uses the injected RestClientWithOidcClientFilter REST client with the OIDC client filter to get and propagate an access token to ProtectedResource when either /frontend/user-name-with-oidc-client-token or /frontend/admin-name-with-oidc-client-token is called. 2 7 8 FrontendResource uses the injected RestClientWithTokenPropagationFilter REST client with the OIDC token propagation filter to propagate the current incoming access token to ProtectedResource when either /frontend/user-name-with-propagated-token or /frontend/admin-name-with-propagated-token is called. 4 9 10 FrontendResource uses the programmatically created OIDC client to get and propagate an access token to ProtectedResource by passing it directly to the injected RestClientWithTokenHeaderParam REST client's method as an HTTP Authorization 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 use TokensHelper , pass OIDC Client to it to get the tokens and TokensHelper 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(); } } 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 %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=USD{quarkus.oidc.auth-server-url} quarkus.oidc-client.client-id=USD{quarkus.oidc.client-id} quarkus.oidc-client.credentials.secret=USD{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:USD{port}/protected org.acme.security.openid.connect.client.RestClientWithTokenHeaderParam/mp-rest/url=http://localhost:USD{port}/protected org.acme.security.openid.connect.client.RestClientWithTokenPropagationFilter/mp-rest/url=http://localhost:USD{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. Note Adding a %prod. profile prefix to quarkus.oidc.auth-server-url ensures that Dev Services for Keycloak launches a container for you when the application is run in dev or test modes. For more information, see the Running the application in dev mode section. 2.7. Starting and configuring the Keycloak server Note Do not start the Keycloak server when you run the application in dev or test modes; Dev Services for Keycloak launches a container. For more information, see the Running the application in dev mode section. Ensure you put the realm configuration file on the classpath, in the target/classes directory. This placement ensures that the file is automatically imported in dev mode. However, if you have already built a complete solution , you do not need to add the realm file to the classpath because the build process has already done so. To start a Keycloak Server, you can use Docker and just run the following command: docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev Set {keycloak.version} to 25.0.6 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 Using Maven: ./mvnw quarkus:dev Using Gradle: ./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 alice , with the password, alice . This user has both admin and user roles. Access /frontend/user-name-with-propagated-token , which returns 200 . Access /frontend/admin-name-with-propagated-token , which returns 200 . Log out and back in as bob with the password, bob . This user has a user role. Access /frontend/user-name-with-propagated-token , which returns 200 . Access /frontend/admin-name-with-propagated-token , which returns 403 . 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 Using Maven: ./mvnw install Using Gradle: ./gradlew build Then, run it: java -jar target/quarkus-app/quarkus-run.jar 2.10. Running the application in native mode You can compile this demo into native code; no modifications are required. This implies that you no longer need to install a JVM on your production environment, as the runtime technology is included in the produced binary and optimized to run with minimal resources. Compilation takes longer, so this step is turned off by default. To build again, enable the native profile: Using the Quarkus CLI: quarkus build --native Using Maven: ./mvnw install -Dnative Using Gradle: ./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 2.11. Testing the application For more information about testing your application in dev mode, see the preceding Running the application in dev mode section. You can test the application launched in JVM or Native modes with curl . Obtain an access token for alice : export access_token=USD(\ 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 the 200 status code and the name alice : curl -i -X GET \ http://localhost:8080/frontend/user-name-with-propagated-token \ -H "Authorization: Bearer "USDaccess_token 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 "USDaccess_token , obtain an access token for admin : export access_token=USD(\ 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 a 200 status code and the name admin : curl -i -X GET \ http://localhost:8080/frontend/user-name-with-propagated-token \ -H "Authorization: Bearer "USDaccess_token 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 "USDaccess_token , 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 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 In contrast with the preceding command, this command returns a 403 status code. , 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 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 , 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 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 2.12. References OpenID Connect Client and Token Propagation Reference Guide OIDC Bearer token authentication Quarkus Security overview

Commands:
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
mvn com.redhat.quarkus.platform:quarkus-maven-plugin:3.15.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
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'
<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")
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.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 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 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.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 java.util.Map; import org.eclipse.microprofile.config.inject.ConfigProperty; import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.OidcClientConfig; import io.quarkus.oidc.client.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; 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 = new OidcClientConfig(); cfg.setId("myclient"); cfg.setAuthServerUrl(oidcProviderAddress); cfg.setClientId("backend-service"); cfg.getCredentials().setSecret("secret"); cfg.getGrant().setType(Type.PASSWORD); cfg.setGrantOptions(Map.of("password", Map.of("username", "alice", "password", "alice"))); return oidcClients.newClient(cfg); } }
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.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(); } }
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=USD{quarkus.oidc.auth-server-url} quarkus.oidc-client.client-id=USD{quarkus.oidc.client-id} quarkus.oidc-client.credentials.secret=USD{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:USD{port}/protected org.acme.security.openid.connect.client.RestClientWithTokenHeaderParam/mp-rest/url=http://localhost:USD{port}/protected org.acme.security.openid.connect.client.RestClientWithTokenPropagationFilter/mp-rest/url=http://localhost:USD{port}/protected
docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
quarkus build
./mvnw install
./gradlew build
java -jar target/quarkus-app/quarkus-run.jar
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
./target/security-openid-connect-quickstart-1.0.0-SNAPSHOT-runner
export access_token=USD( 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' )
curl -i -X GET http://localhost:8080/frontend/user-name-with-propagated-token -H "Authorization: Bearer "USDaccess_token
curl -i -X GET http://localhost:8080/frontend/admin-name-with-propagated-token -H "Authorization: Bearer "USDaccess_token
export access_token=USD( 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' )
curl -i -X GET http://localhost:8080/frontend/user-name-with-propagated-token -H "Authorization: Bearer "USDaccess_token
curl -i -X GET http://localhost:8080/frontend/admin-name-with-propagated-token -H "Authorization: Bearer "USDaccess_token
curl -i -X GET http://localhost:8080/frontend/user-name-with-oidc-client-token
curl -i -X GET http://localhost:8080/frontend/admin-name-with-oidc-client-token
curl -i -X GET http://localhost:8080/frontend/user-name-with-oidc-client-token-header-param
curl -i -X GET http://localhost:8080/frontend/admin-name-with-oidc-client-token-header-param
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/admin-name-with-oidc-client-token-header-param-blocking