The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

OpenID Connect Client and Token Propagation Quickstart

This quickstart demonstrates how to use OpenID Connect Client Reactive Filter to acquire and propagate access tokens as HTTP Authorization Bearer access tokens, alongside OpenID Token Propagation Reactive Filter which propagates the incoming HTTP Authorization Bearer access tokens.

Please check OpenID Connect Client and Token Propagation Reference Guide for all the information related to Oidc Client and Token Propagation support in Quarkus.

Please also read Using OpenID Connect to Protect Service Applications guide if you need to protect your applications using Bearer Token Authorization.

先决条件

要完成这个指南,你需要:

  • 大概15分钟

  • 编辑器

  • 安装JDK 11以上版本并正确配置了 JAVA_HOME

  • Apache Maven 3.8.6

  • A working container runtime (Docker or Podman)

  • 如果你愿意的话,还可以选择使用Quarkus CLI

  • 如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)

  • jq工具

应用结构

In this example, we will build an application which consists of two JAX-RS resources, FrontendResource and ProtectedResource. FrontendResource propagates access tokens to ProtectedResource and uses either OpenID Connect Client Reactive Filter to acquire a token first before propagating it or OpenID Token Propagation Reactive Filter to propagate the incoming, already existing access token.

FrontendResource has 4 endpoints:

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

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

  • /frontend/user-name-with-propagated-token

  • /frontend/admin-name-with-propagated-token

FrontendResource will use REST Client with OpenID Connect Client Reactive Filter to acquire 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. And it will use REST Client with OpenID Connect Token Propagation Reactive Filter to propagate the current incoming access token to ProtectedResource when either /frontend/user-name-with-propagated-token or /frontend/admin-name-with-propagated-token is called.

ProtecedResource has 2 endpoints:

  • /protected/user-name

  • /protected/admin-name

Both of these 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.

解决方案

我们建议您按照下一节的说明逐步创建应用程序。然而,您可以直接转到已完成的示例。

克隆 Git 仓库: git clone https://github.com/quarkusio/quarkus-quickstarts.git ,或下载一个 存档

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

创建Maven项目

首先,我们需要一个新的项目。使用以下命令创建一个新的项目:

CLI
quarkus create app org.acme:security-openid-connect-client-quickstart \
    --stream=2.16 \
    --extension='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive' \
    --no-code
cd security-openid-connect-client-quickstart

创建Grade项目,请添加 --gradle 或者 --gradle-kotlin-dsl 参数。

关于如何安装并使用Quarkus CLI的更多信息,请参考Quarkus CLI指南

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:2.16.12.Final:create \
    -DplatformVersion=2.16 \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-openid-connect-client-quickstart \
    -Dextensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive' \
    -DnoCode
cd security-openid-connect-client-quickstart

创建Grade项目,请添加 -DbuildTool=gradle 或者 -DbuildTool=gradle-kotlin-dsl 参数。

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

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

CLI
quarkus extension add 'oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'
Maven
./mvnw quarkus:add-extension -Dextensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'
Gradle
./gradlew addExtension --extensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'

这将在您的构建文件中添加以下内容:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-client-reactive-filter</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-token-propagation-reactive</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive")

编写应用程序

Let’s start by implementing ProtectedResource:

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

import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.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());
    }
}

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

Next let’s add a REST Client with OidcClientRequestReactiveFilter and another REST Client with AccessTokenRequestReactiveFilter. FrontendResource will use these two clients to call ProtectedResource:

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

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

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import io.quarkus.oidc.client.reactive.filter.OidcClientRequestReactiveFilter;
import io.smallrye.mutiny.Uni;

@RegisterRestClient
@RegisterProvider(OidcClientRequestReactiveFilter.class)
@Path("/")
public interface RestClientWithOidcClientFilter {

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

    @GET
    @Produces("text/plain")
    @Path("adminName")
    Uni<String> getAdminName();
}

where RestClientWithOidcClientFilter will depend on OidcClientRequestReactiveFilter to acquire and propagate the tokens and

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

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

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;
import io.smallrye.mutiny.Uni;

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

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

    @GET
    @Produces("text/plain")
    @Path("adminName")
    Uni<String> getAdminName();
}

where RestClientWithTokenPropagationFilter will depend on AccessTokenRequestReactiveFilter to propagate the incoming, already existing tokens.

Note that both RestClientWithOidcClientFilter and RestClientWithTokenPropagationFilter interfaces are identical - the reason behind it is that combining OidcClientRequestReactiveFilter and AccessTokenRequestReactiveFilter on the same REST Client will cause side effects as both filters can interfere with other, for example, OidcClientRequestReactiveFilter may override the token propagated by AccessTokenRequestReactiveFilter or AccessTokenRequestReactiveFilter can fail if it is called when no token is available to propagate and OidcClientRequestReactiveFilter is expected to acquire a new token instead.

Now let’s complete creating the application with adding FrontendResource:

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

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

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

import io.smallrye.mutiny.Uni;

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

    @Inject
    @RestClient
    RestClientWithTokenPropagationFilter restClientWithTokenPropagationFilter;

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

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

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

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

FrontendResource will use REST Client with OpenID Connect Client Reactive Filter to acquire 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. And it will use REST Client with OpenID Connect Token Propagation Reactive Filter to propagate the current incoming access token to ProtectedResource when either /frontend/user-name-with-propagated-token or /frontend/admin-name-with-propagated-token is called.

Finally, let’s add a JAX-RS ExceptionMapper:

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

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.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 RESTEasy Reactive will correctly convert the exceptions which will escape from REST Client calls to 500 to avoid leaking the information from the downstream resources such as ProtectedResource but in the tests it will not be possible to assert that 500 is in fact caused by an authorization exception as opposed to some internal error.

配置该应用程序

We have prepared the code, and now let’s 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 not effective when running the application in JVM or Native modes but only in dev and test modes.

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

# Configure OIDC Client

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

# Configure REST Clients

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

org.acme.security.openid.connect.client.RestClientWithOidcClientFilter/mp-rest/url=http://localhost:${port}/protected
org.acme.security.openid.connect.client.RestClientWithTokenPropagationFilter/mp-rest/url=http://localhost:${port}/protected

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

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

启动和配置Keycloak服务器

Do not start the Keycloak server when you run the application in dev mode or test modes - Dev Services for Keycloak will launch a container. See Running the Application in Dev mode section below for more information. Make sure to put the realm configuration file on the classpath (target/classes directory) so that it gets imported automatically when running in dev mode - unless you have already built a complete solution in which case this realm file will be added to the classpath during the build.

要启动Keycloak服务器,您可以使用Docker服务,且只需运行以下命令:

docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev

其中 keycloak.version 应该设置为 17.0.0 或更高。

你应该能够通过 localhost:8180 访问你的Keycloak服务器。

admin 用户身份登录,访问Keycloak管理控制台。用户名应该是 admin ,密码是 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 will add a frontend client, and alice and admin users. alice has a user role, admin - both user and admin roles.

在开发模式下运行应用程序

要在开发模式下运行应用程序,请使用:

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

Keycloak开发服务 将启动一个Keycloak容器并导入一个 quarkus-realm.json

打开 /q/dev 提供的 开发用户界面 ,点击 OpenID Connect Dev UI 卡片中的 Provider: Keycloak 链接。

您将被要求登录到由 OpenID Connect Dev UI 提供的 Single Page Application

  • alice (密码: alice )的身份登录,他的角色(role)是 user

    • accessing /frontend/user-name-with-propagated-token will return 200

    • accessing /frontend/admin-name-with-propagated-token will return 403

  • 退出并以 admin (密码: admin )的身份登录,他同时拥有 adminuser 的角色(roles)

    • accessing /frontend/user-name-with-propagated-token will return 200

    • accessing /frontend/admin-name-with-propagated-token will return 200

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

在JVM模式下运行应用程序

当您尝试完毕 开发 模式后,您可以把它作为一个标准的Java应用程序运行。

首先编译它:

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

然后运行:

java -jar target/quarkus-app/quarkus-run.jar

在本地模式下运行应用程序

这个同样的演示可以被编译成本地代码:不需要任何修改。

这意味着您不再需要在您的生产环境中安装JVM,因为运行时技术包含在生产的二进制文件中,并以最小的资源开销优化运行。

编译会花一点时间,所以这一步默认是禁用的;让我们通过启用 native profile来再次构建:

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native

一杯咖啡的时间后,您就可以直接运行该二进制文件了:

./target/security-openid-connect-quickstart-1.0.0-SNAPSHOT-runner

测试应用程序

See Running the Application in Dev mode section above about testing your application in dev mode.

您可以用 curl 测试在JVM或Native模式下启动的应用程序。

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' \
 )

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

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token

will return 200 status code and the name alice while

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token

will return 403 - recall that alice only has a user role.

Next obtain an access token for admin:

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

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

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token

will return 200 status code and the name admin, and

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token

will also return 200 status code and the name admin, as admin has both user and admin roles.

Now let’s check FrontendResource methods which do not propagate the existing tokens but use OidcClient to acquire and propagate the tokens. You have seen that OidcClient is configured to acquire the tokens for the alice user, so:

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-oidc-client-token

will return 200 status code and the name alice, but

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-oidc-client-token

will return 403 status code.