OpenID Connect (OIDC) Bearer token authentication

使用 Quarkus OpenID Connect (OIDC) 扩展,通过 Bearer 令牌认证安全地访问您应用程序中的 Jakarta REST(以前称为 JAX-RS)端点。

Secure HTTP access to Jakarta REST (formerly known as JAX-RS) endpoints in your application with Bearer token authentication by using the Quarkus OpenID Connect (OIDC) extension.

Overview of the Bearer token authentication mechanism in Quarkus

Quarkus 通过 Quarkus OpenID Connect (OIDC) 扩展支持 Bearer 令牌认证机制。

Quarkus supports the Bearer token authentication mechanism through the Quarkus OpenID Connect (OIDC) extension.

Bearer 令牌由 OIDC 和 OAuth 2.0 兼容授权服务器(例如 Keycloak)颁发。

The bearer tokens are issued by OIDC and OAuth 2.0 compliant authorization servers, such as Keycloak.

Bearer 令牌认证是在基于 Bearer 令牌的存在及有效性的过程中授权 HTTP 请求。Bearer 令牌提供有关调用主题的信息,此信息用于确定能否访问 HTTP 资源。

Bearer token authentication is the process of authorizing HTTP requests based on the existence and validity of a bearer token. The bearer token provides information about the subject of the call, which is used to determine whether or not an HTTP resource can be accessed.

以下示意图概述了 Quarkus 中的 Bearer 令牌认证机制:

The following diagrams outline the Bearer token authentication mechanism in Quarkus:

security bearer token authorization mechanism 1
Figure 1. Bearer token authentication mechanism in Quarkus with single-page application
  1. The Quarkus service retrieves verification keys from the OIDC provider. The verification keys are used to verify the bearer access token signatures.

  2. The Quarkus user accesses the single-page application (SPA).

  3. The single-page application uses Authorization Code Flow to authenticate the user and retrieve tokens from the OIDC provider.

  4. The single-page application uses the access token to retrieve the service data from the Quarkus service.

  5. The Quarkus service verifies the bearer access token signature by using the verification keys, checks the token expiry date and other claims, allows the request to proceed if the token is valid, and returns the service response to the single-page application.

  6. The single-page application returns the same data to the Quarkus user.

security bearer token authorization mechanism 2
Figure 2. Bearer token authentication mechanism in Quarkus with Java or command line client
  1. The Quarkus service retrieves verification keys from the OIDC provider. The verification keys are used to verify the bearer access token signatures.

  2. The client uses client_credentials that requires client id and secret or password grant, which requires client id, secret, username, and password to retrieve the access token from the OIDC provider.

  3. The client uses the access token to retrieve the service data from the Quarkus service.

  4. The Quarkus service verifies the bearer access token signature by using the verification keys, checks the token expiry date and other claims, allows the request to proceed if the token is valid, and returns the service response to the client.

如果您需要通过使用 OIDC 授权代码流对用户进行身份验证和授权,请参阅 Quarkus OpenID Connect authorization code flow mechanism for protecting web applications指南。此外,如果您使用 Keycloak 和携带者令牌,请参阅 Quarkus Using Keycloak to centralize authorization指南。

If you need to authenticate and authorize users by using OIDC authorization code flow, see the Quarkus OpenID Connect authorization code flow mechanism for protecting web applications guide. Also, if you use Keycloak and bearer tokens, see the Quarkus Using Keycloak to centralize authorization guide.

要了解如何使用 OIDC 携带者令牌身份验证保护服务应用程序,请参阅以下自学教程:* Protect a web application by using OpenID Connect (OIDC) authorization code flow.

To learn about how you can protect service applications by using OIDC Bearer token authentication, see the following tutorial: * Protect a web application by using OpenID Connect (OIDC) authorization code flow.

有关如何支持多租户的信息,请参阅 Quarkus Using OpenID Connect Multi-Tenancy指南。

For information about how to support multiple tenants, see the Quarkus Using OpenID Connect Multi-Tenancy guide.

Accessing JWT claims

如果您需要访问 JWT 令牌权利声明,可以注入 JsonWebToken

If you need to access JWT token claims, you can inject JsonWebToken:

package org.acme.security.openid.connect;

import org.eclipse.microprofile.jwt.JsonWebToken;
import jakarta.inject.Inject;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/api/admin")
public class AdminResource {

    @Inject
    JsonWebToken jwt;

    @GET
    @RolesAllowed("admin")
    @Produces(MediaType.TEXT_PLAIN)
    public String admin() {
        return "Access for subject " + jwt.getSubject() + " is granted";
    }
}

@ApplicationScoped@Singleton`和 `@RequestScoped`范围内支持注入 `JsonWebToken。但是,如果将各个权利声明作为简单类型注入,则需要使用 @RequestScoped。有关详细信息,请参阅 Quarkus “使用 JWT RBAC”指南的 Supported injection scopes部分。

Injection of JsonWebToken is supported in @ApplicationScoped, @Singleton, and @RequestScoped scopes. However, the use of @RequestScoped is required if the individual claims are injected as simple types. For more information, see the Supported injection scopes section of the Quarkus "Using JWT RBAC" guide.

UserInfo

如果您必须从 OIDC UserInfo`端点请求 UserInfo JSON 对象,请设置 `quarkus.oidc.authentication.user-info-required=true。将向 OIDC 提供程序 UserInfo`端点发送请求,并将创建一个 `io.quarkus.oidc.UserInfo(一个简单的 javax.json.JsonObject`包装器)对象。`io.quarkus.oidc.UserInfo`可以用作 `SecurityIdentity `userinfo`属性注入或访问。

If you must request a UserInfo JSON object from the OIDC UserInfo endpoint, set quarkus.oidc.authentication.user-info-required=true. A request is sent to the OIDC provider UserInfo endpoint, and an io.quarkus.oidc.UserInfo (a simple javax.json.JsonObject wrapper) object is created. io.quarkus.oidc.UserInfo can be injected or accessed as a SecurityIdentity userinfo attribute.

如果满足以下某个条件,则 `quarkus.oidc.authentication.user-info-required`将自动启用:

quarkus.oidc.authentication.user-info-required is automatically enabled if one of these conditions is met:

  • if quarkus.oidc.roles.source is set to userinfo or quarkus.oidc.token.verify-access-token-with-user-info is set to true or quarkus.oidc.authentication.id-token-required is set to false, the current OIDC tenant must support a UserInfo endpoint in these cases.

  • if io.quarkus.oidc.UserInfo injection point is detected but only if the current OIDC tenant supports a UserInfo endpoint.

Configuration metadata

io.quarkus.oidc.OidcConfigurationMetadata`表示当前租户发现的 OpenID Connect Configuration Metadata,可以用作 `SecurityIdentity `configuration-metadata`属性注入或访问。

The current tenant’s discovered OpenID Connect Configuration Metadata is represented by io.quarkus.oidc.OidcConfigurationMetadata and can be injected or accessed as a SecurityIdentity configuration-metadata attribute.

如果端点为公共端点,则会注入默认租户的 OidcConfigurationMetadata

The default tenant’s OidcConfigurationMetadata is injected if the endpoint is public.

Token claims and SecurityIdentity roles

您可以按如下方式映射已验证 JWT 访问令牌中的 `SecurityIdentity`角色:

You can map SecurityIdentity roles from the verified JWT access tokens as follows:

  • If the quarkus.oidc.roles.role-claim-path property is set, and matching array or string claims are found, then the roles are extracted from these claims. For example, customroles, customroles/array, scope, "http://namespace-qualified-custom-claim"/roles, "http://namespace-qualified-roles".

  • If a groups claim is available, then its value is used.

  • If a realm_access/roles or resource_access/client_id/roles (where client_id is the value of the quarkus.oidc.client-id property) claim is available, then its value is used. This check supports the tokens issued by Keycloak.

例如,以下 JWT 令牌具有包含包含角色的 `roles`数组的复杂 `groups`权利声明:

For example, the following JWT token has a complex groups claim that contains a roles array that includes roles:

{
    "iss": "https://server.example.com",
    "sub": "24400320",
    "upn": "jdoe@example.com",
    "preferred_username": "jdoe",
    "exp": 1311281970,
    "iat": 1311280970,
    "groups": {
        "roles": [
          "microprofile_jwt_user"
        ],
    }
}

您必须将 microprofile_jwt_user 角色映射到 SecurityIdentity 角色,可以通过以下配置进行: quarkus.oidc.roles.role-claim-path=groups/roles

You must map the microprofile_jwt_user role to SecurityIdentity roles, and you can do so with this configuration: quarkus.oidc.roles.role-claim-path=groups/roles.

如果令牌是不透明的(二进制),则使用远程令牌内省响应中的一个 scope 属性。

If the token is opaque (binary), then a scope property from the remote token introspection response is used.

如果 UserInfo 是角色的来源,则设置 quarkus.oidc.authentication.user-info-required=truequarkus.oidc.roles.source=userinfo,并且在需要时设置 quarkus.oidc.roles.role-claim-path

If UserInfo is the source of the roles, then set quarkus.oidc.authentication.user-info-required=true and quarkus.oidc.roles.source=userinfo, and if needed, set quarkus.oidc.roles.role-claim-path.

此外,还可以使用自定义 SecurityIdentityAugmentor 来添加角色。更多信息请参阅 Quarkus “安全提示和技巧”指南的 Security identity customization 部分。

Additionally, a custom SecurityIdentityAugmentor can also be used to add the roles. For more information, see the Security identity customization section of the Quarkus "Security tips and tricks" guide.

您还可以使用 HTTP Security policy 将从令牌声明创建的 SecurityIdentity 角色映射到部署特定角色。

You can also map SecurityIdentity roles created from token claims to deployment-specific roles by using the HTTP Security policy.

Token scopes and SecurityIdentity permissions

SecurityIdentity 权限以 io.quarkus.security.StringPermission 的形式从 source of the roles 的范围参数映射,并使用相同的声明分隔符。

SecurityIdentity permissions are mapped in the form of io.quarkus.security.StringPermission from the scope parameter of the token-claims-and-security-identity-roles and using the same claim separator.

import java.util.List;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.security.PermissionsAllowed;

@Path("/service")
public class ProtectedResource {

    @Inject
    JsonWebToken accessToken;

    @PermissionsAllowed("email") 1
    @GET
    @Path("/email")
    public Boolean isUserEmailAddressVerifiedByUser() {
        return accessToken.getClaim(Claims.email_verified.name());
    }

    @PermissionsAllowed("orders_read") 2
    @GET
    @Path("/order")
    public List<Order> listOrders() {
        return List.of(new Order(1));
    }

    public static class Order {
        String id;
        public Order() {
        }
        public Order(String id) {
            this.id = id;
        }
        public String getId() {
            return id;
        }
        public void setId() {
            this.id = id;
        }
    }
}
1 Only requests with OpenID Connect scope email will be granted access.
2 The read access is limited to the client requests with the orders_read scope.

有关 io.quarkus.security.PermissionsAllowed 注释的更多信息,请参阅 “Web 端点授权” 指南的 Permission annotation 部分。

For more information about the io.quarkus.security.PermissionsAllowed annotation, see the Permission annotation section of the "Authorization of web endpoints" guide.

Token verification and introspection

如果令牌是 JWT 令牌,则默认情况下,它将使用来自本地 JsonWebKeySetJsonWebKey (JWK) 密钥进行验证,该密钥是通过 OIDC 提供程序的 JWK 端点检索的。令牌的密钥标识符 (kid) 头部值用于查找匹配的 JWK 密钥。如果本地没有可用的匹配 JWK,则通过从 JWK 端点获取当前密钥集刷新 JsonWebKeySetJsonWebKeySet 刷新只能在 quarkus.oidc.token.forced-jwk-refresh-interval 过期之后才能重复执行。默认过期时间为 10 分钟。如果刷新后仍没有可用的匹配 JWK,则 JWT 令牌将发送到 OIDC 提供程序的令牌内省端点。

If the token is a JWT token, then, by default, it is verified with a JsonWebKey (JWK) key from a local JsonWebKeySet, retrieved from the OIDC provider’s JWK endpoint. The token’s key identifier (kid) header value is used to find the matching JWK key. If no matching JWK is available locally, then JsonWebKeySet is refreshed by fetching the current key set from the JWK endpoint. The JsonWebKeySet refresh can be repeated only after the quarkus.oidc.token.forced-jwk-refresh-interval expires. The default expiry time is 10 minutes. If no matching JWK is available after the refresh, the JWT token is sent to the OIDC provider’s token introspection endpoint.

如果令牌是不透明的,这意味着它可以是二进制令牌或加密的 JWT 令牌,则它总是会被发送到 OIDC 提供程序的令牌内省端点。

If the token is opaque, which means it can be a binary token or an encrypted JWT token, then it is always sent to the OIDC provider’s token introspection endpoint.

如果您仅使用 JWT 令牌并期望始终提供匹配的 JsonWebKey,例如在刷新密钥集之后,则必须禁用令牌内省,如下例所示:

If you work only with JWT tokens and expect a matching JsonWebKey to always be available, for example, after refreshing a key set, you must disable token introspection, as shown in the following example:

quarkus.oidc.token.allow-jwt-introspection=false
quarkus.oidc.token.allow-opaque-token-introspection=false

在某些情况下,必须仅通过内省验证 JWT 令牌,可以通过仅配置内省端点地址来强制执行。以下属性配置展示了如何使用 Keycloak 实现此目的:

There might be cases where JWT tokens must be verified through introspection only, which can be forced by configuring an introspection endpoint address only. The following properties configuration shows you an example of how you can achieve this with Keycloak:

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.discovery-enabled=false
# Token Introspection endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/tokens/introspect
quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect

远程强制执行 JWT 令牌内省具有优缺点。一种优势是,它消除了对两个远程调用的需求:一个远程 OIDC 元数据发现调用,随后是另一个远程调用来获取不会被使用的验证密钥。一种劣势是,您需要了解内省端点地址并手动进行配置。

There are advantages and disadvantages to indirectly enforcing the introspection of JWT tokens remotely. An advantage is that you eliminate the need for two remote calls: a remote OIDC metadata discovery call followed by another remote call to fetch the verification keys that will not be used. A disadvantage is that you need to know the introspection endpoint address and configure it manually.

另一种方法是允许 OIDC 元数据发现的默认选项,但也要求仅执行远程 JWT 内省,如下例所示:

The alternative approach is to allow the default option of OIDC metadata discovery but also require that only the remote JWT introspection is performed, as shown in the following example:

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.token.require-jwt-introspection-only=true

此方法的一个优点是配置更简单且更容易理解。一种劣势是,即使不会获取验证密钥,也需要进行远程 OIDC 元数据发现调用来发现内省端点地址。

An advantage of this approach is that the configuration is simpler and easier to understand. A disadvantage is that a remote OIDC metadata discovery call is required to discover an introspection endpoint address, even though the verification keys will not be fetched.

将创建一个 io.quarkus.oidc.TokenIntrospection,一个简单的 jakarta.json.JsonObject 包装对象。它可以作为 SecurityIdentity introspection 属性进行注入或访问,前提是已经成功内省了 JWT 或不透明令牌。

The io.quarkus.oidc.TokenIntrospection, a simple jakarta.json.JsonObject wrapper object, will be created. It can be injected or accessed as a SecurityIdentity introspection attribute, providing either the JWT or opaque token has been successfully introspected.

Token introspection and UserInfo cache

所有不透明访问令牌必须远程内省。有时,JWT 访问令牌也可能必须内省。如果 UserInfo 也是必需的,则在后续的远程调用中向 OIDC 提供程序使用相同的访问令牌。因此,如果 UserInfo 是必需的,并且当前访问令牌是不透明的,则针对每个此类令牌进行两个远程调用;一个远程调用来内省令牌,另一个远程调用来获取 UserInfo。如果令牌是 JWT,则只需要进行一个远程调用来获取 UserInfo,除非它也必须被内省。

All opaque access tokens must be remotely introspected. Sometimes, JWT access tokens might also have to be introspected. If UserInfo is also required, the same access token is used in a subsequent remote call to the OIDC provider. So, if UserInfo is required, and the current access token is opaque, two remote calls are made for every such token; one remote call to introspect the token and another to get UserInfo. If the token is JWT, only a single remote call to get UserInfo is needed, unless it also has to be introspected.

对于每个传入承载令牌或代码流程访问令牌进行多达两个远程调用的成本有时可能是有问题的。

The cost of making up to two remote calls for every incoming bearer or code flow access token can sometimes be problematic.

如果在生产过程中出现了这种情况,请考虑将令牌自省和 UserInfo 数据缓存一小段时间,例如 3 或 5 分钟。

If this is the case in production, consider caching the token introspection and UserInfo data for a short period, for example, 3 or 5 minutes.

quarkus-oidc 提供 quarkus.oidc.TokenIntrospectionCachequarkus.oidc.UserInfoCache 接口,可用于 @ApplicationScoped 缓存实现。使用 @ApplicationScoped 缓存实现来存储和检索 quarkus.oidc.TokenIntrospection 和/或 quarkus.oidc.UserInfo 对象,如下例所示:

quarkus-oidc provides quarkus.oidc.TokenIntrospectionCache and quarkus.oidc.UserInfoCache interfaces, usable for @ApplicationScoped cache implementation. Use @ApplicationScoped cache implementation to store and retrieve quarkus.oidc.TokenIntrospection and/or quarkus.oidc.UserInfo objects, as outlined in the following example:

@ApplicationScoped
@Alternative
@Priority(1)
public class CustomIntrospectionUserInfoCache implements TokenIntrospectionCache, UserInfoCache {
...
}

每个 OIDC 租户都可以通过布尔 quarkus.oidc."tenant".allow-token-introspection-cachequarkus.oidc."tenant".allow-user-info-cache 属性允许或拒绝存储其 quarkus.oidc.TokenIntrospection 数据、quarkus.oidc.UserInfo 数据,或两者都存储。

Each OIDC tenant can either permit or deny the storing of its quarkus.oidc.TokenIntrospection data, quarkus.oidc.UserInfo data, or both with boolean quarkus.oidc."tenant".allow-token-introspection-cache and quarkus.oidc."tenant".allow-user-info-cache properties.

此外,quarkus-oidc 提供了一个简单的基于内存的默认令牌缓存,它同时实现了 quarkus.oidc.TokenIntrospectionCachequarkus.oidc.UserInfoCache 接口。

Additionally, quarkus-oidc provides a simple default memory-based token cache, which implements both quarkus.oidc.TokenIntrospectionCache and quarkus.oidc.UserInfoCache interfaces.

您可以按如下方式配置和激活 OIDC 令牌缓存:

You can configure and activate the default OIDC token cache as follows:

# 'max-size' is 0 by default, so the cache can be activated by setting 'max-size' to a positive value:
quarkus.oidc.token-cache.max-size=1000
# 'time-to-live' specifies how long a cache entry can be valid for and will be used by a cleanup timer:
quarkus.oidc.token-cache.time-to-live=3M
# 'clean-up-timer-interval' is not set by default, so the cleanup timer can be activated by setting 'clean-up-timer-interval':
quarkus.oidc.token-cache.clean-up-timer-interval=1M

默认的缓存以令牌作为键,并且每个条目可以具有 TokenIntrospectionUserInfo`或两者。它只会保留最多 `max-size 个条目。当添加新条目时,如果缓存已满,则会尝试通过删除单个过期的条目来寻找空间。此外,如果激活了清理计时器,则它会定期检查过期的条目并将其删除。

The default cache uses a token as a key, and each entry can have TokenIntrospection, UserInfo, or both. It will only keep up to a max-size number of entries. If the cache is already full when a new entry is to be added, an attempt is made to find a space by removing a single expired entry. Additionally, the cleanup timer, if activated, periodically checks for expired entries and removes them.

您可以试用默认的缓存实现或注册自定义缓存。

You can experiment with the default cache implementation or register a custom one.

JSON Web Token claim verification

在验证承载人 JWT 令牌的签名并检查其 expires at (exp) 声明后,接下来将验证 iss (issuer) 声明值。

After the bearer JWT token’s signature has been verified and its expires at (exp) claim has been checked, the iss (issuer) claim value is verified next.

默认情况下,会将 iss 声明值与已知提供程序配置中可能已发现的 issuer 属性进行比较。但是,如果设置了 quarkus.oidc.token.issuer 属性,则会将 iss 声明值与之进行比较。

By default, the iss claim value is compared to the issuer property, which might have been discovered in the well-known provider configuration. However, if the quarkus.oidc.token.issuer property is set, then the iss claim value is compared to it instead.

在某些情况下,此 iss 声明验证可能无法正常工作。例如,如果发现的 issuer 属性包含一个内部 HTTP/IP 地址,而令牌 iss 声明值包含一个外部 HTTP/IP 地址。或者当发现的 issuer 属性包含模板租户变量时,而令牌 iss 声明值具有完整的租户特定的颁发者值。

In some cases, this iss claim verification might not work. For example, if the discovered issuer property contains an internal HTTP/IP address while the token iss claim value contains an external HTTP/IP address. Or when a discovered issuer property contains the template tenant variable, but the token iss claim value has the complete tenant-specific issuer value.

在这些情况下,请考虑通过设置 quarkus.oidc.token.issuer=any 来跳过颁发者验证。仅在没有其他选项可用时才跳过颁发者验证:

In such cases, consider skipping the issuer verification by setting quarkus.oidc.token.issuer=any. Only skip the issuer verification if no other options are available:

  • If you are using Keycloak and observe the issuer verification errors caused by the different host addresses, configure Keycloak with a KEYCLOAK_FRONTEND_URL property to ensure the same host address is used.

  • If the iss property is tenant-specific in a multitenant deployment, use the SecurityIdentity tenant-id attribute to check that the issuer is correct in the endpoint or the custom Jakarta filter. For example:

import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;

import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.security.identity.SecurityIdentity;

@Provider
public class IssuerValidator implements ContainerRequestFilter {
    @Inject
    OidcConfigurationMetadata configMetadata;

    @Inject JsonWebToken jwt;
    @Inject SecurityIdentity identity;

    public void filter(ContainerRequestContext requestContext) {
        String issuer = configMetadata.getIssuer().replace("{tenant-id}", identity.getAttribute("tenant-id"));
        if (!issuer.equals(jwt.getIssuer())) {
            requestContext.abortWith(Response.status(401).build());
        }
    }
}

考虑使用 quarkus.oidc.token.audience 属性来验证令牌 aud (audience) 声明值。

Consider using the quarkus.oidc.token.audience property to verify the token aud (audience) claim value.

Jose4j Validator

在初始化 org.eclipse.microprofile.jwt.JsonWebToken 之前,您可以注册一个自定义 Jose4j Validator 以自定义 JWT 声明验证流程。例如:

You can register a custom Jose4j Validator to customize the JWT claim verification process, before org.eclipse.microprofile.jwt.JsonWebToken is initialized. For example:

package org.acme.security.openid.connect;

import static org.eclipse.microprofile.jwt.Claims.iss;

import io.quarkus.arc.Unremovable;
import jakarta.enterprise.context.ApplicationScoped;

import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.JwtContext;
import org.jose4j.jwt.consumer.Validator;

@Unremovable
@ApplicationScoped
public class IssuerValidator implements Validator { 1

    @Override
    public String validate(JwtContext jwtContext) throws MalformedClaimException {
        if (jwtContext.getJwtClaims().hasClaim(iss.name())
                && "my-issuer".equals(jwtContext.getJwtClaims().getClaimValueAsString(iss.name()))) {
            return "wrong issuer"; 2
        }
        return null; 3
    }
}
1 Register Jose4j Validator to verify JWT tokens for all OIDC tenants.
2 Return the claim verification error description.
3 Return null to confirm that this Validator has successfully verified the token.

使用 @quarkus.oidc.TenantFeature 注释将自定义验证器仅绑定到特定 OIDC 租户。

Use a @quarkus.oidc.TenantFeature annotation to bind a custom Validator to a specific OIDC tenant only.

Single-page applications

单页应用程序 (SPA) 通常使用 XMLHttpRequest(XHR) 和 OIDC 提供商提供的 JavaScript 实用工具代码获取承载令牌来访问 Quarkus `service`应用程序。

A single-page application (SPA) typically uses XMLHttpRequest(XHR) and the JavaScript utility code provided by the OIDC provider to acquire a bearer token to access Quarkus service applications.

例如,如果你使用 Keycloak,你可以用 `keycloak.js`对用户进行身份验证,并且刷新 SPA 中过期的令牌:

For example, if you work with Keycloak, you can use keycloak.js to authenticate users and refresh the expired tokens from the SPA:

<html>
<head>
    <title>keycloak-spa</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="http://localhost:8180/js/keycloak.js"></script>
    <script>
        var keycloak = new Keycloak();
        keycloak.init({onLoad: 'login-required'}).success(function () {
            console.log('User is now authenticated.');
        }).error(function () {
            window.location.reload();
        });
        function makeAjaxRequest() {
            axios.get("/api/hello", {
                headers: {
                    'Authorization': 'Bearer ' + keycloak.token
                }
            })
            .then( function (response) {
                console.log("Response: ", response.status);
            }).catch(function (error) {
                console.log('refreshing');
                keycloak.updateToken(5).then(function () {
                    console.log('Token refreshed');
                }).catch(function () {
                    console.log('Failed to refresh token');
                    window.location.reload();
                });
            });
    }
    </script>
</head>
<body>
    <button onclick="makeAjaxRequest()">Request</button>
</body>
</html>

Cross-origin resource sharing

如果你计划在你运行于不同域的单页应用程序中使用你的 OIDC `service`应用程序,你必须配置跨源资源共享 (CORS)。更多信息,请参阅“跨源资源共享”指南的 CORS filter部分。

If you plan to use your OIDC service application from a single-page application running on a different domain, you must configure cross-origin resource sharing (CORS). For more information, see the CORS filter section of the "Cross-origin resource sharing" guide.

Provider endpoint configuration

一个 OIDC service`应用程序需要知道 OIDC 提供商的令牌、`JsonWebKey (JWK) 集合,可能还有 `UserInfo`和内省终结点地址。

An OIDC service application needs to know the OIDC provider’s token, JsonWebKey (JWK) set, and possibly UserInfo and introspection endpoint addresses.

默认情况下,它们是通过向配置的 `quarkus.oidc.auth-server-url`添加一个 `/.well-known/openid-configuration`路径而被发现的。

By default, they are discovered by adding a /.well-known/openid-configuration path to the configured quarkus.oidc.auth-server-url.

此外,如果发现终结点不可用,或者如果你想节约发现终结点往返时间,你可以禁用发现并使用相对路径值对其进行配置。例如:

Alternatively, if the discovery endpoint is not available, or if you want to save on the discovery endpoint round-trip, you can disable the discovery and configure them with relative path values. For example:

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.discovery-enabled=false
# Token endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/token
quarkus.oidc.token-path=/protocol/openid-connect/token
# JWK set endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/certs
quarkus.oidc.jwks-path=/protocol/openid-connect/certs
# UserInfo endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/userinfo
quarkus.oidc.user-info-path=/protocol/openid-connect/userinfo
# Token Introspection endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/tokens/introspect
quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect

Token propagation

有关承载访问令牌传播到下游服务的信息,请参阅 Quarkus“OpenID Connect (OIDC) 和 OAuth2 客户端及过滤器参考”指南的 Token propagation部分。

For information about bearer access token propagation to the downstream services, see the Token propagation section of the Quarkus "OpenID Connect (OIDC) and OAuth2 client and filters reference" guide.

JWT token certificate chain

在某些情况下,JWT 承载令牌有一个 `x5c`头,它表示一条 X509 证书链,其末级证书包含一个必须用来验证该令牌签名的公钥。在接受该公钥来验证签名之前,必须首先验证证书链。证书链验证涉及几个步骤:

In some cases, JWT bearer tokens have an x5c header which represents an X509 certificate chain whose leaf certificate contains a public key that must be used to verify this token’s signature. Before this public key can be accepted to verify the signature, the certificate chain must be validated first. The certificate chain validation involves several steps:

  1. Confirm that every certificate but the root one is signed by the parent certificate.

  2. Confirm the chain’s root certificate is also imported in the truststore.

  3. Validate the chain’s leaf certificate. If a common name of the leaf certificate is configured then a common name of the chain’s leaf certificate must match it. Otherwise the chain’s leaf certificate must also be avaiable in the truststore, unless one or more custom TokenCertificateValidator implementations are registered.

  4. quarkus.oidc.TokenCertificateValidator can be used to add a custom certificate chain validation step. It can be used by all tenants expecting tokens with the certificate chain or bound to specific OIDC tenants with the @quarkus.oidc.TenantFeature annotation.

例如,以下是如何配置 Quarkus OIDC 来验证令牌证书链,而无需使用 quarkus.oidc.TokenCertificateValidator

For example, here is how you can configure Quarkus OIDC to verify the token’s certificate chain, without using quarkus.oidc.TokenCertificateValidator:

quarkus.oidc.certificate-chain.trust-store-file=truststore-rootcert.p12 1
quarkus.oidc.certificate-chain.trust-store-password=storepassword
quarkus.oidc.certificate-chain.leaf-certificate-name=www.quarkusio.com 2
1 The truststore must contain the certificate chain’s root certificate.
2 The certificate chain’s leaf certificate must have a common name equal to www.quarkusio.com. If this property is not configured then the truststore must contain the certificate chain’s leaf certificate unless one or more custom TokenCertificateValidator implementations are registered.

你可以通过注册自定义 `quarkus.oidc.TokenCertificateValidator`来添加自定义证书链验证步骤,例如:

You can add a custom certificate chain validation step by registering a custom quarkus.oidc.TokenCertificateValidator, for example:

package io.quarkus.it.keycloak;

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenCertificateValidator;
import io.quarkus.oidc.runtime.TrustStoreUtils;
import io.vertx.core.json.JsonObject;

@ApplicationScoped
@Unremovable
public class BearerGlobalTokenChainValidator implements TokenCertificateValidator {

    @Override
    public void validate(OidcTenantConfig oidcConfig, List<X509Certificate> chain, String tokenClaims) throws CertificateException {
        String rootCertificateThumbprint = TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1));
        JsonObject claims = new JsonObject(tokenClaims);
        if (!rootCertificateThumbprint.equals(claims.getString("root-certificate-thumbprint"))) { 1
            throw new CertificateException("Invalid root certificate");
        }
    }
}
1 Confirm that the certificate chain’s root certificate is bound to the custom JWT token’s claim.

OIDC provider client authentication

当需要向 OIDC 提供商发出远程请求时,则使用 quarkus.oidc.runtime.OidcProviderClient。如果必须内省承载令牌,则 `OidcProviderClient`必须对 OIDC 提供商进行身份验证。有关支持的身份验证选项的更多信息,请参阅 Quarkus“保护 Web 应用程序的 OpenID Connect 授权代码流机制”指南的 OIDC provider client authentication部分。

quarkus.oidc.runtime.OidcProviderClient is used when a remote request to an OIDC provider is required. If introspection of the Bearer token is necessary, then OidcProviderClient must authenticate to the OIDC provider. For more information about supported authentication options, see the OIDC provider client authentication section in the Quarkus "OpenID Connect authorization code flow mechanism for protecting web applications" guide.

Testing

如果你必须测试需要 Keycloak authorization的 Quarkus OIDC 服务终结点,请按照 Test Keycloak authorization部分进行操作。

If you have to test Quarkus OIDC service endpoints that require Keycloak authorization, follow the Test Keycloak authorization section.

你可以通过向测试项目中添加以下依赖项,开始测试:

You can begin testing by adding the following dependencies to your test project:

pom.xml
<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-junit5")

WireMock

向测试项目中添加以下依赖项:

Add the following dependencies to your test project:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-oidc-server</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-test-oidc-server")

准备 REST 测试端点,并设置 application.properties 。例如:

Prepare the REST test endpoint and set application.properties. For example:

# keycloak.url is set by OidcWiremockTestResource
quarkus.oidc.auth-server-url=${keycloak.url:replaced-by-test-resource}/realms/quarkus/
quarkus.oidc.client-id=quarkus-service-app
quarkus.oidc.application-type=service

最后,编写测试代码。例如:

Finally, write the test code. For example:

import static org.hamcrest.Matchers.equalTo;

import java.util.Set;

import org.junit.jupiter.api.Test;

import io.quarkus.test.common.WithTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.server.OidcWiremockTestResource;
import io.restassured.RestAssured;
import io.smallrye.jwt.build.Jwt;

@QuarkusTest
@WithTestResource(OidcWiremockTestResource.class)
public class BearerTokenAuthorizationTest {

    @Test
    public void testBearerToken() {
        RestAssured.given().auth().oauth2(getAccessToken("alice", Set.of("user")))
            .when().get("/api/users/me")
            .then()
            .statusCode(200)
            // The test endpoint returns the name extracted from the injected `SecurityIdentity` principal.
            .body("userName", equalTo("alice"));
    }

    private String getAccessToken(String userName, Set<String> groups) {
        return Jwt.preferredUserName(userName)
                .groups(groups)
                .issuer("https://server.example.com")
                .audience("https://service.example.com")
                .sign();
    }
}

quarkus-test-oidc-server 扩展包括一个采用 JSON Web Key (JWK) 格式的签名 RSA 私有密钥文件,并使用 smallrye.jwt.sign.key.location 配置属性指向它。它允许你使用无参数 sign() 操作签署令牌。

The quarkus-test-oidc-server extension includes a signing RSA private key file in a JSON Web Key (JWK) format and points to it with a smallrye.jwt.sign.key.location configuration property. It allows you to sign the token by using a no-argument sign() operation.

使用 OidcWiremockTestResource 测试你的 quarkus-oidc service 应用程序,可以提供最佳覆盖率,因为即使通信通道也能针对 WireMock HTTP 存根进行测试。如果你需要使用 OidcWiremockTestResource 尚未支持的 WireMock 存根运行测试,你可以将 WireMockServer 实例注入到测试类中,如下例所示:

Testing your quarkus-oidc service application with OidcWiremockTestResource provides the best coverage because even the communication channel is tested against the WireMock HTTP stubs. If you need to run a test with WireMock stubs that are not yet supported by OidcWiremockTestResource, you can inject a WireMockServer instance into the test class, as shown in the following example:

OidcWiremockTestResource 不能配合 @QuarkusIntegrationTest 用于针对 Docker 容器的测试,因为 WireMock 服务器运行在运行测试的 JVM 中,而 Docker 容器运行的 Quarkus 应用程序无法访问该 JVM。

OidcWiremockTestResource does not work with @QuarkusIntegrationTest against Docker containers because the WireMock server runs in the JVM that runs the test, which is inaccessible from the Docker container that runs the Quarkus application.

package io.quarkus.it.keycloak;

import static com.github.tomakehurst.wiremock.client.WireMock.matching;
import static org.hamcrest.Matchers.equalTo;

import org.junit.jupiter.api.Test;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.server.OidcWireMock;
import io.restassured.RestAssured;

@QuarkusTest
public class CustomOidcWireMockStubTest {

    @OidcWireMock
    WireMockServer wireMockServer;

    @Test
    public void testInvalidBearerToken() {
        wireMockServer.stubFor(WireMock.post("/auth/realms/quarkus/protocol/openid-connect/token/introspect")
                .withRequestBody(matching(".*token=invalid_token.*"))
                .willReturn(WireMock.aResponse().withStatus(400)));

        RestAssured.given().auth().oauth2("invalid_token").when()
                .get("/api/users/me/bearer")
                .then()
                .statusCode(401)
                .header("WWW-Authenticate", equalTo("Bearer"));
    }
}

OidcTestClient

如果你使用 SaaS OIDC 提供程序(如 Auth0),并且想针对测试(开发)域名运行测试,或针对远程 keycloak 测试领域运行测试,则可以在已经配置 quarkus.oidc.auth-server-url 的前提下使用 OidcTestClient

If you use SaaS OIDC providers, such as Auth0, and want to run tests against the test (development) domain or to run tests against a remote Keycloak test realm, if you already have quarkus.oidc.auth-server-url configured, you can use OidcTestClient.

例如,你采用了以下配置:

For example, you have the following configuration:

%test.quarkus.oidc.auth-server-url=https://dev-123456.eu.auth0.com/
%test.quarkus.oidc.client-id=test-auth0-client
%test.quarkus.oidc.credentials.secret=secret

开始时,添加相同的依赖项 quarkus-test-oidc-server,如 WireMock 部分中所述。

To start, add the same dependency, quarkus-test-oidc-server, as described in the WireMock section.

接下来,按如下方式编写测试代码:

Next, write the test code as follows:

package org.acme;

import org.junit.jupiter.api.AfterAll;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

import java.util.Map;

import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.client.OidcTestClient;

@QuarkusTest
public class GreetingResourceTest {

    static OidcTestClient oidcTestClient = new OidcTestClient();

    @AfterAll
    public static void close() {
        oidcTestClient.close();
    }

    @Test
    public void testHelloEndpoint() {
        given()
          .auth().oauth2(getAccessToken("alice", "alice"))
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("Hello, Alice"));
    }

    private String getAccessToken(String name, String secret) {
        return oidcTestClient.getAccessToken(name, secret,
            Map.of("audience", "https://dev-123456.eu.auth0.com/api/v2/",
	           "scope", "profile"));
    }
}

此测试代码使用测试 Auth0 域中的 password 授权,从该域获取令牌,该域已使用客户端 ID test-auth0-client 注册了应用程序,并使用密码 alice 创建了用户 alice。此类测试要起作用,测试 Auth0 应用程序必须启用 password 授权。此示例代码还展示了如何传递其他参数。对于 Auth0 来说,这些是 audiencescope 参数。

This test code acquires a token by using a password grant from the test Auth0 domain, which has registered an application with the client id test-auth0-client, and created the user alice with password alice. For a test like this to work, the test Auth0 application must have the password grant enabled. This example code also shows how to pass additional parameters. For Auth0, these are the audience and scope parameters.

Dev Services for Keycloak

针对 Keycloak 进行集成测试的首选方法为 Dev Services for KeycloakDev Services for Keycloak 将启动和初始化一个测试容器。然后,它将创建一个 quarkus 领域和一个 quarkus-app 客户端 (secret 机密),并添加 alice (adminuser 角色) 和 bob (user 角色) 用户,所有这些属性都可自定义。

The preferred approach for integration testing against Keycloak is Dev Services for Keycloak. Dev Services for Keycloak will start and initialize a test container. Then, it will create a quarkus realm and a quarkus-app client (secret secret) and add alice (admin and user roles) and bob (user role) users, where all of these properties can be customized.

首先,添加以下依赖项,它提供了一个实用程序类 io.quarkus.test.keycloak.client.KeycloakTestClient,可以在测试中使用它来获取访问令牌:

First, add the following dependency, which provides a utility class io.quarkus.test.keycloak.client.KeycloakTestClient that you can use in tests for acquiring the access tokens:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-keycloak-server</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-test-keycloak-server")

接下来,准备 application.properties 配置文件。你可以从一个空 application.properties 文件开始,因为 Dev Services for Keycloak 会注册 quarkus.oidc.auth-server-url,并将其指向运行中的测试容器 quarkus.oidc.client-id=quarkus-appquarkus.oidc.credentials.secret=secret

Next, prepare your application.properties configuration file. You can start with an empty application.properties file because Dev Services for Keycloak registers quarkus.oidc.auth-server-url and points it to the running test container, quarkus.oidc.client-id=quarkus-app, and quarkus.oidc.credentials.secret=secret.

然而,如果你已配置必需的 quarkus-oidc 属性,那么你只需要将 quarkus.oidc.auth-server-urlprod 配置关联,以便为 Keycloak 启动一个容器,如下例所示:

However, if you have already configured the required quarkus-oidc properties, then you only need to associate quarkus.oidc.auth-server-url with the prod profile for `Dev Services for Keycloak`to start a container, as shown in the following example:

%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus

如果在运行测试前需要将一个自定义领域文件导入 Keycloak,请按如下方式配置 Dev Services for Keycloak

If a custom realm file has to be imported into Keycloak before running the tests, configure Dev Services for Keycloak as follows:

%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.keycloak.devservices.realm-path=quarkus-realm.json

最后,编写你的测试,它将在 JVM 模式中执行,如下例所示:

Finally, write your test, which will be executed in JVM mode, as shown in the following examples:

Example of a test executed in JVM mode:
package org.acme.security.openid.connect;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.keycloak.client.KeycloakTestClient;
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;

@QuarkusTest
public class BearerTokenAuthenticationTest {

    KeycloakTestClient keycloakClient = new KeycloakTestClient();

    @Test
    public void testAdminAccess() {
        RestAssured.given().auth().oauth2(getAccessToken("alice"))
                .when().get("/api/admin")
                .then()
                .statusCode(200);
        RestAssured.given().auth().oauth2(getAccessToken("bob"))
                .when().get("/api/admin")
                .then()
                .statusCode(403);
    }

    protected String getAccessToken(String userName) {
        return keycloakClient.getAccessToken(userName);
    }
}
Example of a test executed in native mode:
package org.acme.security.openid.connect;

import io.quarkus.test.junit.QuarkusIntegrationTest;

@QuarkusIntegrationTest
public class NativeBearerTokenAuthenticationIT extends BearerTokenAuthenticationTest {
}

有关为 Keycloak 初始化和配置 Dev Services 的更多信息,请参阅 Dev Services for Keycloak 指南。

For more information about initializing and configuring Dev Services for Keycloak, see the Dev Services for Keycloak guide.

KeycloakTestResourceLifecycleManager

你也可以使用 KeycloakTestResourceLifecycleManager 与 Keycloak 进行集成测试。

You can also use KeycloakTestResourceLifecycleManager for integration testing with Keycloak.

除非你有特殊需求需要使用 KeycloakTestResourceLifecycleManager,否则,在与 Keycloak 进行集成测试时,使用 Dev Services for Keycloak 来替代 KeycloakTestResourceLifecycleManager

Use bearer-token-integration-testing-keycloak-devservices instead of KeycloakTestResourceLifecycleManager for integration testing with Keycloak, unless you have specific requirements for using KeycloakTestResourceLifecycleManager.

首先,添加以下依赖关系:

First, add the following dependency:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-keycloak-server</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-test-keycloak-server")

它提供了 io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager,这是一个实现 `io.quarkus.test.common.QuarkusTestResourceLifecycleManager`的 Keycloak 容器启动。

It provides io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager, which is an implementation of io.quarkus.test.common.QuarkusTestResourceLifecycleManager that starts a Keycloak container.

像下面一样配置 Maven Surefire 插件,或使用 maven.failsafe.plugin 类似地进行本机映像测试:

Configure the Maven Surefire plugin as follows, or similarly with maven.failsafe.plugin for native image testing:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <systemPropertyVariables>
            <!-- Or, alternatively, configure 'keycloak.version' -->
            <keycloak.docker.image>${keycloak.docker.image}</keycloak.docker.image>
            <!--
              Disable HTTPS if required:
              <keycloak.use.https>false</keycloak.use.https>
            -->
        </systemPropertyVariables>
    </configuration>
</plugin>

准备 REST 测试端点并设置 application.properties,如下例所示:

Prepare the REST test endpoint and set application.properties as outlined in the following example:

# keycloak.url is set by KeycloakTestResourceLifecycleManager
quarkus.oidc.auth-server-url=${keycloak.url:replaced-by-test-resource}/realms/quarkus/
quarkus.oidc.client-id=quarkus-service-app
quarkus.oidc.credentials=secret
quarkus.oidc.application-type=service

最后,编写测试代码。例如:

Finally, write the test code. For example:

import static io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager.getAccessToken;
import static org.hamcrest.Matchers.equalTo;

import org.junit.jupiter.api.Test;

import io.quarkus.test.common.WithTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
import io.restassured.RestAssured;

@QuarkusTest
@WithTestResource(KeycloakTestResourceLifecycleManager.class)
public class BearerTokenAuthorizationTest {

    @Test
    public void testBearerToken() {
        RestAssured.given().auth().oauth2(getAccessToken("alice"))
            .when().get("/api/users/preferredUserName")
            .then()
            .statusCode(200)
            // The test endpoint returns the name extracted from the injected SecurityIdentity Principal
            .body("userName", equalTo("alice"));
    }

}
Summary

在提供的示例中, KeycloakTestResourceLifecycleManager 注册了两个用户: aliceadmin。默认情况下:*用户 alice 拥有 user 角色,你可以使用 keycloak.token.user-roles 系统属性进行自定义。*用户 admin 拥有 useradmin 角色,你可以使用 keycloak.token.admin-roles 系统属性进行自定义。

Summary

In the provided example, KeycloakTestResourceLifecycleManager registers two users: alice and admin. By default: * The user alice has the user role, which you can customize by using a keycloak.token.user-roles system property. * The user admin has both the user and admin roles, which you can customize by using the keycloak.token.admin-roles system property.

默认情况下, KeycloakTestResourceLifecycleManager 使用 HTTPS 初始化 Keycloak 实例,你可以使用 keycloak.use.https=false 来禁用它。默认域名称是 quarkus,客户端 ID 是 quarkus-service-app。如果你想自定义这些值,请设置 keycloak.realmkeycloak.service.client 系统属性。

By default, KeycloakTestResourceLifecycleManager uses HTTPS to initialize a Keycloak instance, and this can be disabled by using keycloak.use.https=false. The default realm name is quarkus, and the client id is quarkus-service-app. If you want to customize these values, set the keycloak.realm and keycloak.service.client system properties.

Local public key

你可以使用本地内嵌公钥来测试你的 quarkus-oidc service 应用程序,如下例所示:

You can use a local inlined public key for testing your quarkus-oidc service applications, as shown in the following example:

quarkus.oidc.client-id=test
quarkus.oidc.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB

smallrye.jwt.sign.key.location=/privateKey.pem

要生成 JWT token,从 main Quarkus 存储库中的 integration-tests/oidc-tenancy 复制 privateKey.pem,并使用类似于前面 WireMock 部分中的测试代码。如果你愿意,可以使用自己的测试密钥。

To generate JWT tokens, copy privateKey.pem from the integration-tests/oidc-tenancy in the main Quarkus repository and use a test code similar to the one in the preceding WireMock section. You can use your own test keys, if preferred.

与 WireMock 方法相比,此方法提供的覆盖范围有限。例如,远程通信代码未涵盖。

This approach provides limited coverage compared to the WireMock approach. For example, the remote communication code is not covered.

TestSecurity annotation

你可以使用 @TestSecurity@OidcSecurity 注解来测试 service 应用程序端点代码,它依赖于以下三种注入中的任意一种,或全部三种:

You can use @TestSecurity and @OidcSecurity annotations to test the service application endpoint code, which depends on either one, or all three, of the following injections:

  • JsonWebToken

  • UserInfo

  • OidcConfigurationMetadata

首先,添加以下依赖关系:

First, add the following dependency:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-security-oidc</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-test-security-oidc")

按照以下示例编写测试代码:

Write a test code as outlined in the following example:

import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.oidc.Claim;
import io.quarkus.test.security.oidc.ConfigMetadata;
import io.quarkus.test.security.oidc.OidcSecurity;
import io.quarkus.test.security.oidc.OidcConfigurationMetadata;
import io.quarkus.test.security.oidc.UserInfo;
import io.restassured.RestAssured;

@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
public class TestSecurityAuthTest {

    @Test
    @TestSecurity(user = "userOidc", roles = "viewer")
    public void testOidc() {
        RestAssured.when().get("test-security-oidc").then()
                .body(is("userOidc:viewer"));
    }

    @Test
    @TestSecurity(user = "userOidc", roles = "viewer")
    @OidcSecurity(claims = {
            @Claim(key = "email", value = "user@gmail.com")
    }, userinfo = {
            @UserInfo(key = "sub", value = "subject")
    }, config = {
            @ConfigMetadata(key = "issuer", value = "issuer")
    })
    public void testOidcWithClaimsUserInfoAndMetadata() {
        RestAssured.when().get("test-security-oidc-claims-userinfo-metadata").then()
                .body(is("userOidc:viewer:user@gmail.com:subject:issuer"));
    }

}

本代码示例中使用的 ProtectedResource 类可能如下所示:

The ProtectedResource class, which is used in this code example, might look like this:

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

import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.Authenticated;

import org.eclipse.microprofile.jwt.JsonWebToken;

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

    @Inject
    JsonWebToken accessToken;
    @Inject
    UserInfo userInfo;
    @Inject
    OidcConfigurationMetadata configMetadata;

    @GET
    @Path("test-security-oidc")
    public String testSecurityOidc() {
        return accessToken.getName() + ":" + accessToken.getGroups().iterator().next();
    }

    @GET
    @Path("test-security-oidc-claims-userinfo-metadata")
    public String testSecurityOidcWithClaimsUserInfoMetadata() {
        return accessToken.getName() + ":" + accessToken.getGroups().iterator().next()
                + ":" + accessToken.getClaim("email")
                + ":" + userInfo.getString("sub")
                + ":" + configMetadata.get("issuer");
    }
}

你必须始终使用 @TestSecurity 注解。它的 user 属性返回 JsonWebToken.getName(),它的 roles 属性返回 JsonWebToken.getGroups()@OidcSecurity 注解是可选的,你可以使用它设置其他 token 声明以及 UserInfoOidcConfigurationMetadata 属性。此外,如果 quarkus.oidc.token.issuer 属性已配置,则它将用作 OidcConfigurationMetadata issuer 属性值。

You must always use the @TestSecurity annotation. Its user property is returned as JsonWebToken.getName() and its roles property is returned as JsonWebToken.getGroups(). The @OidcSecurity annotation is optional and you can use it to set the additional token claims and the UserInfo and OidcConfigurationMetadata properties. Additionally, if the quarkus.oidc.token.issuer property is configured, it is used as an OidcConfigurationMetadata issuer property value.

如果你使用不透明 token,可以按照以下代码示例进行测试:

If you work with opaque tokens, you can test them as shown in the following code example:

import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.oidc.OidcSecurity;
import io.quarkus.test.security.oidc.TokenIntrospection;
import io.restassured.RestAssured;

@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
public class TestSecurityAuthTest {

    @Test
    @TestSecurity(user = "userOidc", roles = "viewer")
    @OidcSecurity(introspectionRequired = true,
        introspection = {
            @TokenIntrospection(key = "email", value = "user@gmail.com")
        }
    )
    public void testOidcWithClaimsUserInfoAndMetadata() {
        RestAssured.when().get("test-security-oidc-claims-userinfo-metadata").then()
                .body(is("userOidc:viewer:userOidc:viewer"));
    }

}

本代码示例中使用的 ProtectedResource 类可能如下所示:

The ProtectedResource class, which is used in this code example, might look like this:

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

import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;

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

    @Inject
    SecurityIdentity securityIdentity;
    @Inject
    TokenIntrospection introspection;

    @GET
    @Path("test-security-oidc-opaque-token")
    public String testSecurityOidcOpaqueToken() {
        return securityIdentity.getPrincipal().getName() + ":" + securityIdentity.getRoles().iterator().next()
            + ":" + introspection.getString("username")
            + ":" + introspection.getString("scope")
            + ":" + introspection.getString("email");
    }
}

@TestSecurityuser、和 roles 属性可用作 TokenIntrospectionusername、和 scope 属性。使用 io.quarkus.test.security.oidc.TokenIntrospection 添加其他 introspection 响应属性,例如 email,等等。

The @TestSecurity, user, and roles attributes are available as TokenIntrospection, username, and scope properties. Use io.quarkus.test.security.oidc.TokenIntrospection to add the additional introspection response properties, such as an email, and so on.

可以将 @TestSecurity@OidcSecurity 组合在一个元注解中,如下例所示:

@TestSecurity and @OidcSecurity can be combined in a meta-annotation, as outlined in the following example:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD })
    @TestSecurity(user = "userOidc", roles = "viewer")
    @OidcSecurity(introspectionRequired = true,
        introspection = {
            @TokenIntrospection(key = "email", value = "user@gmail.com")
        }
    )
    public @interface TestSecurityMetaAnnotation {

    }

如果多个测试方法必须使用同一组安全设置,这将特别有用。

This is particularly useful if multiple test methods must use the same set of security settings.

Check errors in the logs

要查看令牌验证错误的更多详细信息,请启用 @[3] 和 @[4] 级别日志记录:

To see more details about token verification errors, enable io.quarkus.oidc.runtime.OidcProvider and TRACE level logging:

quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".min-level=TRACE

要查看 @[5] 客户端初始化错误的更多详细信息,请启用 @[6] 和 @[7] 级别日志记录,如下所示:

To see more details about OidcProvider client initialization errors, enable io.quarkus.oidc.runtime.OidcRecorder and TRACE level logging as follows:

quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".min-level=TRACE

External and internal access to OIDC providers

OIDC 提供商的外部分发令牌和其他端点与自动发现的 URL 或相对于 @[8] 内部 URL 配置的 URL,可能具有不同的 HTTP(S) URL。例如,假设您的 SPA 从外部令牌端点地址获取令牌,并将其作为持有者令牌发送到 Quarkus。在这种情况下,端点可能会报告颁发者验证失败。

The externally-accessible token of the OIDC provider and other endpoints might have different HTTP(S) URLs compared to the URLs that are auto-discovered or configured relative to the quarkus.oidc.auth-server-url internal URL. For example, suppose your SPA acquires a token from an external token endpoint address and sends it to Quarkus as a bearer token. In that case, the endpoint might report an issuer verification failure.

在这些情况下,如果您使用 Keycloak,请使用 @[9] 系统属性将其设置为外部可访问的 URL。如果您使用其他 OIDC 提供商,请参阅您的提供商的文档。

In such cases, if you work with Keycloak, start it with the KEYCLOAK_FRONTEND_URL system property set to the externally accessible base URL. If you work with other OIDC providers, refer to your provider’s documentation.

Using the client-id property

@[10] 属性标识请求当前持有者令牌的 OIDC 客户端。OIDC 客户端可以是在浏览器中运行的 SPA 应用程序或将访问令牌传播到 Quarkus @[11] 机密客户端应用程序的 Quarkus @[12] 应用程序。

The quarkus.oidc.client-id property identifies the OIDC client that requested the current bearer token. The OIDC client can be an SPA application running in a browser or a Quarkus web-app confidential client application propagating the access token to the Quarkus service application.

如果 @[13] 应用程序预期远程检查令牌(对于不透明令牌始终如此),则此属性是必需的。对于本地 JSON Web 令牌 (JWT) 验证,此属性是可选的。

This property is required if the service application is expected to introspect the tokens remotely, which is always the case for the opaque tokens. This property is optional for local JSON Web Token (JWT) verification only.

即使端点不需要访问远程 introspection 端点,也建议设置 @[14] 属性。这是因为在设置 @[15] 时,它可用于验证令牌受众。当令牌验证失败时,它也会包含在日志中,从而可以更好地追溯颁发给特定客户端的令牌,并在较长时间内进行分析。

Setting the quarkus.oidc.client-id property is encouraged even if the endpoint does not require access to the remote introspection endpoint. This is because when client-id is set, it can be used to verify the token audience. It will also be included in logs when the token verification fails, enabling better traceability of tokens issued to specific clients and analysis over a longer period.

例如,如果您的 OIDC 提供商设置了令牌受众,请考虑以下配置模式:

For example, if your OIDC provider sets a token audience, consider the following configuration pattern:

# Set client-id
quarkus.oidc.client-id=quarkus-app
# Token audience claim must contain 'quarkus-app'
quarkus.oidc.token.audience=${quarkus.oidc.client-id}

如果您设置了 @[16],但是您的端点不要求远程访问 OIDC 提供商端点(自省、令牌获取等)之一,则不要使用 @[17] 或类似属性设置客户端机密,因为它将不会被使用。

If you set quarkus.oidc.client-id, but your endpoint does not require remote access to one of the OIDC provider endpoints (introspection, token acquisition, and so on), do not set a client secret with quarkus.oidc.credentials or similar properties because it will not be used.

Quarkus @[18] 应用程序始终需要 @[19] 属性。

Quarkus web-app applications always require the quarkus.oidc.client-id property.

Authentication after an HTTP request has completed

有时,当没有活动的 HTTP 请求上下文时,必须为给定令牌创建 @[20]。@[21] 扩展提供了 @[22],用于将令牌转换为 @[23] 实例。例如,HTTP 请求完成后必须验证令牌的一种情况是,您正在处理带 @[25] 的消息。下面的示例在不同的 CDI 请求上下文中使用“product-order”消息。因此,注入的 @[24] 不会正确地表示已验证的身份,并且是匿名的。

Sometimes, SecurityIdentity for a given token must be created when there is no active HTTP request context. The quarkus-oidc extension provides io.quarkus.oidc.TenantIdentityProvider to convert a token to a SecurityIdentity instance. For example, one situation when you must verify the token after the HTTP request has completed is when you are processing messages with Vert.x event bus. The example below uses the 'product-order' message within different CDI request contexts. Therefore, an injected SecurityIdentity would not correctly represent the verified identity and be anonymous.

package org.acme.quickstart.oidc;

import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;

import jakarta.inject.Inject;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import io.vertx.core.eventbus.EventBus;

@Path("order")
public class OrderResource {

    @Inject
    EventBus eventBus;

    @POST
    public void order(String product, @HeaderParam(AUTHORIZATION) String bearer) {
        String rawToken = bearer.substring("Bearer ".length()); 1
        eventBus.publish("product-order", new Product(product, rawToken));
    }

    public static class Product {
         public String product;
         public String customerAccessToken;
         public Product() {
         }
         public Product(String product, String customerAccessToken) {
             this.product = product;
             this.customerAccessToken = customerAccessToken;
         }
    }
}
1 At this point, the token is not verified when proactive authentication is disabled.
package org.acme.quickstart.oidc;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.Tenant;
import io.quarkus.oidc.TenantIdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.ConsumeEvent;
import io.smallrye.common.annotation.Blocking;

@ApplicationScoped
public class OrderService {

    @Tenant("tenantId")
    @Inject
    TenantIdentityProvider identityProvider;

    @Inject
    TenantIdentityProvider defaultIdentityProvider; 1

    @Blocking
    @ConsumeEvent("product-order")
    void processOrder(Product product) {
        AccessTokenCredential tokenCredential = new AccessTokenCredential(product.customerAccessToken);
        SecurityIdentity securityIdentity = identityProvider.authenticate(tokenCredential).await().indefinitely(); 2
        ...
    }

}
1 For the default tenant, the Tenant qualifier is optional.
2 Executes token verification and converts the token to a SecurityIdentity.

当在 HTTP 请求期间使用提供程序时,可以按 @[29] 指南所述解析租户配置。但是,当没有活动 HTTP 请求时,您必须使用 @[28] 限定符明确选择租户。

When the provider is used during an HTTP request, the tenant configuration can be resolved as described in the Using OpenID Connect Multi-Tenancy guide. However, when there is no active HTTP request, you must select the tenant explicitly with the io.quarkus.oidc.Tenant qualifier.

@[30] 目前不受支持。需要动态租户的身份验证将失败。

Dynamic tenant configuration resolution is currently not supported. Authentication that requires a dynamic tenant will fail.

OIDC request filters

您可以通过注册一个或多个 @[31] 实现来筛选 Quarkus 对 OIDC 提供商发出的 OIDC 请求,这些实现可以更新或添加新的请求头并记录请求。要了解更多信息,请参阅 @[32]。

You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more OidcRequestFilter implementations, which can update or add new request headers, and log requests. For more information, see OIDC request filters.