OIDC web-app 应用需要 OIDC 供应商的授权、令牌、JsonWebKey (JWK)集的 URL,以及 UserInfo、introspection 和 end-session (RP-initiated logout)端点。
按照惯例,通过将 /.well-known/openid-configuration 路径添加到配置的 quarkus.oidc.auth-server-url 来发现它们。
或者,如果发现端点不可用,或者您希望减少发现端点往返,您可以禁用端点发现并配置相对路径值。例如:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.discovery-enabled=false
# Authorization endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/auth
quarkus.oidc.authorization-path=/protocol/openid-connect/auth
# 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/token/introspect
quarkus.oidc.introspection-path=/protocol/openid-connect/token/introspect
# End-session endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/logout
quarkus.oidc.end-session-path=/protocol/openid-connect/logout
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.discovery-enabled=false
# Authorization endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/auth
quarkus.oidc.authorization-path=/protocol/openid-connect/auth
# 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/token/introspect
quarkus.oidc.introspection-path=/protocol/openid-connect/token/introspect
# End-session endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/logout
quarkus.oidc.end-session-path=/protocol/openid-connect/logout
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
有些 OIDC 供应商支持元数据发现,但不要返回授权代码流完成或支持应用程序功能所需的所有端点 URL 值,如用户注销。要临时解决这个问题,您可以在本地配置缺少的端点 URL 值,如下例所示:
Metadata is auto-discovered but it does not return an end-session endpoint URL
Configure the end-session URL locally.
It can be an absolute or relative (to 'quarkus.oidc.auth-server-url') address
# Metadata is auto-discovered but it does not return an end-session endpoint URL
quarkus.oidc.auth-server-url=http://localhost:8180/oidcprovider/account
# Configure the end-session URL locally.
# It can be an absolute or relative (to 'quarkus.oidc.auth-server-url') address
quarkus.oidc.end-session-path=logout
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
如果 URL 无法用于本地 Quarkus 端点,并且需要更具体的值,则可以使用同样的配置来覆盖发现的端点 URL。例如,支持全局和特定于应用程序的最终用户会话端点的供应商会返回一个全局最终用户会话 URL,如 http://localhost:8180/oidcprovider/account/global-logout 。此 URL 将从当前登录的用户的所有应用中注销用户。但是,如果要求是当前应用程序只将用户从特定应用程序注销,您可以通过设置 quarkus.oidc.end-session=logout 参数来覆盖全局 end-session URL。
OIDC 供应商通常需要在与 OIDC 端点交互时识别和验证应用程序。quarkus OIDC,特别是 quarkus.oidc.runtime.OidcProviderClient 类,当必须刷新或内省授权代码时,向 OIDC 供应商进行身份验证。
通常,当授权到 OIDC 供应商时,为给定应用程序定义客户端 ID 和客户端 secret。所有 OIDC 客户端身份验证 选项都被支持。例如:
或者:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.client-secret.value=mysecret
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.client-secret.value=mysecret
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
以下示例显示了从 凭证供应商 检索的 secret:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
# This is a key which will be used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc.credentials.client-secret.provider.key=mysecret-key
# This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation
quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc.credentials.client-secret.provider.name=oidc-credentials-provider
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
# This is a key which will be used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc.credentials.client-secret.provider.key=mysecret-key
# This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation
quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc.credentials.client-secret.provider.name=oidc-credentials-provider
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
在 application.properties 中内联带有 PEM 密钥的 private_key_jwt 示例,其中签名算法为 RS256 :
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.key=Base64-encoded private key representation
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.key=Base64-encoded private key representation
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
使用 PEM 密钥文件的 private_key_jwt 示例,签名算法为 RS256:。
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.key-file=privateKey.pem
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.key-file=privateKey.pem
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
使用 client_secret_jwt 或 private_key_jwt 身份验证方法可确保客户端 secret 没有发送到 OIDC 供应商,因此避免了被 'man-in-the-middle' 攻击所截获的 secret 的风险。
如果使用 client_secret_jwt、private_key_jwt 或 Apple post_jwt 身份验证方法,您可以自定义 JWT 签名算法、密钥标识符、使用者、主题和签发者。例如:
private_key_jwt client authentication
This is a token key identifier 'kid' header - set it if your OIDC provider requires it:
Note if the key is represented in a JSON Web Key (JWK) format with a `kid` property, then
using 'quarkus.oidc.credentials.jwt.token-key-id' is not necessary.
Use RS512 signature algorithm instead of the default RS256
The token endpoint URL is the default audience value, use the base address URL instead:
custom subject instead of the client id:
custom issuer instead of the client id:
# private_key_jwt client authentication
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.key-file=privateKey.pem
# This is a token key identifier 'kid' header - set it if your OIDC provider requires it:
# Note if the key is represented in a JSON Web Key (JWK) format with a `kid` property, then
# using 'quarkus.oidc.credentials.jwt.token-key-id' is not necessary.
quarkus.oidc.credentials.jwt.token-key-id=mykey
# Use RS512 signature algorithm instead of the default RS256
quarkus.oidc.credentials.jwt.signature-algorithm=RS512
# The token endpoint URL is the default audience value, use the base address URL instead:
quarkus.oidc.credentials.jwt.audience=${quarkus.oidc-client.auth-server-url}
# custom subject instead of the client id:
quarkus.oidc.credentials.jwt.subject=custom-subject
# custom issuer instead of the client id:
quarkus.oidc.credentials.jwt.issuer=custom-issuer
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
Apple OIDC 供应商使用 client_secret_post 方法,其中 secret 是使用 private_key_jwt 身份验证方法生成的 JWT,但使用 Apple 帐户特定的签发者和主题声明。
在 Quarkus Security 中,quarkus-oidc 支持非标准 client_secret_post_jwt 身份验证方法,如下所示:
Apple provider configuration sets a 'client_secret_post_jwt' authentication method
Apple provider configuration sets ES256 signature algorithm
# Apple provider configuration sets a 'client_secret_post_jwt' authentication method
quarkus.oidc.provider=apple
quarkus.oidc.client-id=${apple.client-id}
quarkus.oidc.credentials.jwt.key-file=ecPrivateKey.pem
quarkus.oidc.credentials.jwt.token-key-id=${apple.key-id}
# Apple provider configuration sets ES256 signature algorithm
quarkus.oidc.credentials.jwt.subject=${apple.subject}
quarkus.oidc.credentials.jwt.issuer=${apple.issuer}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
有些 OIDC 供应商可能需要客户端作为 mutual TLS 身份验证过程的一部分进行身份验证。
以下示例演示了如何配置 quarkus-oidc 以支持 mTLS :
quarkus.oidc.tls.verification=certificate-validation
# Keystore configuration
quarkus.oidc.tls.key-store-file=client-keystore.jks
quarkus.oidc.tls.key-store-password=${key-store-password}
# Add more keystore properties if needed:
#quarkus.oidc.tls.key-store-alias=keyAlias
#quarkus.oidc.tls.key-store-alias-password=keyAliasPassword
# Truststore configuration
quarkus.oidc.tls.trust-store-file=client-truststore.jks
quarkus.oidc.tls.trust-store-password=${trust-store-password}
# Add more truststore properties if needed:
#quarkus.oidc.tls.trust-store-alias=certAlias
quarkus.oidc.tls.verification=certificate-validation
# Keystore configuration
quarkus.oidc.tls.key-store-file=client-keystore.jks
quarkus.oidc.tls.key-store-password=${key-store-password}
# Add more keystore properties if needed:
#quarkus.oidc.tls.key-store-alias=keyAlias
#quarkus.oidc.tls.key-store-alias-password=keyAliasPassword
# Truststore configuration
quarkus.oidc.tls.trust-store-file=client-truststore.jks
quarkus.oidc.tls.trust-store-password=${trust-store-password}
# Add more truststore properties if needed:
#quarkus.oidc.tls.trust-store-alias=certAlias
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
有些提供程序(如 Strava OAuth2 提供程序 )需要发布客户端凭证作为 HTTP POST 查询参数:
quarkus.oidc.provider=strava
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.client-secret.value=mysecret
quarkus.oidc.credentials.client-secret.method=query
quarkus.oidc.provider=strava
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.client-secret.value=mysecret
quarkus.oidc.credentials.client-secret.method=query
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
有些 OIDC 供应商需要使用基本身份验证以及与 client_id 和 client_secret 不同的凭证进行身份验证。如果您之前已将安全身份验证配置为支持 client_secret_basic 或 client_secret_post 客户端身份验证方法,如 OIDC 供应商 客户端身份验证部分中所述,您可能需要应用额外的配置,如下所示。
如果必须内省令牌,并且需要内省特定于内省端点的身份验证机制,您可以配置 quarkus-oidc,如下所示:
quarkus.oidc.introspection-credentials.name=introspection-user-name
quarkus.oidc.introspection-credentials.secret=introspection-user-secret
quarkus.oidc.introspection-credentials.name=introspection-user-name
quarkus.oidc.introspection-credentials.secret=introspection-user-secret
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
您可以通过注册一个或多个 OidcRequestFilter 实现(可以更新或添加新的请求标头)来过滤 Quarkus 向 OIDC 供应商发出的 OIDC 请求,也可以记录请求。
例如:
package io.quarkus.it.keycloak;
import io.quarkus.oidc.OidcConfigurationMetadata;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpRequest;
@ApplicationScoped
@Unremovable
public class OidcTokenRequestCustomizer implements OidcRequestFilter {
@Override
public void filter(HttpRequest<Buffer> request, Buffer buffer, OidcRequestContextProperties contextProps) {
OidcConfigurationMetadata metadata = contextProps.get(OidcConfigurationMetadata.class.getName());
// Metadata URI is absolute, request URI value is relative
if (metadata.getTokenUri().endsWith(request.uri())) {
request.putHeader("TokenGrantDigest", calculateDigest(buffer.toString()));
}
}
private String calculateDigest(String bodyString) {
// Apply the required digest algorithm to the body string
}
}
package io.quarkus.it.keycloak;
import io.quarkus.oidc.OidcConfigurationMetadata;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpRequest;
@ApplicationScoped
@Unremovable
public class OidcTokenRequestCustomizer implements OidcRequestFilter {
@Override
public void filter(HttpRequest<Buffer> request, Buffer buffer, OidcRequestContextProperties contextProps) {
OidcConfigurationMetadata metadata = contextProps.get(OidcConfigurationMetadata.class.getName()); 1
// Metadata URI is absolute, request URI value is relative
if (metadata.getTokenUri().endsWith(request.uri())) { 2
request.putHeader("TokenGrantDigest", calculateDigest(buffer.toString()));
}
}
private String calculateDigest(String bodyString) {
// Apply the required digest algorithm to the body string
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
1
get OidcConfigurationMetadata,其中包含所有支持的 OIDC 端点地址。
2
使用 OidcConfigurationMetadata 只过滤对 OIDC 令牌端点的请求。
或者,您可以使用 @OidcEndpoint 注解将此过滤器应用到令牌端点请求:
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpRequest;
@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.DISCOVERY)
public class OidcDiscoveryRequestCustomizer implements OidcRequestFilter {
@Override
public void filter(HttpRequest<Buffer> request, Buffer buffer, OidcRequestContextProperties contextProps) {
request.putHeader("Discovery", "OK");
}
}
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpRequest;
@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.DISCOVERY) 1
public class OidcDiscoveryRequestCustomizer implements OidcRequestFilter {
@Override
public void filter(HttpRequest<Buffer> request, Buffer buffer, OidcRequestContextProperties contextProps) {
request.putHeader("Discovery", "OK");
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
1
将这个过滤器限制为只针对 OIDC 发现端点的请求。
当用户重定向到 OIDC 供应商以进行身份验证时,重定向 URL 包含一个 redirect_uri 查询参数,该参数表示在身份验证完成后必须重定向到的供应商。在我们的情形中,这是 Quarkus 应用程序。
默认情况下,quarkus 将此参数设置为当前应用程序请求 URL。例如,如果用户试图访问 http://localhost:8080/service/1 上的 Quarkus 服务端点,则 redirect_uri 参数被设置为 http://localhost:8080/service/1 。同样,如果请求 URL 是 http://localhost:8080/service/2 ,则 redirect_uri 参数被设置为 http://localhost:8080/service/2 。
有些 OIDC 供应商要求将 redirect_uri 的值与给定应用程序的值相同,例如 http://localhost:8080/service/callback ,用于所有重定向 URL。在这种情况下,必须设置 quarkus.oidc.authentication.redirect-path 属性。例如,quarkus.oidc.authentication.redirect-path=/service/callback,Quarkus 会将 redirect_uri 参数设置为绝对 URL,如 http://localhost:8080/service/callback ,它都相同,无论当前请求 URL 都相同。
如果设置了 quarkus.oidc.authentication.redirect-path,但您需要在用户重定向到唯一回调 URL 后恢复原始请求 URL,例如 http://localhost:8080/service/callback ,将 quarkus.oidc.authentication.restore-path-after-redirect 属性设置为 true。这将恢复请求 URL,如 http://localhost:8080/service/1 。
默认情况下,只有 response_type (设置为 代码)、scope (设置为 openid)、client_id、redirect_uri、以及 status 属性在用户重定向到时传递给 OIDC 供应商的授权端点。
您可以使用 quarkus.oidc.authentication.extra-params 添加更多属性。例如,有些 OIDC 供应商可能会选择返回授权代码作为重定向 URI 片段的一部分,这会中断身份验证过程。以下示例演示了如何解决这个问题:
quarkus.oidc.authentication.extra-params.response_mode=query
quarkus.oidc.authentication.extra-params.response_mode=query
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
另请参阅 OIDC 重定向过滤器 部分来说明如何使用自定义 OidcRedirectFilter 自定义 OIDC 重定向,包括给 OIDC 授权端点。
当用户重定向到 OIDC 授权端点以进行身份验证时,如果需要,授权 Quarkus 应用程序,这个重定向请求可能会失败,例如,当重定向 URI 中包含无效的范围时。在这种情况下,供应商会将用户重新重定向到 Quarkus,并带有 error 和 error_description 参数,而不是预期的 代码 参数。
例如,当重定向到提供程序的无效范围或其他无效参数时,可能会发生这种情况。
在这种情况下,默认会返回 HTTP 401 错误。但是,您可以请求调用自定义公共错误端点,以返回更用户友好的 HTML 错误页面。要做到这一点,请设置 quarkus.oidc.authentication.error-path 属性,如下例所示:
quarkus.oidc.authentication.error-path=/error
quarkus.oidc.authentication.error-path=/error
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
确保属性以正斜杠(/)字符开头,并且路径相对于当前端点的基本 URI。例如,如果设为 '/error',并且当前请求 URI 为 https://localhost:8080/callback?error=invalid_scope ,则向 https://localhost:8080/error?error=invalid_scope 发出最终重定向。
若要防止用户重定向到此页面,需要重新验证,请确保此错误端点为公共资源。
您可以注册一个或多个 io.quarkus.oidc.OidcRedirectFilter 实现,以过滤 OIDC 重定向到 OIDC 授权和注销端点,还可以本地重定向到自定义错误和会话过期页面。自定义 OidcRedirectFilter 可以添加额外的查询参数、响应标头和设置新的 Cookie。
例如,以下简单自定义 OidcRedirectFilter 添加额外的查询参数,并为 Quarkus OIDC 可以执行的所有重定向请求添加一个自定义响应标头:
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.OidcRedirectFilter;
@ApplicationScoped
@Unremovable
public class GlobalOidcRedirectFilter implements OidcRedirectFilter {
@Override
public void filter(OidcRedirectContext context) {
if (context.redirectUri().contains("/session-expired-page")) {
context.additionalQueryParams().add("redirect-filtered", "true,");
context.routingContext().response().putHeader("Redirect-Filtered", "true");
}
}
}
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.OidcRedirectFilter;
@ApplicationScoped
@Unremovable
public class GlobalOidcRedirectFilter implements OidcRedirectFilter {
@Override
public void filter(OidcRedirectContext context) {
if (context.redirectUri().contains("/session-expired-page")) {
context.additionalQueryParams().add("redirect-filtered", "true,"); 1
context.routingContext().response().putHeader("Redirect-Filtered", "true"); 2
}
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
1
添加额外的查询参数。请注意,queury 名称和值是 Quarkus OIDC 编码的 URL,在本例中将 redirect-filtered=true%20C 查询参数添加到重定向 URI 中。
2
添加自定义 HTTP 响应标头。
另请参阅 自定义身份验证请求 部分,了解如何为 OIDC 授权点配置额外的查询参数。
自定义 OidcRedirectFilter 用于本地错误,会话过期页面也可以创建安全 Cookie,以帮助生成此类页面。
例如,假设您需要将会话过期至自定义会话过期页面的当前用户重定向到 http://localhost:8080/session-expired-page 处提供的自定义会话过期页面。以下自定义 OidcRedirectFilter 使用 OIDC 租户客户端 secret 在自定义 session_expired cookie 中加密用户名:
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.jwt.Claims;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.OidcRedirectFilter;
import io.quarkus.oidc.Redirect;
import io.quarkus.oidc.Redirect.Location;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.jwt.build.Jwt;
@ApplicationScoped
@Unremovable
@TenantFeature("tenant-refresh")
@Redirect(Location.SESSION_EXPIRED_PAGE)
public class SessionExpiredOidcRedirectFilter implements OidcRedirectFilter {
@Override
public void filter(OidcRedirectContext context) {
if (context.redirectUri().contains("/session-expired-page")) {
AuthorizationCodeTokens tokens = context.routingContext().get(AuthorizationCodeTokens.class.getName());
String userName = OidcUtils.decodeJwtContent(tokens.getIdToken()).getString(Claims.preferred_username.name());
String jwe = Jwt.preferredUserName(userName).jwe()
.encryptWithSecret(context.oidcTenantConfig().credentials.secret.get());
OidcUtils.createCookie(context.routingContext(), context.oidcTenantConfig(), "session_expired",
jwe + "|" + context.oidcTenantConfig().tenantId.get(), 10);
}
}
}
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.jwt.Claims;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.OidcRedirectFilter;
import io.quarkus.oidc.Redirect;
import io.quarkus.oidc.Redirect.Location;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.jwt.build.Jwt;
@ApplicationScoped
@Unremovable
@TenantFeature("tenant-refresh")
@Redirect(Location.SESSION_EXPIRED_PAGE) 1
public class SessionExpiredOidcRedirectFilter implements OidcRedirectFilter {
@Override
public void filter(OidcRedirectContext context) {
if (context.redirectUri().contains("/session-expired-page")) {
AuthorizationCodeTokens tokens = context.routingContext().get(AuthorizationCodeTokens.class.getName()); 2
String userName = OidcUtils.decodeJwtContent(tokens.getIdToken()).getString(Claims.preferred_username.name()); 3
String jwe = Jwt.preferredUserName(userName).jwe()
.encryptWithSecret(context.oidcTenantConfig().credentials.secret.get()); 4
OidcUtils.createCookie(context.routingContext(), context.oidcTenantConfig(), "session_expired",
jwe + "|" + context.oidcTenantConfig().tenantId.get(), 10); 5
}
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
1
确保仅在重定向到会话过期页面期间调用此重定向过滤器。
2
访问与现在 已过期会话关联的令牌作为 RoutingContext 属性。
3
解码 ID 令牌声明并获取用户名。
4
在使用当前 OIDC 租户的客户端 secret 加密的 JWT 令牌中保存用户名。
5
为 5 秒创建一个自定义 session_expired cookie,它会使用 "|" 分隔符加入加密的令牌和租户 ID。在自定义 Cookie 中记录租户 ID 有助于在多租户 OIDC 设置中生成正确的会话过期页面。
接下来,生成会话过期页面的公共 JAX-RS 资源可使用此 Cookie 创建为此用户定制的页面,以及对应的 OIDC 租户,例如:
package io.quarkus.it.keycloak;
import jakarta.inject.Inject;
import jakarta.ws.rs.CookieParam;
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.oidc.OidcTenantConfig;
import io.quarkus.oidc.runtime.OidcUtils;
import io.quarkus.oidc.runtime.TenantConfigBean;
import io.smallrye.jwt.auth.principal.DefaultJWTParser;
import io.vertx.ext.web.RoutingContext;
@Path("/session-expired-page")
public class SessionExpiredResource {
@Inject
RoutingContext context;
@Inject
TenantConfigBean tenantConfig;
@GET
public String sessionExpired(@CookieParam("session_expired") String sessionExpired) throws Exception {
// Cookie format: jwt|<tenant id>
String[] pair = sessionExpired.split("\\|");
OidcTenantConfig oidcConfig = tenantConfig.getStaticTenantsConfig().get(pair[1]).getOidcTenantConfig();
JsonWebToken jwt = new DefaultJWTParser().decrypt(pair[0], oidcConfig.credentials.secret.get());
OidcUtils.removeCookie(context, oidcConfig, "session_expired");
return jwt.getClaim(Claims.preferred_username) + ", your session has expired. "
+ "Please login again at http://localhost:8081/" + oidcConfig.tenantId.get();
}
}
package io.quarkus.it.keycloak;
import jakarta.inject.Inject;
import jakarta.ws.rs.CookieParam;
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.oidc.OidcTenantConfig;
import io.quarkus.oidc.runtime.OidcUtils;
import io.quarkus.oidc.runtime.TenantConfigBean;
import io.smallrye.jwt.auth.principal.DefaultJWTParser;
import io.vertx.ext.web.RoutingContext;
@Path("/session-expired-page")
public class SessionExpiredResource {
@Inject
RoutingContext context;
@Inject
TenantConfigBean tenantConfig; 1
@GET
public String sessionExpired(@CookieParam("session_expired") String sessionExpired) throws Exception {
// Cookie format: jwt|<tenant id>
String[] pair = sessionExpired.split("\\|"); 2
OidcTenantConfig oidcConfig = tenantConfig.getStaticTenantsConfig().get(pair[1]).getOidcTenantConfig(); 3
JsonWebToken jwt = new DefaultJWTParser().decrypt(pair[0], oidcConfig.credentials.secret.get()); 4
OidcUtils.removeCookie(context, oidcConfig, "session_expired"); 5
return jwt.getClaim(Claims.preferred_username) + ", your session has expired. "
+ "Please login again at http://localhost:8081/" + oidcConfig.tenantId.get(); 6
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
1
注入 TenantConfigBean,用于访问所有当前的 OIDC 租户配置。
2
将自定义 Cookie 值分成 2 个部分,第一部分是加密的令牌,最后一个部分是租户 ID。
3
获取 OIDC 租户配置。
4
使用 OIDC 租户的客户端 secret 解密 Cookie 值。
5
删除自定义 Cookie。
6
使用解密令牌中的用户名和租户 ID 生成服务过期的页面响应。
您可以通过不同的方式访问授权信息。
OIDC 代码身份验证机制在授权代码流期间获取三个令牌: ID 令牌 、访问令牌和刷新令牌。
ID 令牌始终是一个 JWT 令牌,代表使用 JWT 声明进行用户身份验证。您可以使用它来发出 OIDC 端点、用户名以及名为 声明 的其他信息。您可以使用 IdToken qualifier 注入 JsonWebToken 来访问 ID 令牌声明:
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
@Path("/web-app")
@Authenticated
public class ProtectedResource {
@Inject
@IdToken
JsonWebToken idToken;
@GET
public String getUserName() {
return idToken.getName();
}
}
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
@Path("/web-app")
@Authenticated
public class ProtectedResource {
@Inject
@IdToken
JsonWebToken idToken;
@GET
public String getUserName() {
return idToken.getName();
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
OIDC web-app 应用程序通常使用访问令牌代表当前登录的用户访问其他端点。您可以访问原始访问令牌,如下所示:
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.security.Authenticated;
@Path("/web-app")
@Authenticated
public class ProtectedResource {
@Inject
JsonWebToken accessToken;
// or
// @Inject
// AccessTokenCredential accessTokenCredential;
@GET
public String getReservationOnBehalfOfUser() {
String rawAccessToken = accessToken.getRawToken();
//or
//String rawAccessToken = accessTokenCredential.getToken();
// Use the raw access token to access a remote endpoint.
// For example, use RestClient to set this token as a `Bearer` scheme value of the HTTP `Authorization` header:
// `Authorization: Bearer rawAccessToken`.
return getReservationfromRemoteEndpoint(rawAccesstoken);
}
}
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.security.Authenticated;
@Path("/web-app")
@Authenticated
public class ProtectedResource {
@Inject
JsonWebToken accessToken;
// or
// @Inject
// AccessTokenCredential accessTokenCredential;
@GET
public String getReservationOnBehalfOfUser() {
String rawAccessToken = accessToken.getRawToken();
//or
//String rawAccessToken = accessTokenCredential.getToken();
// Use the raw access token to access a remote endpoint.
// For example, use RestClient to set this token as a `Bearer` scheme value of the HTTP `Authorization` header:
// `Authorization: Bearer rawAccessToken`.
return getReservationfromRemoteEndpoint(rawAccesstoken);
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
当授权代码流访问令牌注入为 JsonWebToken 时,除强制 ID 令牌验证外,会自动启用其验证。如果真正需要,您可以使用 quarkus.oidc.authentication.verify-access-token=false 禁用此代码流访问令牌验证。
如果发布到 Quarkus web-app 应用的访问令牌是不透明的,并且无法解析到 JsonWebToken,或者应用程序需要内内容,则使用 AccessTokenCredential。
@RequestScoped 和 @ApplicationScoped 上下文都支持 JsonWebToken 和 AccessTokenCredential 注入。
quarkus OIDC 使用刷新令牌来刷新当前的 ID 和访问令牌,作为其 会话管理 过程的一部分。
如果 ID 令牌没有提供有关当前经过身份验证的用户的足够信息,您可以从 UserInfo 端点获取更多信息。设置 quarkus.oidc.authentication.user-info-required=true 属性,从 OIDC UserInfo 端点请求 UserInfo JSON 对象。
使用通过授权代码授权响应返回的访问令牌将向 OIDC 提供程序 UserInfo 端点发送请求,以及一个 io.quarkus.oidc.UserInfo (一个简单的 jakarta.json.JsonObject wrapper)对象被创建。io.quarkus.oidc.UserInfo 可以作为 SecurityIdentity userinfo 属性注入或访问。
如果满足这些条件之一,则会自动启用 quarkus.oidc.authentication.user-info-required :
如果 quarkus.oidc.roles.source 设置为 userinfo 或 quarkus.oidc.token.verify-access-token-with-user-info,则为 true 或 quarkus.oidc.authentication.id-token-required 设置为 false,当前 OIDC 租户必须支持 UserInfo 端点。
如果检测到 io.quarkus.oidc.UserInfo 注入点,但只有当前 OIDC 租户支持 UserInfo 端点时才检测到。
角色从验证令牌映射到 SecurityIdentity 角色的方式与为 Bearer 令牌 的方式相同。唯一的区别是 ID 令牌 默认用作角色的来源。
如果使用 Keycloak,请为 ID 令牌设置 microprofile-jwt 客户端范围,使其包含 组 声明。如需更多信息,请参阅 Keycloak 服务器管理指南 。
但是,根据您的 OIDC 供应商,角色可能会存储在访问令牌或用户信息中。
如果访问令牌包含角色,并且此访问令牌不应传播到下游端点,则设置 quarkus.oidc.roles.source=accesstoken。
如果 UserInfo 是角色的来源,则设置 quarkus.oidc.roles.source=userinfo,如果需要,quarkus.oidc.roles.role-claim-path。
另外,您还可以使用自定义 SecurityIdentityAugmentor 来添加角色。如需更多信息,请参阅安全身份管理 自定义 。您还可以使用 HTTP 安全策略 将创建从令牌声明创建的 SecurityIdentity 角色映射到特定于部署的角色。
身份验证过程的一个核心部分是确保信息的信任链和有效性。这可以通过确保令牌可以被信任来实现。
代码流访问令牌不会被内省,除非它们应当是角色的来源。但是,它们将用于获取 UserInfo。如果令牌内省、UserInfo 或两者都需要,则需要一个或多个带有代码流访问令牌的远程调用。
有关使用默认令牌缓存或注册自定义缓存实现的更多信息,请参阅 令牌内省和用户Info 缓存 。
有关声明验证的详情,包括 iss (issuer)声明,请参阅 JSON Web 令牌声明验证 部分。如果 web-app 应用请求访问令牌验证,它适用于 ID 令牌以及 JWT 格式的访问令牌验证。
您可以注册自定义 [Jose4j Validator],以自定义 JWT 声明验证过程。如需更多信息,请参阅 Jose4j 部分。
代码 交换的证明密钥 (PKCE)可最大程度降低授权代码拦截器的风险。
虽然 PKCE 是公共 OIDC 客户端的主要重要性,如在浏览器中运行的 SPA 脚本,但它还可以提供对 Quarkus OIDC web-app 应用程序的额外保护。使用 PKCE 时,Quarkus OIDC web-app 应用程序充当机密 OIDC 客户端,这些客户端可以安全地存储客户端 secret,并使用它来交换令牌的代码。
您可以使用 quarkus.oidc.authentication.pkce-required 属性和 32 个字符的 secret 为 OIDC web-app 端点启用 PKCE 代码验证器,如下例所示:
quarkus.oidc.authentication.pkce-required=true
quarkus.oidc.authentication.state-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU
quarkus.oidc.authentication.pkce-required=true
quarkus.oidc.authentication.state-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
如果您已有 32 个字符的客户端 secret,则不需要设置 quarkus.oidc.authentication.pkce-secret 属性,除非您更喜欢使用不同的 secret 密钥。如果没有配置此 secret,且在客户端 secret 小于 16 个字符时,无法自动生成此 secret。
当用户使用 code_challenge 查询参数重定向到 OIDC 供应商时,需要加密随机生成的 PKCE code_verifier。当用户重定向到 Quarkus 时,code_verifier 会解密,并发送到令牌端点,代码、客户端 secret 和其他参数来完成代码交换。如果 code_verifier 的 SHA256 摘要与身份验证请求过程中提供的 code_challenge 不匹配,则供应商将失败。
身份验证的另一个重要要求是确保会话基于的数据是最新的,而不需要用户为每个请求进行身份验证。在有些情况下,明确请求 logout 事件。使用以下关键点查找保护 Quarkus 应用程序的正确平衡:
OIDC 适配器使用 Cookie 来保留会话、代码流和超时状态。这个状态是控制身份验证数据生命周期的关键元素。
使用 quarkus.oidc.authentication.cookie-path 属性来确保当您访问具有重叠或不同根的受保护的资源时,可以看到相同的 Cookie。例如:
/index.html and /web-app/service
/web-app/service1 and /web-app/service2
/web-app1/service and /web-app2/service
默认情况下,quarkus.oidc.authentication.cookie-path 被设置为 /,但如果需要,您可以将它改为更具体的路径,例如 /web-app。
要动态设置 Cookie 路径,请配置 quarkus.oidc.authentication.cookie-path-header 属性。例如,要使用 X-Forwarded-Prefix HTTP 标头的值动态设置 Cookie 路径,请将 属性配置为 quarkus.oidc.authentication.cookie-path-header=X-Forwarded-Prefix。
如果设置了 quarkus.oidc.authentication.cookie-path-header,但当前请求中没有配置的 HTTP 标头,则会检查 quarkus.oidc.authentication.cookie-path。
如果您的应用程序在多个域中部署,请设置 quarkus.oidc.authentication.cookie-domain 属性,以便会话 Cookie 对所有受保护的 Quarkus 服务可见。例如,如果您在以下两个域中部署了 Quarkus 服务,您必须将 quarkus.oidc.authentication.cookie-domain 属性设置为 company.net :
https://whatever.wherever.company.net/
https://another.address.company.net/
State Cookie 用于支持授权代码流完成。当授权代码流启动时,Quarkus 会在将用户重定向到 OIDC 供应商前创建一个 状态 Cookie 和匹配的状态 查询参数。当用户重定向到 Quarkus 以完成授权代码流时,Quarkus 预期请求 URI 必须包含 state 查询参数,它必须与当前状态 Cookie 值匹配。
默认状态 Cookie 期限为 5 分钟,您可以使用 quarkus.oidc.authentication.state-cookie-age Duration 属性更改它。
当每次启动新的授权代码流来支持多选项卡身份验证时,quarkus 都会创建一个唯一的状态 Cookie 名称。代表同一用户的许多并发身份验证请求可能会导致大量状态 Cookie。如果您不想允许用户使用多个浏览器标签页进行身份验证,则建议使用 quarkus.oidc.authentication.allow-multiple-code-flows=false 禁用它。它还确保为每个新的用户身份验证创建相同的状态 Cookie 名称。
OIDC CodeAuthenticationMechanism 使用默认的 io.quarkus.oidc.TokenStateManager 接口实现,以保留授权代码中返回的 ID、访问和刷新令牌,或在加密会话 Cookie 中刷新授权响应。
它使 Quarkus OIDC 端点完全无状态,建议遵循此策略来实现最佳的可扩展性结果。
有关令牌存储的替代方法,请参阅 Session cookie 和 custom TokenStateManager 部分。这对于那些希望自定义解决方案进行令牌状态管理的理想选择,特别是当标准服务器端存储不符合您的特定要求时。
您可以配置默认的 TokenStateManager,以避免在会话 Cookie 中保存访问令牌,并只保留 ID 和刷新令牌或单个 ID 令牌。
只有在端点需要执行以下操作时才需要访问令牌:
检索 UserInfo
使用这个访问令牌访问下游服务
使用与访问令牌关联的角色,这些角色会被默认检查
在这种情况下,使用 quarkus.oidc.token-state-manager.strategy 属性来配置令牌状态策略,如下所示:
Expand to… 将属性设置为 …
只保留 ID 和刷新令牌
quarkus.oidc.token-state-manager.strategy=id-refresh-tokens
仅保留 ID 令牌
quarkus.oidc.token-state-manager.strategy=id-token
Show more
如果您所选的会话 Cookie 策略组合了令牌并生成大于 4KB 的大型会话 Cookie 值,则一些浏览器可能无法处理此类 Cookie 大小。当 ID、访问和刷新令牌是 JWT 令牌且所选策略为 keep-all-tokens 时,或者在策略 id-refresh-token 时使用 ID 和刷新令牌会出现这种情况。要临时解决这个问题,您可以设置 quarkus.oidc.token-state-manager.split-tokens=true 来为每个令牌创建一个唯一的会话令牌。
默认 TokenStateManager 会加密令牌,然后再将其存储在会话 Cookie 中。以下示例演示了如何将其配置为分割令牌并加密它们:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.oidc.token-state-manager.split-tokens=true
quarkus.oidc.token-state-manager.encryption-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.oidc.token-state-manager.split-tokens=true
quarkus.oidc.token-state-manager.encryption-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
令牌加密 secret 必须至少为 32 个字符。如果没有配置此密钥,则 quarkus.oidc.credentials.secret 或 quarkus.oidc.credentials.jwt.secret 将会被哈希来创建加密密钥。
如果 Quarkus 使用以下身份验证方法之一向 OIDC 供应商进行身份验证,请配置 quarkus.oidc.token-state-manager.encryption-secret 属性:
mTLS
private_key_jwt,其中使用私有 RSA 或 EC 密钥为 JWT 令牌签名
否则,会生成一个随机密钥,如果 Quarkus 应用程序在管理请求的多个 pod 在云中运行,则可能会造成问题。
您可以通过设置 quarkus.oidc.token-state-manager.encryption-required=false 来禁用会话 Cookie 中的令牌加密。
如果要自定义令牌与会话 Cookie 关联的方式,请注册自定义 io.quarkus.oidc.TokenStateManager 实现作为 @ApplicationScoped CDI bean。
例如,您可能想要将令牌保留在缓存集群中,且只有一个密钥存储在会话 Cookie 中。请注意,如果您需要在多个微服务节点上提供令牌,这种方法可能会带来一些挑战。
下面是一个简单的示例:
package io.quarkus.oidc.test;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import jakarta.inject.Inject;
import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenStateManager;
import io.quarkus.oidc.runtime.DefaultTokenStateManager;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
@Alternative
@Priority(1)
public class CustomTokenStateManager implements TokenStateManager {
@Inject
DefaultTokenStateManager tokenStateManager;
@Override
public Uni<String> createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig,
AuthorizationCodeTokens sessionContent, OidcRequestContext<String> requestContext) {
return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent, requestContext)
.map(t -> (t + "|custom"));
}
@Override
public Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig,
String tokenState, OidcRequestContext<AuthorizationCodeTokens> requestContext) {
if (!tokenState.endsWith("|custom")) {
throw new IllegalStateException();
}
String defaultState = tokenState.substring(0, tokenState.length() - 7);
return tokenStateManager.getTokens(routingContext, oidcConfig, defaultState, requestContext);
}
@Override
public Uni<Void> deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState,
OidcRequestContext<Void> requestContext) {
if (!tokenState.endsWith("|custom")) {
throw new IllegalStateException();
}
String defaultState = tokenState.substring(0, tokenState.length() - 7);
return tokenStateManager.deleteTokens(routingContext, oidcConfig, defaultState, requestContext);
}
}
package io.quarkus.oidc.test;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import jakarta.inject.Inject;
import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenStateManager;
import io.quarkus.oidc.runtime.DefaultTokenStateManager;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
@Alternative
@Priority(1)
public class CustomTokenStateManager implements TokenStateManager {
@Inject
DefaultTokenStateManager tokenStateManager;
@Override
public Uni<String> createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig,
AuthorizationCodeTokens sessionContent, OidcRequestContext<String> requestContext) {
return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent, requestContext)
.map(t -> (t + "|custom"));
}
@Override
public Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig,
String tokenState, OidcRequestContext<AuthorizationCodeTokens> requestContext) {
if (!tokenState.endsWith("|custom")) {
throw new IllegalStateException();
}
String defaultState = tokenState.substring(0, tokenState.length() - 7);
return tokenStateManager.getTokens(routingContext, oidcConfig, defaultState, requestContext);
}
@Override
public Uni<Void> deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState,
OidcRequestContext<Void> requestContext) {
if (!tokenState.endsWith("|custom")) {
throw new IllegalStateException();
}
String defaultState = tokenState.substring(0, tokenState.length() - 7);
return tokenStateManager.deleteTokens(routingContext, oidcConfig, defaultState, requestContext);
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
有关将令牌存储在加密会话 Cookie 中的默认 TokenStateManager 的详情,请参考 Session cookie 和 default TokenStateManager 。
身份验证信息过期的方法有两种:令牌过期且没有续订或显式退出操作。
我们从明确注销操作开始。
用户可以通过向 Quarkus 端点注销路径发送请求,该路径使用 quarkus.oidc.logout.path 属性来请求注销。例如,如果端点地址为 https://application.com/webapp ,并且 quarkus.oidc.logout.path 设置为 /logout,则必须将 logout 请求发送到 https://application.com/webapp/logout。
此注销请求将启动一个 RP-initiated logout 。用户将被重定向到 OIDC 供应商以注销,并要求他们确认注销是否确实是正常的。
在注销完成后,用户将返回到端点 post-logout 页面,并且设置了 quarkus.oidc.logout.post-logout-path 属性。例如,如果端点地址为 https://application.com/webapp ,并且 quarkus.oidc.logout.post-logout-path 设置为 /signin,则用户将返回到 https://application.com/webapp/signin。https://application.com/webapp/signin 请注意,此 URI 必须注册为 OIDC 供应商中的有效的 post_logout_redirect_uri。
如果设置了 quarkus.oidc.logout.post-logout-path,则会创建一个 q_post_logout cookie,并将匹配的 state 查询参数添加到注销重定向 URI 中,OIDC 供应商会在注销完成后返回此状态。建议 Quarkus web-app 应用程序检查 state 查询参数是否与 q_post_logout cookie 的值匹配,例如在 Jakarta REST 过滤器中。
请注意,在使用 OpenID Connect Multi-Tenancy 时,cookie 名称会有所不同。例如,它将被命名为 q_post_logout_tenant_1,用于具有 tenant_1 ID 的租户,以此类推。
以下是如何配置 Quarkus 应用程序以启动注销流程的示例:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=frontend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.oidc.logout.path=/logout
# Logged-out users should be returned to the /welcome.html site which will offer an option to re-login:
quarkus.oidc.logout.post-logout-path=/welcome.html
# Only the authenticated users can initiate a logout:
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated
# All users can see the Welcome page:
quarkus.http.auth.permission.public.paths=/welcome.html
quarkus.http.auth.permission.public.policy=permit
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=frontend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.oidc.logout.path=/logout
# Logged-out users should be returned to the /welcome.html site which will offer an option to re-login:
quarkus.oidc.logout.post-logout-path=/welcome.html
# Only the authenticated users can initiate a logout:
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated
# All users can see the Welcome page:
quarkus.http.auth.permission.public.paths=/welcome.html
quarkus.http.auth.permission.public.policy=permit
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
您可能还希望将 quarkus.oidc.authentication.cookie-path 设置为对所有应用程序资源通用的路径值,本例中为 /。如需更多信息,请参阅 Cookies 部分。
有些 OIDC 供应商不支持 RP-initiated logout 规格,且不会返回 OpenID Connect 已知的 end_session_endpoint 元数据属性。但是,这不是 Quarkus 的问题,因为此类 OIDC 供应商的特定注销机制仅在如何命名 logout URL 查询参数时有所不同。
根据 RP-initiated logout 规格,quarkus.oidc.logout.post-logout-path 属性表示为 post_logout_redirect_uri 查询参数,它不被不支持此规格的供应商识别。
您可以使用 quarkus.oidc.logout.post-logout-url-param 来临时解决这个问题。您还可以使用 quarkus.oidc.logout.extra-params 添加请求更多注销查询参数。例如,以下是如何使用 Auth0 支持注销:
quarkus.oidc.auth-server-url=https://dev-xxx.us.auth0.com
quarkus.oidc.client-id=redacted
quarkus.oidc.credentials.secret=redacted
quarkus.oidc.application-type=web-app
quarkus.oidc.tenant-logout.logout.path=/logout
quarkus.oidc.tenant-logout.logout.post-logout-path=/welcome.html
# Auth0 does not return the `end_session_endpoint` metadata property. Instead, you must configure it:
quarkus.oidc.end-session-path=v2/logout
# Auth0 will not recognize the 'post_logout_redirect_uri' query parameter so ensure it is named as 'returnTo':
quarkus.oidc.logout.post-logout-uri-param=returnTo
# Set more properties if needed.
# For example, if 'client_id' is provided, then a valid logout URI should be set as the Auth0 Application property, without it - as Auth0 Tenant property:
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
quarkus.oidc.auth-server-url=https://dev-xxx.us.auth0.com
quarkus.oidc.client-id=redacted
quarkus.oidc.credentials.secret=redacted
quarkus.oidc.application-type=web-app
quarkus.oidc.tenant-logout.logout.path=/logout
quarkus.oidc.tenant-logout.logout.post-logout-path=/welcome.html
# Auth0 does not return the `end_session_endpoint` metadata property. Instead, you must configure it:
quarkus.oidc.end-session-path=v2/logout
# Auth0 will not recognize the 'post_logout_redirect_uri' query parameter so ensure it is named as 'returnTo':
quarkus.oidc.logout.post-logout-uri-param=returnTo
# Set more properties if needed.
# For example, if 'client_id' is provided, then a valid logout URI should be set as the Auth0 Application property, without it - as Auth0 Tenant property:
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
OIDC 供应商可以使用身份验证数据强制注销所有应用程序。这称为 back-channel logout。在这种情况下,OIDC 将调用每个应用程序中的一个特定 URL 来触发该退出。
OIDC 供应商使用 Back-channel logout 从当前用户登录的所有应用程序注销,绕过用户代理。
您可以将 Quarkus 配置为支持 Back-channel logout,如下所示:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=frontend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.oidc.logout.backchannel.path=/back-channel-logout
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=frontend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.oidc.logout.backchannel.path=/back-channel-logout
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
absolute back-channel logout URL 通过向当前端点 URL 添加 quarkus.oidc.back-channel-logout.path 来计算,例如 http://localhost:8080/back-channel-logout 。您需要在 OIDC 供应商的管理控制台中配置此 URL。
如果您的 OIDC 供应商没有在当前注销令牌中设置到期声明,您还需要为退出令牌验证配置令牌 age 属性。例如,设置 quarkus.oidc.token.age=10S,以确保自注销令牌的 iat (在)时间后没有超过 10 秒。
您可以使用 Front-channel logout 直接从用户代理注销当前用户,例如其浏览器。它与 Back-channel logout 类似,但注销步骤由用户代理(如浏览器)执行,而不是在 OIDC 供应商在后台执行。这个选项很少被使用。
您可以配置 Quarkus 以支持 Front-channel logout,如下所示:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=frontend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.oidc.logout.frontchannel.path=/front-channel-logout
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=frontend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.oidc.logout.frontchannel.path=/front-channel-logout
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
此路径将与当前请求的路径进行比较,如果这些路径匹配,用户将被注销。
User-initiated logout 将从 OIDC 供应商注销用户。如果将其用作单点登录,这可能不是您需要的。例如,如果您的 OIDC 供应商是 Google,您将从 Google 及其服务中登出。相反,用户可能只希望退出该特定应用程序。另一个用例可能是 OIDC 供应商没有注销端点。
通过使用 OidcSession ,您可以支持本地注销,这意味着只清除本地会话 cookie,如下例所示:
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.quarkus.oidc.OidcSession;
@Path("/service")
public class ServiceResource {
@Inject
OidcSession oidcSession;
@GET
@Path("logout")
public String logout() {
oidcSession.logout().await().indefinitely();
return "You are logged out";
}
}
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.quarkus.oidc.OidcSession;
@Path("/service")
public class ServiceResource {
@Inject
OidcSession oidcSession;
@GET
@Path("logout")
public String logout() {
oidcSession.logout().await().indefinitely();
return "You are logged out";
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
io.quarkus.oidc.OidcSession 是当前 IdToken 的打包程序,可帮助执行 本地注销 ,检索当前会话的租户标识符,并检查会话何时过期。随着时间的推移,将向它添加更有用的方法。
默认情况下,注销基于 OIDC 供应商发布的 ID 令牌的过期时间。当 ID 令牌过期时,Quarkus 端点中的当前用户会话无效,用户会被重新重定向到 OIDC 供应商以进行身份验证。如果 OIDC 供应商中的会话仍然处于活跃状态,则用户会自动重新验证,而无需再次提供其凭证。
通过启用 quarkus.oidc.token.refresh-expired 属性,可以自动扩展当前用户会话。如果设置为 true,当当前 ID 令牌过期时,将使用刷新令牌授权来刷新 ID 令牌,以及访问和刷新令牌。
如果您使用 Quarkus OIDC web-app 应用程序,则 Quarkus OIDC 代码身份验证机制管理用户会话生命周期。
要使用刷新令牌,您应该仔细配置会话 Cookie 年龄。会话年龄应超过 ID 令牌寿命,并且接近或等于刷新令牌生命周期。
您可以通过添加当前 ID 令牌的 lifespan 值以及 quarkus.oidc. authentication.session-age-extension 和 quarkus.oidc.token.lifespan-grace 属性的值来计算会话年龄。
如果需要,只使用 quarkus.oidc.authentication.session-age-extension 属性来显著扩展会话生命周期。您可以使用 quarkus.oidc.token.lifespan-grace 属性来考虑一些小时钟偏移。
当当前经过身份验证的用户返回到受保护的 Quarkus 端点并且与会话 Cookie 关联的 ID 令牌已过期时,默认情况下,用户会自动重定向到 OIDC Authorization 端点来重新验证。如果用户和此 OIDC 供应商间的会话仍然活跃,OIDC 供应商可能会再次挑战用户,如果会话配置为最后的时间超过 ID 令牌,则可能会出现这种情况。
如果 quarkus.oidc.token.refresh-expired 设为 true,则使用返回初始授权代码的刷新令牌刷新过期的 ID 令牌(和访问令牌)。此刷新令牌也可能被回收(刷新)本身作为此过程的一部分。因此,会创建新的会话 Cookie,会话会被扩展。
在用户未激活的情况下,您可以使用 quarkus.oidc.authentication.session-age-extension 属性来帮助处理过期的 ID 令牌。如果 ID 令牌过期,则会话 Cookie 可能不会在下一个用户请求期间返回到 Quarkus 端点,因为 Cookie 生命周期已过。Quarkus 假设此请求是第一个身份验证请求。将 quarkus.oidc.authentication.session-age-extension 设置为您的裸机用户,并根据您的安全策略。
您可以进一步进入一个步骤,并主动刷新 ID 令牌或访问令牌过期。将 quarkus.oidc.token.refresh-token-time-skew 设置为您要预计刷新的值。如果在当前用户请求过程中,计算当前 ID 令牌将在此 quarkus.oidc.token.refresh-token-time-skew 中过期,然后被刷新,并创建新的会话 cookie。此属性应设置为小于 ID 令牌生命周期的值;更接近这个生命周期值,ID 令牌会被刷新的频率。
您可以通过具有简单的 JavaScript 功能定期 ping Quarkus 端点来进一步优化这个过程,以模拟用户活动,从而减少用户必须重新验证的时间框架。
当无法刷新会话时,当前经过身份验证的用户会被重定向到 OIDC 供应商来重新验证。但是,如果用户在早期成功身份验证后,用户体验可能并不理想。在尝试访问应用程序页面时,用户会突然看到 OIDC 身份验证质询屏幕。
相反,您可以请求用户重定向到公共的、特定于应用程序的会话过期页面。此页面告知用户会话现已过期,并建议按照到安全应用程序欢迎页面的链接重新进行身份验证。用户点击链接和 Quarkus OIDC 强制执行到 OIDC 供应商重新验证的重定向。如果您想这样做,请使用 quarkus.oidc.authentication.session-expired-page 相对路径属性。
例如,设置 quarkus.oidc.authentication.session-expired-page=/session-expired-page 将确保其会话已过期的用户被重定向到 http://localhost:8080/session-expired-page ,假设应用程序位于 http://localhost:8080 。
另请参阅 OIDC 重定向过滤器 部分来说明如何使用自定义 OidcRedirectFilter 自定义 OIDC 重定向,包括会话过期的页面。
您无法无限期扩展用户会话。当刷新令牌已过期后,在 OIDC 供应商端点中的返回用户必须在 OIDC 供应商端点中重新进行身份验证。
GitHub 或 LinkedIn 等一些已知的供应商不是 OpenID Connect 供应商,但支持 授权代码流的 OAuth2 供应商。例如,GitHub OAuth2 和 LinkedIn OAuth2 。请记住,OIDC 基于 OAuth2 构建。
OIDC 和 OAuth2 供应商的主要区别在于,除了标准授权代码流 访问和 刷新 OAuth2 供应商返回的令牌外,OIDC 供应商还会返回一个代表用户身份验证的 ID 令牌。
GitHub 等 OAuth2 提供程序不返回 IdToken,用户身份验证是隐式的,间接由 访问令牌 表示。此 访问令牌 代表了一个经过身份验证的用户,授权当前的 Quarkus web-app 应用代表经过身份验证的用户访问某些数据。
对于 OIDC,您可以将 ID 令牌验证为身份验证的有效性,而在 OAuth2 时验证访问令牌。这随后通过调用需要访问令牌的端点,并且通常会返回用户信息来完成。这个方法与 OIDC UserInfo 方法类似,代表 Quarkus OIDC 获取的 UserInfo。
例如,在使用 GitHub 时,Quarkus 端点可以 获取访问令牌,它允许 Quarkus 端点为当前用户请求 GitHub 配置集。
要支持与这样的 OAuth2 服务器的集成,quarkus-oidc 需要以不同的方式配置,以允许在没有 IdToken:quarkus.oidc.authentication.id-token-required=false 的情况下配置授权代码流响应。
虽然您要将扩展配置为支持没有 IdToken 的授权代码流,但会生成内部 IdToken 来标准化 quarkus-oidc 操作的方式。您可以使用内部 IdToken 来支持身份验证会话,并避免在每个请求上将用户重定向到提供程序,如 GitHub。在这种情况下,IdToken age 设置为授权代码流响应中的标准 expires_in 属性的值。您可以使用 quarkus.oidc.authentication.internal-id-token-lifespan 属性来自定义 ID 令牌年龄。默认 ID 令牌期限为 5 分钟,您可以进一步进行扩展,如 会话管理部分中所述 。
这简化了如何处理支持多个 OIDC 供应商的应用程序。
下一步是确保返回的访问令牌很有用,并且对当前 Quarkus 端点有效。第一种方法是,如果提供商提供了这样的端点,则通过配置 quarkus.oidc.introspection-path 来调用 OAuth2 提供程序内省端点。在这种情况下,您可以使用 quarkus.oidc.roles.source=accesstoken 将访问令牌用作角色源。如果没有内省端点,您可以尝试从提供程序请求 UserInfo ,因为它至少将验证访问令牌。为此,请指定 quarkus.oidc.token.verify-access-token-with-user-info=true。您还需要将 quarkus.oidc.user-info-path 属性设置为 URL 端点,该端点获取用户信息(或访问令牌保护的端点)。对于 GitHub,因为它没有内省端点,需要请求 UserInfo。
需要 UserInfo 涉及在每个请求上进行远程调用。
因此,UserInfo 嵌入到内部生成的 IdToken 中,并保存在加密的会话 Cookie 中。它可以通过 quarkus.oidc.cache-user-info-in-idtoken=false 来禁用。
或者,您可能想要考虑使用默认或自定义 UserInfo 缓存供应商缓存 UserInfo。如需更多信息,请参阅"OpenID Connect (OIDC) Bearer 令牌身份验证"指南中的 Token Introspection 和 UserInfo 缓存 部分。
最知名的社交 OAuth2 供应商强制速率限制,因此您希望将 UserInfo 缓存为几率。
OAuth2 服务器可能不支持已知的配置端点。在这种情况下,您必须禁用发现并配置授权、令牌和内省和 UserInfo 端点路径。
对于知名的 OIDC 或 OAuth2 供应商,如 Apple、Facebook、GitHub、Google、Microsoft、Spotify 和 X (以前为 monitoring), Quarkus 可使用 quarkus.oidc.provider 属性简化应用程序的配置。以下是您可以在创建 GitHub OAuth 应用程序后将 quarkus-oidc 与 GitHub 集成。配置 Quarkus 端点,如下所示:
quarkus.oidc.provider=github
quarkus.oidc.client-id=github_app_clientid
quarkus.oidc.credentials.secret=github_app_clientsecret
# user:email scope is requested by default, use 'quarkus.oidc.authentication.scopes' to request different scopes such as `read:user`.
# See https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps for more information.
# Consider enabling UserInfo Cache
# quarkus.oidc.token-cache.max-size=1000
# quarkus.oidc.token-cache.time-to-live=5M
#
# Or having UserInfo cached inside IdToken itself
# quarkus.oidc.cache-user-info-in-idtoken=true
quarkus.oidc.provider=github
quarkus.oidc.client-id=github_app_clientid
quarkus.oidc.credentials.secret=github_app_clientsecret
# user:email scope is requested by default, use 'quarkus.oidc.authentication.scopes' to request different scopes such as `read:user`.
# See https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps for more information.
# Consider enabling UserInfo Cache
# quarkus.oidc.token-cache.max-size=1000
# quarkus.oidc.token-cache.time-to-live=5M
#
# Or having UserInfo cached inside IdToken itself
# quarkus.oidc.cache-user-info-in-idtoken=true
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
有关配置其他已知的供应商的更多信息,请参阅 OpenID Connect 供应商 。
这是端点所需要的,如这个端点使用 GET http://localhost:8080/github/userinfo 返回当前验证的用户配置集,并将其作为单独的 UserInfo 属性访问:
package io.quarkus.it.keycloak;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.Authenticated;
@Path("/github")
@Authenticated
public class TokenResource {
@Inject
UserInfo userInfo;
@GET
@Path("/userinfo")
@Produces("application/json")
public String getUserInfo() {
return userInfo.getUserInfoString();
}
}
package io.quarkus.it.keycloak;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.Authenticated;
@Path("/github")
@Authenticated
public class TokenResource {
@Inject
UserInfo userInfo;
@GET
@Path("/userinfo")
@Produces("application/json")
public String getUserInfo() {
return userInfo.getUserInfoString();
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
如果您支持多个社交供应商,它带有 OpenID Connect Multi-Tenancy 的帮助,例如 Google,它是一个 OIDC 供应商,返回 IdToken 和 GitHub,它是一个 OAuth2 供应商,它没有返回 IdToken,只允许访问 UserInfo,然后您可以让端点与 Google 和 GitHub 流注入的 SecurityIdentity 工作。当 GitHub 流处于活跃状态时,需要一个简单的 SecurityIdentity,其中使用内部生成的 IdToken 创建的主体将替换为基于 UserInfo的主体:
package io.quarkus.it.keycloak;
import java.security.Principal;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
RoutingContext routingContext = identity.getAttribute(RoutingContext.class.getName());
if (routingContext != null && routingContext.normalizedPath().endsWith("/github")) {
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
UserInfo userInfo = identity.getAttribute("userinfo");
builder.setPrincipal(new Principal() {
@Override
public String getName() {
return userInfo.getString("preferred_username");
}
});
identity = builder.build();
}
return Uni.createFrom().item(identity);
}
}
package io.quarkus.it.keycloak;
import java.security.Principal;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
RoutingContext routingContext = identity.getAttribute(RoutingContext.class.getName());
if (routingContext != null && routingContext.normalizedPath().endsWith("/github")) {
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
UserInfo userInfo = identity.getAttribute("userinfo");
builder.setPrincipal(new Principal() {
@Override
public String getName() {
return userInfo.getString("preferred_username");
}
});
identity = builder.build();
}
return Uni.createFrom().item(identity);
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
现在,当用户使用 Google 或 GitHub 登录应用程序时,以下代码将可以正常工作:
package io.quarkus.it.keycloak;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
@Path("/service")
@Authenticated
public class TokenResource {
@Inject
SecurityIdentity identity;
@GET
@Path("/google")
@Produces("application/json")
public String getGoogleUserName() {
return identity.getPrincipal().getName();
}
@GET
@Path("/github")
@Produces("application/json")
public String getGitHubUserName() {
return identity.getPrincipal().getName();
}
}
package io.quarkus.it.keycloak;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
@Path("/service")
@Authenticated
public class TokenResource {
@Inject
SecurityIdentity identity;
@GET
@Path("/google")
@Produces("application/json")
public String getGoogleUserName() {
return identity.getPrincipal().getName();
}
@GET
@Path("/github")
@Produces("application/json")
public String getGitHubUserName() {
return identity.getPrincipal().getName();
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
可能更为简单的替代方案是注入 @IdToken JsonWebToken 和 UserInfo,并在处理返回 IdToken 的提供程序时使用 JsonWebToken,并将 UserInfo 与不返回 IdToken 的提供程序一起使用。
您必须确保您在 GitHub OAuth 应用程序配置中输入的回调路径与希望在 GitHub 身份验证和应用程序授权成功后重定向用户的端点路径匹配。在这种情况下,它必须设置为 http://localhost:8080/github/userinfo 。
您可以注册 @ApplicationScoped bean,它将观察重要的 OIDC 身份验证事件。当用户第一次登录时,重新验证或刷新会话时,会更新监听程序。未来可能会报告更多事件。例如:
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import io.quarkus.oidc.SecurityEvent;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class SecurityEventListener {
public void event(@Observes SecurityEvent event) {
String tenantId = event.getSecurityIdentity().getAttribute("tenant-id");
RoutingContext vertxContext = event.getSecurityIdentity().getAttribute(RoutingContext.class.getName());
vertxContext.put("listener-message", String.format("event:%s,tenantId:%s", event.getEventType().name(), tenantId));
}
}
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import io.quarkus.oidc.SecurityEvent;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class SecurityEventListener {
public void event(@Observes SecurityEvent event) {
String tenantId = event.getSecurityIdentity().getAttribute("tenant-id");
RoutingContext vertxContext = event.getSecurityIdentity().getAttribute(RoutingContext.class.getName());
vertxContext.put("listener-message", String.format("event:%s,tenantId:%s", event.getEventType().name(), tenantId));
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow