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:
-
The Quarkus service retrieves verification keys from the OIDC provider. The verification keys are used to verify the bearer access token signatures.
-
The Quarkus user accesses the single-page application (SPA).
-
The single-page application uses Authorization Code Flow to authenticate the user and retrieve tokens from the OIDC provider.
-
The single-page application uses the access token to retrieve the service data from the Quarkus service.
-
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.
-
The single-page application returns the same data to the Quarkus user.
-
The Quarkus service retrieves verification keys from the OIDC provider. The verification keys are used to verify the bearer access token signatures.
-
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. -
The client uses the access token to retrieve the service data from the Quarkus service.
-
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 touserinfo
orquarkus.oidc.token.verify-access-token-with-user-info
is set totrue
orquarkus.oidc.authentication.id-token-required
is set tofalse
, 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
orresource_access/client_id/roles
(whereclient_id
is the value of thequarkus.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=true
和 quarkus.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 令牌,则默认情况下,它将使用来自本地 JsonWebKeySet
的 JsonWebKey
(JWK) 密钥进行验证,该密钥是通过 OIDC 提供程序的 JWK 端点检索的。令牌的密钥标识符 (kid
) 头部值用于查找匹配的 JWK 密钥。如果本地没有可用的匹配 JWK
,则通过从 JWK 端点获取当前密钥集刷新 JsonWebKeySet
。JsonWebKeySet
刷新只能在 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.TokenIntrospectionCache
和 quarkus.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-cache
和 quarkus.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.TokenIntrospectionCache
和 quarkus.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
默认的缓存以令牌作为键,并且每个条目可以具有 TokenIntrospection
、UserInfo`或两者。它只会保留最多 `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 theSecurityIdentity
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());
}
}
}
考虑使用 Consider using the |
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. |
使用 |
Use a |
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:
-
Confirm that every certificate but the root one is signed by the parent certificate.
-
Confirm the chain’s root certificate is also imported in the truststore.
-
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. -
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:
<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>
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-junit5")
WireMock
向测试项目中添加以下依赖项:
Add the following dependencies to your test project:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-oidc-server</artifactId>
<scope>test</scope>
</dependency>
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:
|
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
来说,这些是 audience
和 scope
参数。
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 Keycloak 。Dev Services for Keycloak
将启动和初始化一个测试容器。然后,它将创建一个 quarkus
领域和一个 quarkus-app
客户端 (secret
机密),并添加 alice
(admin
和 user
角色) 和 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:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
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-app
和 quarkus.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-url
与 prod
配置关联,以便为 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:
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);
}
}
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:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
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"));
}
}
在提供的示例中, KeycloakTestResourceLifecycleManager
注册了两个用户: alice
和 admin
。默认情况下:*用户 alice
拥有 user
角色,你可以使用 keycloak.token.user-roles
系统属性进行自定义。*用户 admin
拥有 user
和 admin
角色,你可以使用 keycloak.token.admin-roles
系统属性进行自定义。
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.realm
和 keycloak.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:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-oidc</artifactId>
<scope>test</scope>
</dependency>
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 声明以及 UserInfo
和 OidcConfigurationMetadata
属性。此外,如果 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");
}
}
@TestSecurity
、 user
、和 roles
属性可用作 TokenIntrospection
、 username
、和 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.
可以将
如果多个测试方法必须使用同一组安全设置,这将特别有用。 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 |
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 |
@[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.