OpenID Connect (OIDC) Bearer token authentication
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 supports the Bearer token authentication mechanism through the Quarkus OpenID Connect (OIDC) extension.
The bearer tokens are issued by OIDC and OAuth 2.0 compliant authorization servers, such as Keycloak.
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.
The following diagrams outline the Bearer token authentication mechanism in Quarkus:

-
The Quarkus service retrieves verification keys from the OpenID Connect provider. The verification keys are used to verify the bearer access token signatures.
-
The Quarkus user accesses the Single-page application.
-
The Single-page application uses Authorization Code Flow to authenticate the user and retrieve tokens from the OpenID Connect 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 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 OpenID Connect 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 also requires client ID, secret, user name, and password to retrieve the access token from the OpenID Connect 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 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.
If you need to authenticate and authorize the users using OpenID Connect Authorization Code Flow, see OIDC code flow mechanism for protecting web applications. Also, if you use Keycloak and bearer tokens, see Using Keycloak to Centralize Authorization.
To learn about how you can protect service applications by using OIDC Bearer token authentication, see OIDC Bearer token authentication tutorial.
If you want to protect web applications by using OIDC authorization code flow authentication, see OIDC authorization code flow authentication.
For information about how to support multiple tenants, see Using OpenID Connect Multi-Tenancy.
访问JWT声明
如果你需要访问JWT令牌声明,那么你要注入 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
,更多细节请参见 JsonWebToken和声明所支持注入范围(Injection Scopes) 。
用户信息
Set quarkus.oidc.authentication.user-info-required=true
if a UserInfo JSON object from the OIDC userinfo endpoint has to be requested. A request will be sent to the OpenID Provider UserInfo endpoint and an io.quarkus.oidc.UserInfo
(a simple jakarta.json.JsonObject
wrapper) object will be created. io.quarkus.oidc.UserInfo
can be either injected or accessed as a SecurityIdentity userinfo
attribute.
配置元数据
当前租户发现的 OpenID Connect配置元数据 由 io.quarkus.oidc.OidcConfigurationMetadata
表示,可以作为 SecurityIdentity
configuration-metadata
属性注入或访问。
如果端点是公开的,则默认租户的 OidcConfigurationMetadata
会被注入。
令牌声明(Token Claims)和安全身份角色(SecurityIdentity Roles)
安全身份(SecurityIdentity)角色可以从经过验证的JWT访问令牌中映射出来,具体如下:
-
如果
quarkus.oidc.roles.role-claim-path
属性被设置,并且找到匹配的数组或字符串声明,那么角色将从这些声明中提取。例如,customroles
,customroles/array
,scope
,"http://namespace-qualified-custom-claim"/roles
,"http://namespace-qualified-roles"
, 等等。 -
如果存在
groups
声明,则这个声明的值会被使用 -
如果
realm_access/roles
或resource_access/client_id/roles
(其中client_id
是quarkus.oidc.client-id
属性的值)声明是存在的,那么它的值会被使用。该检查支持由Keycloak发行的令牌
如果令牌是不透明的(二进制),那么将使用来自远程令牌自省(token introspection)响应的 scope
属性。
如果使用UserInfo为角色的来源,那么要设置 quarkus.oidc.authentication.user-info-required=true
和 quarkus.oidc.roles.source=userinfo
,如果需要的话,设置 quarkus.oidc.roles.role-claim-path
。
Additionally, a custom SecurityIdentityAugmentor
can also be used to add the roles as documented in Security Identity Customization.
Token scopes And SecurityIdentity permissions
SecurityIdentity permissions are mapped in the form of the io.quarkus.security.StringPermission
from the scope parameter of the source of the roles, using the same claim separator.
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));
}
}
1 | Only requests with OpenID Connect scope email are going to be granted access. |
2 | The read access is limited to the client requests with scope orders_read . |
Please refer to the Permission annotation section of the Authorization of web endpoints guide for more information about the io.quarkus.security.PermissionsAllowed
annotation.
代币验证(Token Verification)和自省(Introspection)
如果令牌是 JWT 令牌,则默认情况下,将使用从 OpenID Connect 提供程序的 JWK 端点检索到的本地 JsonWebKeySet
中的 JsonWebKey
(JWK) 密钥对其进行验证。令牌的密钥标识符 kid
标头值( header value)将用于查找匹配的 JWK 密钥。 如果本地没有匹配的 JWK
可用,则 JsonWebKeySet
将通过从JWK端点获取当前密钥集来刷新。JsonWebKeySet
刷新只能在 quarkus.oidc.token.forced-jwk-refresh-interval
(默认值为10分钟)过期后重复。 如果在刷新后没有匹配的“JWK”可用,则 JWT 令牌将发送到 OpenID Connect 提供程序的令牌自检终结点。
如果令牌是不透明的(可以是二进制令牌或加密的JWT令牌),那么它将总是被发送到OpenID Connect提供者的令牌自省端点。
如果你只使用JWT令牌,并且期望一个匹配的 JsonWebKey
,那么你应该禁用令牌自省:
quarkus.oidc.token.allow-jwt-introspection=false
quarkus.oidc.token.allow-opaque-token-introspection=false
然而,在某些情况下,JWT令牌必须只通过自省来验证。它可以通过配置一个自省端点地址来强制进行,例如,在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
An advantage of this indirect enforcement of JWT tokens being only introspected remotely is that two remote call are avoided: a remote OIDC metadata discovery call followed by another remote call fetching the verification keys which will not be used, while its disavantage is that the users need to know the introspection endpoint address and configure it manually.
The alternative approach is to allow discovering the OIDC metadata (which is a default option) but require that only the remote JWT introspection is performed:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.token.require-jwt-introspection-only=true
An advantage of this approach is that the configuration is simple and easy to understand, while its disavantage is that a remote OIDC metadata discovery call is required to discover an introspection endpoint address (though the verification keys will also not be fetched).
Note that io.quarkus.oidc.TokenIntrospection
(a simple jakarta.json.JsonObject
wrapper) object will be created and can be either injected or accessed as a SecurityIdentity introspection
attribute if either JWT or opaque token has been successfully introspected.
Token Introspection and UserInfo Cache
所有不透明的、有时是JWT不记名的访问令牌都必须进行远程自省。如果还需要 UserInfo
,那么相同的访问令牌将被用来再次远程调用OpenID Connect Provider。因此,如果需要 UserInfo
,并且当前的访问令牌是不透明的,那么对于每一个这样的令牌,将进行两次远程调用—一次是反省,一次是用它来获取UserInfo,如果令牌是JWT,那么通常只需要一次远程调用—用它来获取UserInfo。
每一个传入的不记名流(bearer flow)或授权码流(code flow)访问令牌要进行多达2次的远程呼叫,其开销有时会是个问题。
如果在你的生产中有这种情况,那么可以建议将令牌自省和 UserInfo
数据缓存一小段时间,例如,3或5分钟。
quarkus-oidc
提供 quarkus.oidc.TokenIntrospectionCache
和 quarkus.oidc.UserInfoCache
接口,可用于实现 @ApplicationScoped
缓存实现,可用于存储和检索 quarkus.oidc.TokenIntrospection
和/或 quarkus.oidc.UserInfo
对象,例如:
@ApplicationScoped
@Alternative
@Priority(1)
public class CustomIntrospectionUserInfoCache implements TokenIntrospectionCache, UserInfoCache {
...
}
每个OIDC租户可以允许或拒绝存储其 quarkus.oidc.TokenIntrospection
和/或 quarkus.oidc.UserInfo
数据,其属性为布尔值 quarkus.oidc."tenant".allow-token-introspection-cache
和 quarkus.oidc."tenant".allow-user-info-cache
。
此外, quarkus-oidc
提供了一个简单的基于内存的默认令牌缓存,该缓存同时实现了 quarkus.oidc.TokenIntrospectionCache
和 quarkus.oidc.UserInfoCache
接口。
它可以按以下方式激活和配置:
# '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
。如果要添加一个新的条目时,缓存已经满了,那么将试图通过删除一个过期的条目来为它找到一个空间。此外,清理计时器,如果被激活,将定期检查过期的条目并将其删除。
请尝试使用默认的缓存实现或注册一个自定义的缓存。
JSON Web Token Claim Verification
一旦无记名JWT令牌的签名被验证,其 expires at
( exp
)声明会被检查,接下来也会验证 iss
( issuer
)声明的值。
默认情况下, iss
声明的值会与 issuer
属性进行比较,该属性有可能会在众所周知的提供者配置中找到。但是,如果 quarkus.oidc.token.issuer
属性被设置,那么 iss
声明的值将与它进行比较。
在某些情况下,这种 iss
声明验证可能不起作用。例如,如果发现的 issuer
属性包含一个内部 HTTP/IP地址,而令牌 iss
声明值包含一个外部 HTTP/IP地址。或者当发现的 issuer
属性包含模板租户变量,但令牌 iss
声明值有完整的租户特定发行人(tenant-specific issuer )的值。
在这种情况下,你可能要考虑通过设置 quarkus.oidc.token.issuer=any
,来跳过发行人验证。请注意,不建议这样做,除非没有其他选择,否则应避免这样做:
-
如果你使用Keycloak,并观察到由于不同的主机地址导致发行人验证错误,那么用
KEYCLOAK_FRONTEND_URL
属性配置Keycloak,以确保使用相同的主机地址。 -
If the
iss
property is tenant specific in a multi-tenant deployment then you can use theSecurityIdentity
tenant-id
attribute to check the issuer is correct in the endpoint itself or the custom Jakarta REST filter, for example:
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
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
)声明的值。
单页应用程序
单页应用程序(SPA)通常使用 XMLHttpRequest
(XHR)和OpenID Connect提供商提供的Java Script实用程序代码来获取不记名令牌,并使用它来访问Quarkus service
应用程序。
例如,以下是你如何使用 keycloak.js
来验证用户并从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>
跨域资源共享(CORS)
If you plan to consume your OpenID Connect service
application from a Single Page Application running on a different domain, you will need to configure CORS (Cross-Origin Resource Sharing). Please read the CORS filter section of the "Cross-origin resource sharing" guide for more details.
提供者端点配置
OIDC service
应用程序需要知道OpenID Connect提供者的令牌、 JsonWebKey
(JWK)集以及可能的 UserInfo
和自省端点地址。
默认情况下,它们是通过在配置的 quarkus.oidc.auth-server-url
中,添加一个 /.well-known/openid-configuration
路径来发现的。
另外,如果发现端点不可用,或者你想节省发现端点的往返开销,你可以禁用发现,用相对路径值配置它们,比如说:
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
令牌传播
关于承载访问令牌向下游服务的传播,请参见 令牌 传播部分。
Oidc Provider Client Authentication
quarkus.oidc.runtime.OidcProviderClient
is used when a remote request to an OpenID Connect Provider has to be done. If the bearer token has to be introspected then OidcProviderClient
has to authenticate to the OpenID Connect Provider. Please see OidcProviderClient Authentication for more information about all the supported authentication options.
测试
首先在你的测试项目中添加以下依赖项:
<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
在你的测试项目中添加以下依赖项:
<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
,例如:
# keycloak.url is set by OidcWiremockTestResource
quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.client-id=quarkus-service-app
quarkus.oidc.application-type=service
并最终写出测试代码,例如:
import static org.hamcrest.Matchers.equalTo;
import java.util.Set;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.server.OidcWiremockTestResource;
import io.restassured.RestAssured;
import io.smallrye.jwt.build.Jwt;
@QuarkusTest
@QuarkusTestResource(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()
操作来签署令牌。
Testing your quarkus-oidc
service
application with OidcWiremockTestResource
provides the best coverage as even the communication channel is tested against the Wiremock HTTP stubs. OidcWiremockTestResource
will be enhanced going forward to support more complex bearer token test scenarios.
如果一个测试需要立即定义Wiremock存根(stubs),而目前 OidcWiremockTestResource
不支持,可以通过注入测试类的 WireMockServer
实例来实现,例如:
|
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
If you work with SaaS OIDC providers such as Auth0
and would like to run tests against the test (development) domain or prefer to run tests against a remote Keycloak test realm, when you already have quarkus.oidc.auth-server-url
configured, you can use OidcTestClient
.
For example, lets assume 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
Start with addding the same dependency as in the Wiremock section, quarkus-test-oidc-server
.
Next, write the test code like this:
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() {
client.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"));
}
}
This test code acquires a token using a password
grant from the test Auth0
domain which has an application with the client id test-auth0-client
registered, and which has a user alice
with a password alice
created. The test Auth0
application must have the password
grant enabled for a test like this one to work. This example code also shows how to pass additional parameters. For Auth0
, these are the audience
and scope
parameters.
为Keycloak提供的开发服务
建议使用 Keycloak开发服务 进行针对Keycloak的集成测试。 Keycloak开发服务
将启动和初始化一个测试容器:它将创建一个 quarkus
领域,一个 quarkus-app
客户端( secret
秘密)并添加 alice
( admin
和 user
角色)和 bob
( user
角色)用户,其中所有这些属性都可以被定制。
首先,你需要添加以下依赖关系:
<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.client.KeycloakTestClient
,你可以在测试中使用它来获取访问令牌。
接下来准备你的 application.properties
。你可以从一个完全空的 application.properties
开始,因为 Keycloak开发服务
将注册指向运行中的测试容器的 quarkus.oidc.auth-server-url
,以及 quarkus.oidc.client-id=quarkus-app
和 quarkus.oidc.credentials.secret=secret
。
但是如果你已经配置了所需的 quarkus-oidc
属性,那么你只需要将 quarkus.oidc.auth-server-url
与 Keycloak开发服务
的 prod
配置文件联系起来,以启动一个容器,例如:
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
如果在运行测试前必须将自定义领域文件导入Keycloak,那么你可以按以下方式配置 Keycloak开发服务
:
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.keycloak.devservices.realm-path=quarkus-realm.json
最后编写你的测试,它将在JVM模式下执行:
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);
}
}
在原生模式(native mode)下:
package org.acme.security.openid.connect;
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
public class NativeBearerTokenAuthenticationIT extends BearerTokenAuthenticationTest {
}
请参阅 Keycloak开发服务 ,来了解更多关于它的初始化和配置方式的信息。
KeycloakTestResourceLifecycleManager
If you need to do some integration testing against Keycloak then you are encouraged to do it with Dev Services For Keycloak. Use KeycloakTestResourceLifecycleManager
for your tests only if there is a good reason not to use Dev Services for Keycloak
.
首先要添加以下依赖关系:
<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容器。
并按以下方式配置Maven Surefire插件:
<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>
(在原生image中测试时也是如此 maven.failsafe.plugin
)。
准备好REST测试端点,设置 application.properties
,例如:
# keycloak.url is set by KeycloakTestResourceLifecycleManager
quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.client-id=quarkus-service-app
quarkus.oidc.credentials=secret
quarkus.oidc.application-type=service
并最终写出测试代码,例如:
import static io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager.getAccessToken;
import static org.hamcrest.Matchers.equalTo;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
import io.restassured.RestAssured;
@QuarkusTest
@QuarkusTestResource(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
具有 用户
和 admin
角色 - 可以使用 keycloak.token.admin-roles
系统属性对其进行自定义。
默认情况下, KeycloakTestResourceLifecycleManager
使用HTTPS来初始化Keycloak实例,可以用 keycloak.use.https=false
来禁用。默认的领域(realm)名称是 quarkus
,客户端ID - quarkus-service-app
- 如果需要,可以设置 keycloak.realm
和 keycloak.service.client
系统属性来定制数值。
本地公钥
你也可以使用一个本地内嵌的公钥来测试你的 quarkus-oidc
service
应用程序:
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
从 main
Quarkus资源库中的 integration-tests/oidc-tenancy
中复制 privateKey.pem
,并使用类似于上面 Wiremock
部分的测试代码来生成JWT令牌。如果愿意,你可以使用你自己的测试密钥。
与Wiremock方法相比,这种方法提供了更有限的覆盖范围—例如,远程通信代码没有被覆盖。
TestSecurity注解
你可以使用 @TestSecurity
和 @OidcSecurity
注解来测试 service
应用程序端点代码,该代码依赖于注入的 JsonWebToken
以及 UserInfo
和 OidcConfigurationMetadata
。
添加以下依赖关系:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-oidc</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-test-security-oidc")
并写一个像这样的测试代码:
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
类可能看起来像这样:
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.UserInfo;
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
注解是可选的,可用于设置额外的标记要求,以及 UserInfo
和 OidcConfigurationMetadata
属性。此外,如果配置了 quarkus.oidc.token.issuer
属性,那么它将被用作 OidcConfigurationMetadata
issuer
属性的值。
如果你用不透明的令牌,那么你可以按以下方式测试它们:
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
类可能看起来像这样:
import io.quarkus.oidc.TokenIntrospection;
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
来添加额外的自省响应属性,如 email
,等等。
This is particularly useful if the same set of security settings needs to be used in multiple test methods. |
如何检查日志中的错误
请启用 io.quarkus.oidc.runtime.OidcProvider
TRACE
级日志,以查看有关令牌验证错误的更多细节:
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".min-level=TRACE
请启用 io.quarkus.oidc.runtime.OidcRecorder
TRACE
级日志,以查看关于OidcProvider客户端初始化错误的更多细节:
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".min-level=TRACE
外部和内部访问OpenID Connect的提供者
Note that the OpenID Connect Provider externally accessible token and other endpoints may have different HTTP(S) URLs compared to the URLs auto-discovered or configured relative to quarkus.oidc.auth-server-url
internal URL. For example, if your SPA acquires a token from an external token endpoint address and sends it to Quarkus as a bearer token then an issuer verification failure may be reported by the endpoint.
在这种情况下,如果你使用Keycloak,那么请用 KEYCLOAK_FRONTEND_URL
系统属性设置为外部可访问的基本URL来启动它。如果你使用其他Openid Connect提供商,那么请查看你的提供商的文档。
如何使用 client-id
属性
quarkus.oidc.client-id
属性标识请求当前持有者令牌的 OpenID Connect Client。它可以是在浏览器中运行的SPA应用程序,也可以是Quarkus web-app
机密客户端应用程序,将访问令牌传播到Quarkus service
的应用程序。
如果 service
应用程序被期望是远程自省令牌—对于不透明的令牌来说总是这样,那么这个属性是必需的。如果只使用本地Json Web Key令牌验证,那么该属性是可选的。
尽管如此,即使端点不需要访问远程自省端点,也鼓励设置该属性。其背后的原因是: client-id
,如果设置了这个属性,就可以用来验证令牌受众,当令牌验证失败时,也会包含在日志中,以便更好地追踪发放给特定客户的令牌,并在较长的时间内进行分析。
例如,如果你的OpenID Connect提供商设置了一个令牌受众,那么建议采用以下配置模式:
# Set client-id
quarkus.oidc.client-id=quarkus-app
# Token audience claim must contain 'quarkus-app'
quarkus.oidc.token.audience=${quarkus.oidc.client-id}
如果你设置了 quarkus.oidc.client-id
,但你的端点不需要远程访问OpenID Connect提供者的一个端点(自省、令牌获取等),那么就不要设置带有 quarkus.oidc.credentials
或类似属性的客户秘钥,因为它不会被使用。
注意Quarkus web-app
应用程序总是需要 quarkus.oidc.client-id
属性。
Authentication after HTTP request has completed
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 HTTP request has completed is when you are processing messages with the Vert.x event bus. In the example below, the 'product-order' message is consumed within different CDI request context, 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.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, 1, rawToken));
}
}
1 | At this point, 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.TenantFeature;
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 {
@TenantFeature("tenantId")
@Inject
TenantIdentityProvider identityProvider;
@Inject
TenantIdentityProvider defaultIdentityProvider; (1)
@Blocking
@ConsumeEvent("product-order")
void processOrder(Product product) {
String rawToken = product.customerAccessToken;
AccessTokenCredential token = new AccessTokenCredential(rawToken);
SecurityIdentity = identityProvider.authenticate(token).await().indefinitely(); (2)
...
}
}
1 | For default tenant, the TenantFeature qualifier is optional. |
2 | Executes token verification and converts the token to a SecurityIdentity . |
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 need to select tenant explicitly with the |
Dynamic tenant configuration resolution is currently not supported. Authentication that requires dynamic tenant will fail. |