OpenID Connect (OIDC) 和 OAuth2 动态客户端注册
此技术被认为是实验性的。 在实验性模式下,我们请求尽早反馈以使这个想法成熟。在解决方案成熟之前,我们不保证平台的稳定性或长期存在。欢迎您在我们的邮件列表或我们的 GitHub 问题跟踪器中提出反馈。 有关可能的完整状态列表,请查看我们的常见问题解答条目。 |
通常,您必须在 OIDC 提供商的仪表板中手动注册 OIDC 客户端(应用程序)。在此过程中,您需要指定人工可读的应用程序名称、允许的重定向 URL 和注销后 URL 以及其他属性。注册完成后,您可以将生成的客户端 ID 和密钥复制到您的 Quarkus OIDC 应用程序属性中。
OpenID Connect 和 OAuth2 动态客户端注册允许您动态注册 OIDC 客户端并管理单个客户端注册。您可以在 OIDC 客户端注册 和 OAuth2 动态客户端注册管理协议 规范文档中阅读更多相关信息。
您可以使用 Quarkus quarkus-oidc-client-registration
扩展来注册一个或多个客户端,方法是使用 OIDC 客户端注册配置,并读取、更新和删除已注册客户端的元数据。
OIDC TenantConfigResolver 可以用于使用已注册客户端的元数据创建 OIDC 租户配置。
目前,Quarkus |
OIDC 客户端注册
添加以下依赖项
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-registration</artifactId>
</dependency>
quarkus-oidc-client-registration
扩展允许使用 OIDC 客户端注册配置注册一个或多个客户端,可以在启动时或按需注册,并读取、更新和删除已注册客户端的元数据。
您可以从自定义 OIDC TenantConfigResolver 注册和管理客户端注册。
或者,您甚至可以在不使用 OIDC 的情况下注册客户端。例如,它可以是一个命令行工具,用于注册客户端并将已注册客户端的元数据传递给需要它们的 Quarkus 服务。
每个 OIDC 客户端注册配置都代表一个 OIDC 客户端注册端点,该端点可以接受多个单独的客户端注册。
在启动时注册客户端
首先声明一个或多个 OIDC 客户端注册配置,例如
# Default OIDC client registration which auto-discovers a standard client registration endpoint.
# It does not require an initial registration token.
quarkus.oidc-client-registration.auth-server-url=${quarkus.oidc.auth-server-url}
quarkus.oidc-client-registration.metadata.client-name=Default Client
quarkus.oidc-client-registration.metadata.redirect-uri=https://:8081/protected
# Named OIDC client registration which configures a registration endpoint path:
# It require an initial registration token for a client registration to succeed.
quarkus.oidc-client-registration.tenant-client.registration-path=${quarkus.oidc.auth-server-url}/clients-registrations/openid-connect
quarkus.oidc-client-registration.tenant-client.metadata.client-name=Tenant Client
quarkus.oidc-client-registration.tenant-client.metadata.redirect-uri=https://:8081/protected/tenant
quarkus.oidc-client-registration.initial-token=${initial-registration-token}
上面的配置将导致在您的 OIDC 提供程序中创建两个新的客户端注册。
您可能需要也可能不需要获取初始注册访问令牌。如果您不需要,那么您将必须在 OIDC 提供商的仪表板中启用一个或多个客户端注册策略。例如,请参阅 Keycloak 客户端注册策略。
下一步是注入 quarkus.oidc.client.registration.OidcClientRegistration
(如果只完成单个默认客户端注册)或 quarkus.oidc.client.registration.OidcClientRegistrations
(如果配置了多个注册),并使用使用这些配置注册的客户端的元数据。
例如
package io.quarkus.it.keycloak;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.client.registration.ClientMetadata;
import io.quarkus.oidc.client.registration.OidcClientRegistration;
import io.quarkus.oidc.client.registration.OidcClientRegistrations;
import io.quarkus.oidc.client.registration.RegisteredClient;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@Singleton
public class CustomTenantConfigResolver implements TenantConfigResolver {
@Inject
OidcClientRegistration clientReg;
@Inject
OidcClientRegistrations clientRegs;
@Override
public Uni<OidcTenantConfig> resolve(RoutingContext routingContext,
OidcRequestContext<OidcTenantConfig> requestContext) {
if (routingContext.request().path().endsWith("/protected")) {
// Use the registered client created from the default OIDC client registration
return clientReg.registeredClient().onItem().transform(client -> createTenantConfig("registered-client", client));
} else if (routingContext.request().path().endsWith("/protected/tenant")) {
// Use the registered client created from the named 'tenant-client' OIDC client registration
OidcClientRegistration tenantClientReg = clientRegs.getClientRegistration("tenant-client");
return tenantClientReg.registeredClient().onItem().transform(client -> createTenantConfig("registered-client-tenant", client));
}
return null;
}
// Convert metadata of registered clients to OidcTenantConfig
private OidcTenantConfig createTenantConfig(String tenantId, RegisteredClient client) {
ClientMetadata metadata = client.getMetadata();
String redirectPath = URI.create(metadata.getRedirectUris().get(0)).getPath();
OidcTenantConfig oidcConfig = OidcTenantConfig
.authServerUrl(authServerUrl)
.tenantId(tenantId)
.applicationType(ApplicationType.WEB_APP)
.clientName(metadata.getClientName())
.clientId(metadata.getClientId())
.credentials(metadata.getClientSecret())
.authentication().redirectPath(redirectPath).end()
.build();
return oidcConfig;
}
}
按需注册客户端
您可以按需注册新客户端。您可以将新客户端添加到现有的、已配置的 OidcClientConfiguration
或新创建的 OidcClientConfiguration
。
首先配置一个或多个 OIDC 客户端注册
quarkus.oidc-client-registration.auth-server-url=${quarkus.oidc.auth-server-url}
上面的配置足以使用此配置注册新客户端。例如
package io.quarkus.it.keycloak;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.client.registration.ClientMetadata;
import io.quarkus.oidc.client.registration.OidcClientRegistration;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@Singleton
public class CustomTenantConfigResolver implements TenantConfigResolver {
@Inject
OidcClientRegistration clientReg;
@Inject
@ConfigProperty(name = "quarkus.oidc.auth-server-url")
String authServerUrl;
@Override
public Uni<OidcTenantConfig> resolve(RoutingContext routingContext,
OidcRequestContext<OidcTenantConfig> requestContext) {
if (routingContext.request().path().endsWith("/protected/oidc-client-reg-existing-config")) {
// New client registration done dynamically at the request time using the configured client registration
ClientMetadata metadata = createMetadata("https://:8081/protected/dynamic-tenant",
"Dynamic Tenant Client");
return clientReg.registerClient(metadata).onItem().transform(r ->
createTenantConfig("registered-client-dynamically", r));
}
return null;
}
// Create metadata of registered clients to OidcTenantConfig
private OidcTenantConfig createTenantConfig(String tenantId, ClientMetadata metadata) {
String redirectPath = URI.create(metadata.getRedirectUris().get(0)).getPath();
OidcTenantConfig oidcConfig = OidcTenantConfig
.authServerUrl(authServerUrl)
.tenantId(tenantId)
.applicationType(ApplicationType.WEB_APP)
.clientName(metadata.getClientName())
.clientId(metadata.getClientId())
.credentials(metadata.getClientSecret())
.authentication().redirectPath(redirectPath).end()
.build();
return oidcConfig;
}
protected static ClientMetadata createMetadata(String redirectUri, String clientName) {
return ClientMetadata.builder()
.setRedirectUri(redirectUri)
.setClientName(clientName)
.build();
}
}
或者,您可以使用 OidcClientRegistrations
来准备新的 OidcClientRegistration
,并使用 OidcClientRegistration
来注册客户端。例如
package io.quarkus.it.keycloak;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.client.registration.ClientMetadata;
import io.quarkus.oidc.client.registration.OidcClientRegistration;
import io.quarkus.oidc.client.registration.OidcClientRegistrations;
import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@Singleton
public class CustomTenantConfigResolver implements TenantConfigResolver {
@Inject
OidcClientRegistrations clientRegs;
@Inject
@ConfigProperty(name = "quarkus.oidc.auth-server-url")
String authServerUrl;
@Override
public Uni<OidcTenantConfig> resolve(RoutingContext routingContext,
OidcRequestContext<OidcTenantConfig> requestContext) {
if (routingContext.request().path().endsWith("/protected/new-oidc-client-reg")) {
// New client registration done dynamically at the request time
OidcClientRegistrationConfig clientRegConfig = OidcClientRegistrationConfig
.authServerUrl(authServerUrl)
.metadata("Dynamic Client", "https://:8081/protected/new-oidc-client-reg")
.build();
return clientRegs.newClientRegistration(clientRegConfig)
.onItem().transform(reg ->
createTenantConfig("registered-client-dynamically", reg.registeredClient());
}
return null;
}
// Create metadata of registered clients to OidcTenantConfig
private OidcTenantConfig createTenantConfig(String tenantId, ClientMetadata metadata) {
String redirectPath = URI.create(metadata.getRedirectUris().get(0)).getPath();
OidcTenantConfig oidcConfig = OidcTenantConfig
.authServerUrl(authServerUrl)
.tenantId(tenantId)
.applicationType(ApplicationType.WEB_APP)
.clientName(metadata.getClientName())
.clientId(metadata.getClientId())
.credentials(metadata.getClientSecret())
.authentication().redirectPath(redirectPath).end()
.build();
return oidcConfig;
}
protected static ClientMetadata createMetadata(String redirectUri, String clientName) {
return ClientMetadata.builder()
.setRedirectUri(redirectUri)
.setClientName(clientName)
.build();
}
}
管理已注册的客户端
io.quarkus.oidc.client.registration.RegisteredClient
表示已注册的客户端,可用于读取和更新其元数据。它也可以用于删除此客户端。
例如
package io.quarkus.it.keycloak;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.client.registration.OidcClientRegistration;
import io.quarkus.oidc.client.registration.RegisteredClient;
import io.quarkus.runtime.StartupEvent;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@Singleton
public class CustomTenantConfigResolver implements TenantConfigResolver {
@Inject
OidcClientRegistration clientReg;
RegisteredClient registeredClient;
void onStartup(@Observes StartupEvent event) {
// Default OIDC client registration, client has already been registered at startup, `await()` will return immediately.
registeredClient = clientReg.registeredClient().await().indefinitely();
// Read the latest client metadata
registeredClient = registeredClient.read().await().indefinitely();
}
@Override
public Uni<OidcTenantConfig> resolve(RoutingContext routingContext,
OidcRequestContext<OidcTenantConfig> requestContext) {
if (routingContext.request().path().endsWith("/protected")) {
// Use the registered client created from the default OIDC client registration
return createTenantConfig("registered-client", registeredClient));
}
return null;
}
// Convert metadata of registered clients to OidcTenantConfig
private OidcTenantConfig createTenantConfig(String tenantId, RegisteredClient client) {
ClientMetadata metadata = client.getMetadata();
String redirectPath = URI.create(metadata.getRedirectUris().get(0)).getPath();
OidcTenantConfig oidcConfig = OidcTenantConfig
.authServerUrl(authServerUrl)
.tenantId(tenantId)
.applicationType(ApplicationType.WEB_APP)
.clientName(metadata.getClientName())
.clientId(metadata.getClientId())
.credentials(metadata.getClientSecret())
.authentication().redirectPath(redirectPath).end()
.build();
return oidcConfig;
}
}
避免重复注册
当您在启动时注册客户端时,如 启动时注册客户端 部分所述,您很可能希望避免在重新启动后创建重复的注册。
在这种情况下,您应该将 OIDC 客户端注册配置为在请求时执行注册,而不是在启动时执行注册
quarkus.oidc-client-registration.register-early=false
接下来,您应该在关闭时持久化已注册客户端的注册 URI 和注册令牌,您可以从 io.quarkus.oidc.client.registration.RegisteredClient
实例获取它们。
最后,在启动时,您应该恢复已注册的客户端,而不是再次注册它。
例如
package io.quarkus.it.keycloak;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.client.registration.OidcClientRegistration;
import io.quarkus.oidc.client.registration.RegisteredClient;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@Singleton
public class CustomTenantConfigResolver implements TenantConfigResolver {
@Inject
OidcClientRegistration clientReg;
RegisteredClient registeredClient;
void onStartup(@Observes StartupEvent event) {
String registrationUri = readRegistrationUriFromDatabase("Registered Client");
String registrationToken = readRegistrationTokenFromDatabase("Registered Client");
if (registrationUri != null && registrationToken != null) {
// Read an already registered client
registeredClient = clientReg.readClient(registrationUri, registrationToken).await().indefinitely();
} else {
// Register a new client
registeredClient = clientReg.registeredClient().await().indefinitely();
}
}
void onShutdown(@Observes ShutdownEvent event) {
saveRegistrationUriToDatabase("Registered Client", registeredClient.registrationUri());
saveRegistrationTokenToDatabase("Registered Client", registeredClient.registrationToken());
}
String readRegistrationUriFromDatabase(String clientName) {
// implementation is not shown for brevity
}
String readRegistrationTokenFromDatabase(String clientName) {
// implementation is not shown for brevity
}
void saveRegistrationUriToDatabase(String clientName, String registrationUri) {
// implementation is not shown for brevity
}
void saveRegistrationTokenToDatabase(String clientName, String registrationToken) {
// implementation is not shown for brevity
}
@Override
public Uni<OidcTenantConfig> resolve(RoutingContext routingContext,
OidcRequestContext<OidcTenantConfig> requestContext) {
if (routingContext.request().path().endsWith("/protected")) {
// Use the registered client created from the default OIDC client registration
return createTenantConfig("registered-client", registeredClient));
}
return null;
}
// Convert metadata of registered clients to OidcTenantConfig
private OidcTenantConfig createTenantConfig(String tenantId, RegisteredClient client) {
ClientMetadata metadata = client.getMetadata();
String redirectPath = URI.create(metadata.getRedirectUris().get(0)).getPath();
OidcTenantConfig oidcConfig = OidcTenantConfig
.authServerUrl(authServerUrl)
.tenantId(tenantId)
.applicationType(ApplicationType.WEB_APP)
.clientName(metadata.getClientName())
.clientId(metadata.getClientId())
.credentials(metadata.getClientSecret())
.authentication().redirectPath(redirectPath).end()
.build();
return oidcConfig;
}
}
如果您按需动态注册客户端,如 按需注册客户端 部分所述,则不应出现重复客户端注册的问题。如有必要,您可以持久化已注册客户端的注册 URI 和注册令牌,并检查它们以避免任何重复预订风险。
OIDC 请求过滤器
您可以通过注册一个或多个 OidcRequestFilter
实现来过滤 OIDC 客户端注册和已注册的客户端请求,这些实现可以更新或添加新的请求标头。例如,过滤器可以分析请求正文并将其摘要添加为新的标头值
您可以让单个过滤器拦截所有 OIDC 注册和已注册的客户端请求,或者使用 @OidcEndpoint
注释将此过滤器仅应用于 OIDC 注册或已注册的客户端端点响应。例如
package io.quarkus.it.keycloak;
import org.jboss.logging.Logger;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.core.json.JsonObject;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.CLIENT_REGISTRATION) (1)
public class ClientRegistrationRequestFilter implements OidcRequestFilter {
private static final Logger LOG = Logger.getLogger(ClientRegistrationRequestFilter.class);
@Override
public void filter(OidcRequestContext rc) {
JsonObject body = rc.requestBody().toJsonObject();
if ("Default Client".equals(body.getString("client_name"))) { (2)
LOG.debug("'Default Client' registration request");
}
}
}
1 | 将此过滤器限制为仅针对 OIDC 客户端注册端点的请求。 |
2 | 检查请求元数据 JSON 中的“client_name”属性。 |
OidcRequestContextProperties
可用于访问请求属性。目前,您可以使用 client_id
键访问客户端租户 ID,使用 grant_type
键访问 OIDC 客户端用于获取令牌的授权类型。
OIDC 响应过滤器
您可以通过注册一个或多个 OidcResponseFilter
实现来过滤对 OIDC 客户端注册和已注册的客户端请求的响应,这些实现可以检查响应状态、标头和正文以便记录它们或执行其他操作。
您可以让单个过滤器拦截对所有 OIDC 注册和已注册的客户端请求的响应,或者使用 @OidcEndpoint
注释将此过滤器仅应用于来自 OIDC 注册或已注册的客户端端点的响应。例如
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcResponseFilter;
import io.vertx.core.json.JsonObject;
@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.CLIENT_REGISTRATION) (1)
public class ClientRegistrationResponseFilter implements OidcResponseFilter {
private static final Logger LOG = Logger.getLogger(ClientRegistrationResponseFilter.class);
@Override
public void filter(OidcResponseContext rc) {
String contentType = rc.responseHeaders().get("Content-Type"); (2)
JsonObject body = rc.responseBody().toJsonObject();
if (contentType.startsWith("application/json")
&& "Default Client".equals(body.getString("client_name"))) { (3)
LOG.debug("'Default Client' has been registered");
}
}
}
1 | 将此过滤器限制为仅针对 OIDC 客户端注册端点的请求。 |
2 | 检查响应 Content-Type 标头。 |
3 | 检查响应元数据 JSON 中的“client_name”属性。 |
或
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcResponseFilter;
@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.REGISTERED_CLIENT) (1)
public class RegisteredClientResponseFilter implements OidcResponseFilter {
private static final Logger LOG = Logger.getLogger(RegisteredClientResponseFilter.class);
@Override
public void filter(OidcResponseContext rc) {
String contentType = rc.responseHeaders().get("Content-Type"); (2)
if (contentType.startsWith("application/json")
&& "Default Client Updated".equals(rc.responseBody().toJsonObject().getString("client_name"))) { (3)
LOG.debug("Registered 'Default Client' has had its name updated to 'Default Client Updated'");
}
}
}
1 | 将此过滤器限制为仅针对已注册的 OIDC 客户端端点的请求。 |
2 | 检查响应 Content-Type 标头。 |
3 | 确认已更新客户端名称属性。 |
配置参考
构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖
配置属性 |
类型 |
默认 |
---|---|---|
如果启用了 OIDC 客户端注册扩展。 环境变量: 显示更多 |
布尔值 |
|
OpenID Connect (OIDC) 服务器的基本 URL,例如, 环境变量: 显示更多 |
字符串 |
|
OIDC 端点的发现。如果未启用,您必须单独配置 OIDC 端点 URL。 环境变量: 显示更多 |
布尔值 |
|
OIDC 动态客户端注册端点的相对路径或绝对 URL。如果 环境变量: 显示更多 |
字符串 |
|
尝试与 OIDC 服务器建立初始连接的持续时间。例如,将持续时间设置为 环境变量: 显示更多 |
||
如果现有 OIDC 连接暂时丢失,则重试重新建立连接的次数。与仅适用于初始连接尝试的 环境变量: 显示更多 |
整数 |
|
当前 OIDC 连接请求超时的秒数。 环境变量: 显示更多 |
|
|
是否应在工作线程上执行 DNS 查找。当您看到有关 HTTP 请求阻塞 Vert.x 事件循环的已记录警告时,请使用此选项。 环境变量: 显示更多 |
布尔值 |
|
WebClient 使用的连接池的最大大小。 环境变量: 显示更多 |
整数 |
|
当 WebClient 收到 HTTP 302 时自动遵循重定向。禁用此属性后,只允许单个重定向到完全相同的原始 URI,但前提是在重定向请求期间设置了一个或多个 cookie。 环境变量: 显示更多 |
布尔值 |
|
OIDC 客户端注册 ID 环境变量: 显示更多 |
字符串 |
|
是否启用此客户端注册配置。 环境变量: 显示更多 |
布尔值 |
|
是否必须在启动时注册使用 环境变量: 显示更多 |
布尔值 |
|
初始访问令牌 环境变量: 显示更多 |
字符串 |
|
客户端名称 环境变量: 显示更多 |
字符串 |
|
重定向 URI 环境变量: 显示更多 |
字符串 |
|
注销后 URI 环境变量: 显示更多 |
字符串 |
|
其他元数据属性 环境变量: 显示更多 |
Map<String,String> |
|
类型 |
默认 |
|
代理的主机名或 IP 地址。 环境变量: 显示更多 |
字符串 |
|
代理的端口号。默认值为 环境变量: 显示更多 |
整数 |
|
用户名(如果代理需要身份验证)。 环境变量: 显示更多 |
字符串 |
|
密码(如果代理需要身份验证)。 环境变量: 显示更多 |
字符串 |
|
类型 |
默认 |
|
要使用的 TLS 配置的名称。 如果配置了名称,它将使用来自 默认情况下,不使用默认 TLS 配置。 环境变量: 显示更多 |
字符串 |
关于 Duration 格式
要写入持续时间值,请使用标准的 您还可以使用简化的格式,以数字开头
在其他情况下,简化格式将被转换为
|