OpenID Connect authorization code flow mechanism for protecting web applications

Overview of the OIDC authorization code flow mechanism

Quarkus OpenID Connect (OIDC) 可以使用 OIDC 授权服务器支持的 OIDC 授权码流程机制来保护应用程序 HTTP 终端,例如 Keycloak

授权码流程机制通过将用户重定向到 OIDC 提供程序(比如 Keycloak)来验证你的 Web 应用程序的用户身份。在验证之后,OIDC 提供程序会通过授权码将用户重定向回应用程序,以确认已经成功验证身份。然后,应用程序会将此代码与 OIDC 提供程序交换身份令牌(代表已验证的用户)、访问令牌和刷新令牌以授权用户访问应用程序。

下图概述了 Quarkus 中的授权码流程机制。

authorization code flow
Figure 1. Authorization code flow mechanism in Quarkus
  1. Quarkus 用户请求访问 Quarkus web-app 应用程序。

  2. Quarkus Web 应用程序将用户重定向到授权终端,即用于验证的 OIDC 提供程序。

  3. OIDC 提供程序将用户重定向到一个登录和验证提示中。

  4. 在提示中,用户输入他们的用户凭证。

  5. OIDC 提供程序验证输入的用户凭证,如果验证通过,则会发布授权码并将用户连同作为查询参数包含的代码重定向回 Quarkus Web 应用程序。

  6. Quarkus Web 应用程序会将此授权码与 OIDC 提供程序交换身份、访问和刷新令牌。

授权码流程完成,Quarkus Web 应用程序会使用发出的令牌访问有关用户的信息,并将相关的基于角色的授权授予该用户。以下令牌会发出:

  • ID 令牌:Quarkus `web-app`应用程序使用 ID 令牌中的用户信息,以支持经过身份验证的用户安全登录,并提供基于角色的网络应用程序访问权限。

  • 访问令牌:Quarkus 网页应用程序可以使用访问令牌来访问 UserInfo API,以便获取有关已通过身份验证的用户的其他信息或将其传播到另一个端点。

  • 刷新令牌:(可选)如果 ID 和访问令牌过期,Quarkus 网页应用程序可以使用刷新令牌来获取新的 ID 和访问令牌。

请参阅 OIDC configuration properties参考指南。

如需了解如何使用 OIDC 授权代码流机制来保护网络应用程序,请参阅 Protect a web application by using OIDC authorization code flow

如果您想要使用 OIDC Bearer 令牌身份验证来保护服务应用程序,请参阅 OIDC Bearer token authentication

如需了解有关如何支持多租户的信息,请参阅 Using OpenID Connect Multi-Tenancy

Using the authorization code flow mechanism

Configuring access to the OIDC provider endpoint

OIDC web-app`应用程序需要 OIDC 提供商的授权、令牌、`JsonWebKey (JWK) 设置和可能有的 UserInfo、内省和终止会话 (RP 发起的注销) 端点的 URL。

惯例上,通过向已配置的 `quarkus.oidc.auth-server-url`添加 `/.well-known/openid-configuration`路径来发现它们。

或者,如果发现端点不可用,或者您更希望减少发现端点往返次数,您可以禁用端点发现并配置相对路径值。例如:

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

一些 OIDC 提供程序支持元数据发现,但不会返回完成授权代码流或支持应用程序功能(例如用户注销)所需的所有端点 URL 值。为解决此限制,您可以按照以下示例中所述的方式在本地配置缺失的端点 URL 值:

# 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

您可以使用相同的配置来覆盖已发现的端点 URL,如果该 URL 不适用于本地 Quarkus 端点且需要更特定的值时。例如,支持全局和特定于应用程序的终止会话端点的提供程序返回全局终止会话的 URL,例如 http://localhost:8180/oidcprovider/account/global-logout。此 URL 将注销用户当前已登录的所有应用程序。但是,如果要求当前应用程序仅注销用户特定的应用程序,您可以通过设置 `quarkus.oidc.end-session-path=logout`参数来覆盖全局终止会话 URL。

OIDC provider client authentication

OIDC 提供程序通常要求应用程序在与 OIDC 端点交互时进行身份识别和身份验证。Quarkus OIDC(尤其是 `quarkus.oidc.runtime.OidcProviderClient`类)在必须将授权代码交换为 ID、访问和刷新令牌时或者必须刷新或内省 ID 和访问令牌时对 OIDC 提供程序进行身份验证。

通常,在给定应用程序向 OIDC 提供商注册时,会为此定义客户端 ID 和客户端机密。支持所有 OIDC client authentication选项。例如:

Example of client_secret_basic:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.secret=mysecret

或者:

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.client-secret.value=mysecret

以下示例演示从 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
Example of client_secret_post
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.credentials.client-secret.method=post
Example of client_secret_jwt, where the signature algorithm is HS256:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow
Example of client_secret_jwt, where the secret is retrieved from a 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.jwt.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.jwt.secret-provider.name=oidc-credentials-provider

使用应用程序中内联的 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

使用 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
Example of private_key_jwt with the keystore file, where the signature algorithm is RS256:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.key-store-file=keystore.jks
quarkus.oidc.credentials.jwt.key-store-password=mypassword
quarkus.oidc.credentials.jwt.key-password=mykeypassword

# Private key alias inside the keystore
quarkus.oidc.credentials.jwt.key-id=mykeyAlias

使用 `client_secret_jwt`或 `private_key_jwt`身份验证方法可以确保不会将客户端机密发送到 OIDC 提供商,因而避免了机密被“中间人”攻击拦截的风险。

Additional JWT authentication options

如果使用 client_secret_jwt、`private_key_jwt`或 Apple `post_jwt`认证方法,那么您可以自定义 JWT 签名算法、密钥标识符、受众、主体和发布者。例如:

# 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

Apple POST JWT

Apple OIDC 提供商使用 `client_secret_post`方法,其中秘密是一个使用 `private_key_jwt`认证方法生成的 JWT,但包含 Apple 账户特定的发布者和主体声明。

在 Quarkus Security 中,`quarkus-oidc`支持非标准 `client_secret_post_jwt`认证方法,您可以如下配置:

# 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}

mutual TLS (mTLS)

一些 OIDC 提供商可能要求客户端通过双向 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

POST query

一些提供商(例如 Strava OAuth2 provider)要求将客户端凭据作为 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

Introspection endpoint authentication

一些 OIDC 提供商要求使用基本认证且凭据不同于 `client_id`和 `client_secret`的机制对认证终结点进行认证。如果您之前已配置安全认证以支持 `client_secret_basic`或 `client_secret_post`客户端认证方法(如 OIDC provider client authentication部分中所述),则可能需要如下应用额外配置。

如果必须检查令牌并且需要检查终结点特定的认证机制,则可以如下配置 quarkus-oidc

quarkus.oidc.introspection-credentials.name=introspection-user-name
quarkus.oidc.introspection-credentials.secret=introspection-user-secret

OIDC request filters

您可以通过注册一个或多个 `OidcRequestFilter`实现来过滤 Quarkus 向 OIDC 提供商发出的 OIDC 请求,这些实现可以更新或添加新的请求头,还可以记录请求。

例如:

package io.quarkus.it.keycloak;

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
    }
}
1 获取 OidcConfigurationMetadata,其中包含所有受支持的 OIDC 终结点地址。
2 使用 `OidcConfigurationMetadata`仅过滤对 OIDC 令牌终结点的请求。

或者,您可以使用 `OidcRequestFilter.Endpoint`枚举仅将此过滤器应用于令牌终结点请求:

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");
    }
}
1 仅将此过滤器限制为针对 OIDC 发现终结点的请求。

Redirecting to and from the OIDC provider

当将用户重定向到 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`对于给定的应用程序具有相同的值(例如,对于所有重定向 URL,`http://localhost:8080/service/callback)。在这种情况下,必须设置 quarkus.oidc.authentication.redirect-path`属性。例如,`quarkus.oidc.authentication.redirect-path=/service/callback,Quarkus 将 redirect_uri`参数设置为绝对 URL(例如 `http://localhost:8080/service/callback),无论当前请求 URL 如何,该绝对 URL 都相同。

如果设置了 quarkus.oidc.authentication.redirect-path,但您需要在将用户重定向回唯一的回调 URL(例如 http://localhost:8080/service/callback)后恢复原始请求 URL,请将 quarkus.oidc.authentication.restore-path-after-redirect`属性设置为 `true。此操作将还原请求 URL,如 http://localhost:8080/service/1

Customizing authentication requests

默认情况下,仅将 response_type(设置为 code)、scope(设置为 openid)、client_id、`redirect_uri`和 `state`属性作为 HTTP 查询参数传递给 OIDC 提供商的授权终结点,即当用户被重定向到该终结点进行认证时。

您可以通过 `quarkus.oidc.authentication.extra-params`为其添加更多属性。例如,一些 OIDC 提供商可能选择将授权码作为重定向 URI 片段的一部分返回,这将中断认证流程。以下示例演示如何解决此问题:

quarkus.oidc.authentication.extra-params.response_mode=query

请参阅OIDC redirect filters一节,该节解释了如何使用自定义`OidcRedirectFilter`来自定义OIDC重定向,包括对OIDC授权端点的重定向。

Customizing the authentication error response

当用户被重定向到OIDC授权端点以进行身份验证,并在必要时授权Quarkus应用程序时,此重定向请求可能失败,例如,当重定向URI中包含无效范围时。在这种情况下,提供方会使用`error`和`error_description`参数而不是预期的`code`参数将用户重定向回Quarkus。

例如,当无效范围或其他无效参数包含在重定向到提供方时,可能会发生这种情况。

在这种情况下,HTTP`401`错误默认返回。但是,您可以请求调用自定义公共错误端点以返回更友好的HTML错误页面。要执行此操作,请设置`quarkus.oidc.authentication.error-path`属性,如下例所示:

quarkus.oidc.authentication.error-path=/error

确保该属性以正斜杠(/)字符开头,并且路径相对于当前端点的基本URI。例如,如果将其设置为'/error',并且当前请求URI是`https://localhost:8080/callback?error=invalid_scope`,则会执行最终重定向到`https://localhost:8080/error?error=invalid_scope`。

为了防止用户被重定向到此页面进行重新身份验证,请确保此错误端点是一个公共资源。

OIDC redirect filters

您可以注册一个或多个`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,"); 1
            context.routingContext().response().putHeader("Redirect-Filtered", "true"); 2
        }
    }

}
1 添加其他查询参数。请注意,此查询名称和值由Quarkus OIDC用URL编码,在这种情况下,一个`redirect-filtered=true%20C`查询参数被添加到重定向URI中。
2 添加自定义HTTP响应头。

请参阅Customizing authentication requests一节,了解如何为OIDC授权点配置其他查询参数。

本地错误和会话过期页面的自定义`OidcRedirectFilter`还可以创建安全的Cookie来帮助生成此类页面。

例如,假设您需要将会话已过期的当前用户重定向到一个位于`http://localhost:8080/session-expired-page`的自定义会话过期页面。以下自定义`OidcRedirectFilter`使用OIDC租户客户端密钥在自定义`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.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
     }
    }
}
1 确保此重定向过滤器只在重定向到会话过期页面期间被调用。
2 访问与现在过期的会话关联的`AuthorizationCodeTokens`令牌作为`RoutingContext`属性。
3 解码ID令牌声明并获取用户名。
4 将用户名保存在使用当前OIDC租户的客户端密钥加密的JWT令牌中。
5 创建一个自定义`session_expired`cookie,有效期为5秒,它使用“|”分隔符连接加密令牌和租户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.TenantConfigBean;
import io.smallrye.jwt.auth.principal.DefaultJWTParser;
import io.vertx.ext.web.RoutingContext;

@Path("/session-expired-page")
public class SessionExpiredResource {

    @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
    }
}
1 注入 TenantConfigBean 可以用于访问所有当前 OIDC 租户配置。
2 将自定义 cookie 值切分成两部分,第一部分是加密令牌,最后部分是租户 ID。
3 获取 OIDC 租户配置。
4 使用 OIDC 租户的客户端密钥对 cookie 值进行解密。
5 Remove the custom cookie.
6 使用已解密令牌中的用户名和租户 ID 生成服务过期页面响应。

Accessing authorization data

你可以通过不同的方式访问授权信息。

Accessing ID and access tokens

OIDC 代码验证机制在授权代码流期间获取三个令牌: ID token、访问令牌和刷新令牌。

ID 令牌永远都是 JWT 令牌,并通过 JWT 声明表示用户验证。你可以使用它来获取颁发 OIDC 端点、用户名和其他称为 claims 的信息。你可以通过注入 JsonWebTokenIdToken 限定符来访问 ID 令牌声明:

import jakarta.inject.Inject;
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();
    }
}

OIDC web-app 应用程序通常使用访问令牌代表当前登录用户访问其他端点。你可以如下访问原始访问令牌:

import jakarta.inject.Inject;
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);
    }
}

当授权代码流访问令牌被注入为 JsonWebToken 时,除了强制性 ID 令牌验证之外,它的验证还会自动启用。如果真的有需要,你可以用 quarkus.oidc.authentication.verify-access-token=false 禁用此代码流访问令牌验证。

当颁发给 Quarkus web-app 应用程序的访问令牌是不透明的(二进制的)并且无法解析为 JsonWebToken 时,或者应用程序需要内部内容时,则使用 AccessTokenCredential

JsonWebTokenAccessTokenCredential 的注入 在 @RequestScoped@ApplicationScoped 上下文中都受支持。

Quarkus OIDC 使用刷新令牌作为其 session management 进程的一部分来刷新当前 ID 和访问令牌。

User info

如果 ID 令牌未提供有关当前经过验证的用户的足够信息,你可以从 UserInfo 端点获取更多信息。设置 quarkus.oidc.authentication.user-info-required=true 属性以从 OIDC UserInfo 端点请求 UserInfo JSON 对象。

将使用授权代码授予响应返回的访问令牌向 OIDC 提供程序 UserInfo 端点发送请求,并将创建 io.quarkus.oidc.UserInfo 对象(一个简单的 jakarta.json.JsonObject 封装)。可以将 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 端点时。

Accessing the OIDC configuration information

当前租户的已发现 OpenID Connect configuration metadataio.quarkus.oidc.OidcConfigurationMetadata 表示,并且可以注入或作为 SecurityIdentity configuration-metadata 属性来访问。

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

Mapping token claims and SecurityIdentity roles

将角色从已验证令牌映射到 SecurityIdentity 角色的方法与 Bearer tokens 中所述方法相同。唯一的区别是默认情况下使用 ID token 作为角色的来源。

如果你使用 Keycloak,请为 ID 令牌设置 microprofile-jwt 客户端范围,以包含 groups 声明。如需了解更多信息,请参阅 Keycloak server administration guide

但是,根据你的 OIDC 提供程序,角色可能存储在访问令牌或用户信息中。

如果访问令牌包含角色,并且此访问令牌并不意味着要传播到下游端点,则设置`quarkus.oidc.roles.source=accesstoken`。

如果UserInfo是角色的来源,则设置 quarkus.oidc.roles.source=userinfo,如果需要,quarkus.oidc.roles.role-claim-path

此外,您还可以使用自定义`SecurityIdentityAugmentor`添加角色。有关更多信息,请参阅 SecurityIdentity customization。 您还可以使用 HTTP Security policy将令牌声明创建的 SecurityIdentity 角色映射到特定于部署的角色。

Ensuring validity of tokens and authentication data

身份验证过程的核心部分是确保可信链和信息的有效性。这是通过确保可以信任令牌来完成的。

Token verification and introspection

OIDC 授权码流令牌的验证过程遵循 Bearer 令牌身份验证令牌验证和内省逻辑。有关更多信息,请参阅“Quarkus OpenID Connect(OIDC)Bearer 令牌身份验证”指南的 Token verification and introspection 部分。

对于 Quarkus web-app 应用程序,默认情况下只验证 IdToken,因为访问令牌不用于访问当前的 Quarkus web-app 端点,并打算传播到预期此访问令牌的服务。如果您希望访问令牌包含访问当前 Quarkus 端点所需的的角色 (quarkus.oidc.roles.source=accesstoken),则也会对其进行验证。

Token introspection and UserInfo cache

除非预计授权码访问令牌是角色的来源,否则不会对其进行内省。但是,它们将被用于获取 UserInfo。 如果需要令牌内省、`UserInfo`或两者,则将存在一个或两个带有授权码访问令牌的远程调用。

有关使用默认令牌缓存或注册自定义缓存实现的更多信息,请参阅 Token introspection and UserInfo cache

JSON web token claim verification

有关声明验证的信息,包括 iss (颁发者)声明,请参阅 JSON Web Token claim verification 部分。如果 web-app 应用程序请求访问令牌验证,则它适用于ID令牌,也适用于JWT格式的访问令牌。

Jose4j Validator

您可以注册自定义 [Jose4j 验证器] 来定制 JWT 声明验证过程。有关更多信息,请参阅 Jose4j 部分。

Proof Key for Code Exchange (PKCE)

Proof Key for Code Exchange (PKCE)最大程度地降低了授权代码拦截风险。

虽然 PKCE 对公共 OIDC 客户端(比如在浏览器中运行的 SPA 脚本)至关重要,但它也可以为 Quarkus OIDC web-app 应用程序提供额外的保护。 使用 PKCE,Quarkus OIDC web-app 应用程序充当机密 OIDC 客户端,可以安全地存储客户端机密并用它来交换令牌的代码。

您可以使用 quarkus.oidc.authentication.pkce-required 属性和一个 32 字符的机密为 OIDC web-app 端点启用 PKCE,该机密用于在状态 Cookie 中加密 PKCE 代码验证器,如下例所示:

quarkus.oidc.authentication.pkce-required=true
quarkus.oidc.authentication.state-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU

如果您已经有一个 32 字符的客户端机密,则不需要设置 quarkus.oidc.authentication.pkce-secret 属性,除非您更喜欢使用不同的密钥。如果未配置此机密,并且在客户端机密少于 16 个字符的情况下无法回退到客户端机密,则会自动生成此机密。

密钥用于在用户使用 code_challenge 查询参数重定向到 OIDC 提供程序进行身份验证时对随机生成的 PKCE code_verifier 进行加密。当用户重定向回 Quarkus 并发送到令牌端点,以及 code、客户端机密和其他参数以完成代码交换时,将对 code_verifier 进行解密。如果 code_verifierSHA256`摘要与身份验证请求期间提供的 `code_challenge 不匹配,提供程序将失败代码交换。

Handling and controlling the lifetime of authentication

身份验证的另一个重要要求是,确保会话所基于的数据是最新的,而无需要求用户对每个请求进行身份验证。 还有一些情况下明确要求注销事件。使用以下要点为保护您的 Quarkus 应用程序找到正确的平衡:

Cookies

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 属性。设置 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 属性,以便受保护的 Quarkus 服务均可见会话 cookie。例如,如果您在以下两个域名上部署了 Quarkus 服务,则必须将 quarkus.oidc.authentication.cookie-domain 属性设置为 company.net:

State cookies

状态 cookie 用于支持授权码流程完成。当启动授权码流程时,Quarkus 创建一个状态 cookie 和一个匹配的 state 查询参数,然后再将用户重定向到 OIDC 提供商。当用户重定向回 Quarkus 以完成授权码流程时,Quarkus 期望请求 URI 必须包含 state 查询参数,并且它必须与当前状态 cookie 值匹配。

状态 cookie 的默认生存期为 5 分钟,您可以通过 quarkus.oidc.authenticaion.state-cookie-age Duration 属性来更改它。

每次启动新的授权码流程时,Quarkus 都会创建一个唯一的状态 cookie 名称以支持多标签页身份验证。代表同一用户的许多并发身份验证请求可能导致创建大量状态 cookie。如果您不希望您的用户使用多个浏览器标签页进行身份验证,建议使用 quarkus.oidc.authenticaion.allow-multiple-code-flows=false 将其禁用。它还确保为每个新的用户身份验证创建相同的状态 cookie 名称。

Session cookie and default TokenStateManager

OIDC CodeAuthenticationMechanism 使用默认 io.quarkus.oidc.TokenStateManager 接口实施来保留授权码或刷新授予响应中返回的 ID、访问和刷新令牌,保存在一个加密会话 cookie 中。

它使 Quarkus OIDC 端点完全无状态,建议遵循此策略以实现最佳的可伸缩性成果。

参阅本指南的 Database TokenStateManager 部分,了解有关在数据库或其他服务器端存储解决方案中存储令牌的信息。如果您愿意并且有令人信服的理由将令牌状态存储在服务器上,那么此方法是合适的。

请参阅 Session cookie and custom TokenStateManager 部分,了解令牌存储的替代方法。这非常适合那些寻求令牌状态管理的定制解决方案,尤其是当标准服务器端存储无法满足您的特定要求时。

您可以配置默认 TokenStateManager 以避免在会话 cookie 中保存访问令牌,并且仅保留 ID 和刷新令牌或仅保留一个 ID 令牌。

仅当端点需要执行以下操作时才需要访问令牌:

  • Retrieve UserInfo

  • 使用此访问令牌访问下游服务

  • 使用与访问令牌关联的角色,默认情况下会检查这些角色

在这种情况下,使用 quarkus.oidc.token-state-manager.strategy 属性将令牌状态策略配置如下:

To…​ 将属性设置为…​

仅保留 ID 和刷新令牌

quarkus.oidc.token-state-manager.strategy=id-refresh-tokens

仅保留 ID 令牌

quarkus.oidc.token-state-manager.strategy=id-token

如果所选会话 cookie 策略组合令牌并生成大于 4KB 的大会话 cookie 值,则一些浏览器可能无法处理此类 cookie 大小。当 ID、访问和刷新令牌是 JWT 令牌,并且所选策略为 keep-all-tokens 时,或者当策略为 id-refresh-token 时具有 ID 和刷新令牌时,可能会发生这种情况。要解决此问题,您可以设置 quarkus.oidc.token-state-manager.split-tokens=true 以为每个令牌创建一个唯一会话令牌。另一种解决方法是将令牌保存在数据库中。有关更多信息,请参阅 Database TokenStateManager.

默认的 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

标记加密密钥至少必须有 32 个字符的长度。如果未配置此键,则将对 quarkus.oidc.credentials.secretquarkus.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 中的标记加密。

Session cookie and custom TokenStateManager

如果您想自定义标记与会话 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, TokenStateManager.CreateTokenStateRequestContext requestContext) {
        return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent, requestContext)
                .map(t -> (t + "|custom"));
    }

    @Override
    public Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig,
            String tokenState, TokenStateManager.GetTokensRequestContext 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,
            TokenStateManager.DeleteTokensRequestContext requestContext) {
        if (!tokenState.endsWith("|custom")) {
            throw new IllegalStateException();
        }
        String defaultState = tokenState.substring(0, tokenState.length() - 7);
        return tokenStateManager.deleteTokens(routingContext, oidcConfig, defaultState, requestContext);
    }
}

有关默认 TokenStateManager 将标记存储在已加密会话 Cookie 中的信息,请参阅 Session cookie and default TokenStateManager

有关将标记存储在数据库中的自定义 Quarkus TokenStateManager 实施的信息,请参阅 Database TokenStateManager

Database TokenStateManager

如果您更愿意遵循有状态标记存储策略,则可以使用 Quarkus 提供的自定义 TokenStateManager 来使您的应用程序将标记存储在数据库中,而不是将它们存储在已加密会话 Cookie 中,这在 Session cookie and default TokenStateManager 部分中以默认方式进行。

要使用此功能,请将以下扩展名添加到您的项目:

CLI
quarkus extension add {add-extension-extensions}
Maven
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
Gradle
./gradlew addExtension --extensions='{add-extension-extensions}'

此扩展名将使用基于数据库的扩展名替换默认的 io.quarkus.oidc.TokenStateManager

OIDC 数据库标记状态管理器在底层使用响应式 SQL 客户端,以避免阻塞,因为身份验证可能发生在 IO 线程上。

根据您的数据库,精确包含并配置一个 Reactive SQL client。支持以下响应式 SQL 客户端:

  • Reactive Microsoft SQL client

  • Reactive MySQL client

  • Reactive PostgreSQL client

  • Reactive Oracle client

  • Reactive DB2 client

如果您的应用程序已经将 Hibernate ORM 与其中一个 JDBC 驱动程序扩展名结合使用,则无需切换到使用响应式 SQL 客户端。

例如,您已经有一个将 Hibernate ORM 扩展名与 PostgreSQL JDBC Driver 结合使用的应用程序,并且您的数据源已配置如下:

quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus_test
quarkus.datasource.password=quarkus_test
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/quarkus_test

现在,如果您决定使用 OIDC 数据库标记状态管理器,则必须添加以下依赖项并设置响应式驱动程序 URL:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-db-token-state-manager</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-oidc-db-token-state-manager")
implementation("io.quarkus:quarkus-reactive-pg-client")
quarkus.datasource.reactive.url=postgresql://localhost:5432/quarkus_test

现在,标记可以存储在数据库中。

默认情况下,会为您创建一个用于存储令牌的数据库表,但是,您可以使用 quarkus.oidc.db-token-state-manager.create-database-table-if-not-exists 配置属性禁用此选项。如果您希望 Hibernate ORM 扩展创建此表,那么您只需包含一个实体,例如以下内容:

package org.acme.manager;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Table(name = "oidc_db_token_state_manager") 1
@Entity
public class OidcDbTokenStateManagerEntity {

    @Id
    String id;

    @Column(name = "id_token", length = 4000) 2
    String idToken;

    @Column(name = "refresh_token", length = 4000)
    String refreshToken;

    @Column(name = "access_token", length = 4000)
    String accessToken;

    @Column(name = "expires_in")
    Long expiresIn;
}
1 只有在生成数据库模式时,Hibernate ORM 扩展才会为您创建此表。有关详细信息,请参阅 Hibernate ORM 指南。
2 您可以根据令牌的长度选择列长度。

Logout and expiration

认证信息失效的主要有两种方式:令牌过期且未续订或触发了明确的注销操作。

我们从明确的注销操作开始。

User-initiated logout

用户可以通过发送请求到使用 quarkus.oidc.logout.path 属性设置的 Quarkus 端点注销路径来请求注销。例如,如果端点地址为 https://application.com/webapp,且 quarkus.oidc.logout.path 设置为 /logout,则必须将注销请求发送到 https://application.com/webapp/logout

此注销请求会启动 RP-initiated logout。用户会重定向到 OIDC 提供者以注销,在这里他们可能会被要求确认注销确实是有意的。

注销完成后,用户会返回到端点注销后页面,前提是已设置 quarkus.oidc.logout.post-logout-path 属性。例如,如果端点地址为 https://application.com/webapp,且 quarkus.oidc.logout.post-logout-path 设置为 /signin,那么用户会返回到 https://application.com/webapp/signin。请注意,此 URI 必须在 OIDC 提供者中注册为有效的 post_logout_redirect_uri

如果设置了 quarkus.oidc.logout.post-logout-path,那么会创建一个 q_post_logout Cookie,并向注销重定向 URI 中添加一个匹配的 state 查询参数,且注销完成后,OIDC 提供者会返回此 state。建议 Quarkus web-app 应用程序检查 state 查询参数是否与 q_post_logout Cookie 的值匹配,例如,可以在 Jakarta REST 过滤器中执行此操作。

请注意,使用 OpenID Connect Multi-Tenancy 时 Cookie 名称会发生变化。例如,对于 ID 为 tenant_1 的租户,它将被命名为 q_post_logout_tenant_1,依此类推。

以下是如何将 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.authentication.cookie-path 设置为您在所有应用程序资源中通用的路径值,例如本例中的 /。有关更多信息,请参阅 Cookies 部分。

一些 OIDC 提供商不支持 RP-initiated logout 规范并且不会返回 OpenID Connect 广为人知的 end_session_endpoint 元数据属性。但是,对于 Quarkus 来说,这不是问题,因为此类 OIDC 提供商的具体退出机制仅在退出 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}

Back-channel logout

OIDC 提供商可以使用身份验证数据强制退出所有应用程序。这称为后通道退出。在这种情况下,OIDC 会从每个应用程序调用一个特定的 URL 来触发退出。

OIDC 供应商使用 Back-channel logout 注销当前用户目前登录的所有应用程序,无需用户代理。

您可以将 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.backchannel.path=/back-channel-logout

绝对`back-channel logout`URL是通过以下方式计算得出的:在当前端点URL后面加上`quarkus.oidc.back-channel-logout.path`,例如`http://localhost:8080/back-channel-logout`。您需要在OIDC提供商的管理控制台中配置此URL。

OIDC提供商在当前登出令牌中未设置到期声明时,您还必须配置令牌年龄属性,以使登出令牌验证成功。例如,将`quarkus.oidc.token.age=10S`设置为确保从登出令牌的`iat`(签发时间)开始,不超过10秒。

Front-channel logout

您可以使用 Front-channel logout直接从用户代理(例如其浏览器)注销当前用户。它类似于Back-channel logout,但登出步骤是由用户代理(例如浏览器)执行的,而不是由OIDC提供商在后台执行的。此选项很少使用。

您可以按照以下步骤配置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.frontchannel.path=/front-channel-logout

此路径将与此请求的路径进行比较,如果路径匹配,用户将登出。

Local logout

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".
    }

Using OidcSession for local logout

`io.quarkus.oidc.OidcSession`是当前`IdToken`的包装器,它可以帮助执行Local logout、检索当前会话的租户标识符以及检查会话何时过期。随着时间的推移,将向其添加更多有用的方法。

Session management

默认情况下,登出会根据OIDC提供商颁发的ID令牌的到期时间进行。当ID令牌过期时,Quarkus端点的当前用户会话将失效,并且用户将被重新定向到OIDC提供商再次进行身份验证。如果OIDC提供商中的会话仍然有效,用户将自动重新身份验证,而无需再次提供其凭据。

可以通过启用`quarkus.oidc.token.refresh-expired`属性来自动扩展当前用户会话。如果设置为`true`,当当前ID令牌过期时,刷令牌授权将用于更新ID令牌以及访问令牌和刷令牌。

如果您有single page application for service applications且您的OIDC提供商脚本(例如`keycloak.js`)在管理授权代码流程,则该脚本还将控制SPA身份验证会话的持续时间。

如果您使用的是Quarkus OIDC`web-app`应用程序,则Quarkus OIDC代码验证机制将管理用户会话的持续时间。

要使用刷令牌,您应仔细配置会话Cookie年龄。会话年龄应大于ID令牌的持续时间,并且应接近或等于刷令牌的持续时间。

您可以通过添加当前ID令牌的持续时间值以及`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授权端点以重新进行身份验证。如果用户与该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`为_reasonably_。

您可以更进一步的主动作ID令牌或即将过期的访问令牌。将`quarkus.oidc.token.refresh-token-time-skew`设置为您希望预期的刷新值。如果在当前用户请求期间,计算出当前ID令牌将在此`quarkus.oidc.token.refresh-token-time-skew`范围内过期,则刷新ID令牌并创建新的会话Cookie。此属性应设置为小于ID令牌持续时间的值;它越接近此持续时间值,ID令牌的刷新频率就越高。

您可以通过定期 ping 您 Quarkus 端点的简单 JavaScript 函数来模拟用户活动,从而进一步优化此过程,这会最大程度地减少用户可能必须重新进行身份验证的时间范围。

如果无法刷新会话,当前经过身份验证的用户将被重定向到 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 redirect filters 部分,该部分解释了如何使用自定义 OidcRedirectFilter 来定制 OIDC 重定向,包括重定向到会话过期页面。

无法无限期地延长用户会话。返回且持有已过期 ID 令牌的用户将必须在刷新令牌过期后从 OIDC 提供程序端点重新进行认证。

Integration with GitHub and non-OIDC OAuth2 providers

一些著名的提供程序,例如 GitHub 或 LinkedIn,不是 OpenID Connect 提供程序,而是 OAuth2 提供程序,它们支持 authorization code flow。例如, GitHub OAuth2LinkedIn OAuth2。请记住,OIDC 构建于 OAuth2 之上。

OIDC 与 OAuth2 提供程序之间的主要区别在于,OIDC 提供程序除了 accessrefresh 提供程序返回的标准授权代码流程 OAuth2 和令牌外,还会返回代表用户身份验证的 ID Token

GitHub 等 OAuth2 提供程序不会返回 IdToken,而用户身份验证是暗示的,并由 access 令牌间接表示。此 access 令牌代表经身份验证的用户,授权当前 Quarkus web-app 应用程序代表经过身份验证的用户访问某些数据。

对于 OIDC,您可以验证 ID 令牌以证明身份验证有效性,而在 OAuth2 情况下,您验证访问令牌。这是通过随后调用需要访问令牌的端点来完成的,并且通常返回用户信息。此方法类似于 OIDC UserInfo 方法,其中 UserInfo 由 Quarkus OIDC 代表您获取。

例如,使用 GitHub 时,Quarkus 端点可以获取 access 令牌,这允许 Quarkus 端点为当前用户请求 GitHub 概要文件。

为了支持与此类 OAuth2 服务器的集成,需要对 quarkus-oidc 进行 sedikit 不同的配置,以允许授权码流响应而无需 IdTokenquarkus.oidc.authentication.id-token-required=false

即使您配置了该扩展名以支持授权代码流而无需 IdToken,也会生成一个内部 IdToken 来标准化 quarkus-oidc 的操作方式。您使用内部 IdToken 来支持身份验证会话,并避免在每次请求时将用户重定向到提供商(例如 GitHub)。在这种情况下,将 IdToken 的 age 设置为授权码流响应中标准 expires_in 属性的值。您可以使用 quarkus.oidc.authentication.internal-id-token-lifespan 属性来自定义 ID 令牌的 age。默认的 ID 令牌 age 为 5 分钟。 您可以根据 session management 部分中所述,进一步对其进行扩展。 这简化了您处理支持多个 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`将嵌入到内部生成的 `IdToken 中,并保存在加密的会话 Cookie 中。它可以用 quarkus.oidc.cache-user-info-in-idtoken=false 禁用。 或者,你可能希望考虑使用默认或自定义 UserInfo 缓存提供程序来缓存 UserInfo。有关详细信息,请参阅“OpenID Connect (OIDC) 持有者令牌认证”指南的 Token Introspection and UserInfo cache 部分。 大多数著名的社交 OAuth2 提供程序都会强制执行速率限制,因此,你很可能会更希望缓存 UserInfo。

OAuth2 服务器可能不支持众所周知的配置端点。在这种情况下,你必须禁用发现并手动配置授权、令牌、内省和 `UserInfo`端点路径。

对于众所周知的 OIDC 或 OAuth2 提供程序(如 Apple、Facebook、GitHub、Google、Microsoft、Spotify 和 X(以前称为 Twitter)),Quarkus 可以通过 `quarkus.oidc.provider`属性显著简化应用程序的配置。以下是如何在 created a GitHub OAuth application之后将 `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

有关配置其他众所周知提供程序的详细信息,请参阅 OpenID Connect providers

对于像这样的端点,只需要所有这些内容即可返回当前经过身份验证用户的个人资料(带有 GET [role="bare"]http://localhost:8080/github/userinfo),并将其作为各个 `UserInfo`属性进行访问:

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();
    }
}

如果你在 OpenID Connect Multi-Tenancy的帮助下支持多个社交提供程序(例如,Google(一个返回 `IdToken`的 OIDC 提供程序)和 GitHub(一个不返回 `IdToken`并仅允许访问 `UserInfo`的 OAuth2 提供程序),那么你可以让端点使用已注入的 `SecurityIdentity`配合 Google 和 GitHub 流程工作。将在内部生成的 `IdToken`创建主体时,需要对 `SecurityIdentity`进行简单的增强,将主体替换为基于 `UserInfo`的主体(当 GitHub 流程处于活动状态时):

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);
    }

}

现在,当用户使用 Google 或 GitHub 登录你的应用程序时,以下代码将起作用:

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 getUserName() {
        return identity.getPrincipal().getName();
    }

    @GET
    @Path("/github")
    @Produces("application/json")
    public String getUserName() {
        return identity.getPrincipal().getUserName();
    }
}

一种更简单的替代方法可能是注入 @IdToken JsonWebToken`和 `UserInfo,并在处理返回 IdToken`的提供程序时使用 `JsonWebToken,在处理不返回 IdToken`的提供程序时使用 `UserInfo

你必须确保在 GitHub OAuth 应用程序配置中输入的回调路径与希望在成功完成 GitHub 身份验证和应用程序授权之后将用户重定向到的端点路径相匹配。在这种情况下,必须将其设置为 http:localhost:8080/github/userinfo

Listening to important authentication events

你可以注册 `@ApplicationScoped`bean,它将观察重要的 OIDC 身份验证事件。当用户首次登录、重新进行身份验证或刷新会话时,都会更新侦听器。将来可能还会报告更多事件。例如:

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;

import io.quarkus.oidc.IdTokenCredential;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.security.identity.AuthenticationRequestContext;
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));
    }
}

你可以按照安全提示和技巧指南的 Observe security events部分所述监听其他安全事件。

Propagating tokens to downstream services

有关将授权代码流访问令牌传播到下游服务的详细信息,请参阅 Token Propagation部分。

Integration considerations

受 OIDC 保护的应用程序集成到一个环境中,从单页面应用程序中可以调用它。它必须与众所周知的 OIDC 提供程序配合使用,在 HTTP 反向代理后面运行,需要外部和内部访问,等等。

本部分讨论这些注意事项。

Single-page applications

你可以检查按照“OpenID Connect (OIDC) 持有者令牌认证”指南的 Single-page applications部分中建议的方式实现单页面应用程序 (SPA),是否能满足你的要求。

如果你更喜欢在 Quarkus Web 应用程序中使用 SPA 和 JavaScript API(如 Fetch`或 `XMLHttpRequest(XHR),请注意 OIDC 提供程序可能不支持对授权端点的跨源资源共享 (CORS),用户在从 Quarkus 进行重定向后在此端点进行身份验证。如果 Quarkus 应用程序和 OIDC 提供程序托管在不同的 HTTP 域、端口或两者上,这将导致身份验证失败。

在这些情况下,将 quarkus.oidc.authentication.java-script-auto-redirect 属性设置为 false,它将指示 Quarkus 返回 499 状态代码和一个带有 OIDC 值的 WWW-Authenticate 标头。

浏览器脚本必须设置标头,以将当前请求标识为针对 499 状态代码的 JavaScript 请求,以在 quarkus.oidc.authentication.java-script-auto-redirect 属性设置为 false 时返回。

如果脚本引擎自身设置特定于引擎的请求标头,则可以注册自定义 quarkus.oidc.JavaScriptRequestChecker bean,它会通知 Quarkus 当前请求是否为 JavaScript 请求。例如,如果 JavaScript 引擎设置了 `HX-Request: true`之类的标头,则可以像这样进行检查:

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.oidc.JavaScriptRequestChecker;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomJavaScriptRequestChecker implements JavaScriptRequestChecker {

    @Override
    public boolean isJavaScriptRequest(RoutingContext context) {
        return "true".equals(context.request().getHeader("HX-Request"));
    }
}

然后,在 499 状态代码时重新加载最后请求的页面。

否则,还必须更新浏览器脚本,以设置带有 JavaScript 值的 X-Requested-With 标头,并在 499 状态代码时重新加载最后请求的页面。

例如:

Future<void> callQuarkusService() async {
    Map<String, String> headers = Map.fromEntries([MapEntry("X-Requested-With", "JavaScript")]);

    await http
        .get("https://localhost:443/serviceCall")
        .then((response) {
            if (response.statusCode == 499) {
                window.location.assign("https://localhost.com:443/serviceCall");
            }
         });
  }

Cross-origin resource sharing

如果计划从运行在不同域上的单页面应用程序中使用此应用程序,则需要配置跨域资源共享 (CORS)。有关更多信息,请参阅“跨域资源共享”指南的 CORS filter 部分。

Calling Cloud provider services

Google Cloud

你可以使用 Quarkus OIDC web-app 应用程序访问 Google Cloud services,例如 BigQuery,代表当前已验证的用户,这些用户已在其 Google 开发者控制台中为这些服务启用了 OIDC 授权码流程权限。

你可以使用 Quarkiverse Google Cloud Services 来执行此操作。你只需要添加 latest tag 服务依赖项,如以下示例所示:

pom.xml
<dependency>
    <groupId>io.quarkiverse.googlecloudservices</groupId>
    <artifactId>quarkus-google-cloud-bigquery</artifactId>
    <version>${quarkiverse.googlecloudservices.version}</version>
</dependency>
build.gradle
implementation("io.quarkiverse.googlecloudservices:quarkus-google-cloud-bigquery:${quarkiverse.googlecloudservices.version}")

然后,配置 Google OIDC 属性:

quarkus.oidc.provider=google
quarkus.oidc.client-id={GOOGLE_CLIENT_ID}
quarkus.oidc.credentials.secret={GOOGLE_CLIENT_SECRET}
quarkus.oidc.token.issuer=https://accounts.google.com

Running Quarkus application behind a reverse proxy

当 Quarkus 应用程序运行在反向代理、网关或防火墙后,OIDC 身份验证机制可能会受到影响,这时 HTTP Host 标头可能会重置为内部 IP 地址,HTTPS 连接可能会被终止,等等。例如,授权码流程 redirect_uri 参数可能会设置为内部主机,而不是预期的外部主机。

在这种情况下,将 Quarkus 配置为识别由代理转发来的原始标头是必需的。有关更多信息,请参阅 Running behind a reverse proxy Vert.x 文档部分。

例如,如果你的 Quarkus 端点在 Kubernetes Ingress 后面的群集中运行,则从 OIDC 提供商重定向到此端点的操作可能无法正常执行,因为计算后的 redirect_uri 参数可能指向内部端点地址。可以使用以下配置解决此问题,其中 X-ORIGINAL-HOST 由 Kubernetes Ingress 设置以表示外部端点地址:

quarkus.http.proxy.proxy-address-forwarding=true
quarkus.http.proxy.allow-forwarded=false
quarkus.http.proxy.enable-forwarded-host=true
quarkus.http.proxy.forwarded-host-header=X-ORIGINAL-HOST

当 Quarkus 应用程序运行在终止 SSL 的反向代理之后,还可以使用 quarkus.oidc.authentication.force-redirect-https-scheme 属性。

External and internal access to the OIDC provider

与相对于 quarkus.oidc.auth-server-url 内部 URL 自动发现或配置的 URL 相比,OIDC 提供商可从外部访问的授权、注销和其他端点可以具有不同的 HTTP(S) URL。在这些情况下,端点可能会报告发行人验证失败,并且重定向到可从外部访问的 OIDC 提供商端点可能会失败。

如果你使用的是 Keycloak,请使用一个 KEYCLOAK_FRONTEND_URL 系统属性(已设置为可从外部访问的基本 URL)来启动它。如果你使用的是其他 OIDC 提供商,请查阅你的提供商的文档。

OIDC SAML identity broker

如果你的身份提供商不实现 OpenID Connect,而只实现旧版基于 XML 的 SAML2.0 SSO 协议,则 Quarkus 无法用作 SAML 2.0 适配器,这类似于 quarkus-oidc 用作 OIDC 适配器的方式。

但是,很多 OIDC 提供商(如 Keycloak、Okta、Auth0 和 Microsoft ADFS)提供 OIDC 到 SAML 2.0 桥接器。可以在你的 OIDC 提供商中创建到 SAML 2.0 提供商的身份代理连接,并使用 quarkus-oidc 对你的用户向此 SAML 2.0 提供商进行身份验证,让 OIDC 提供商协调 OIDC 和 SAML 2.0 通信。就 Quarkus 端点而言,它们可以继续使用相同的 Quarkus Security、OIDC API、注解(如 @Authenticated、`SecurityIdentity`等)。

例如,假设 Okta 是你的 SAML 2.0 提供商,而 Keycloak 是你的 OIDC 提供商。以下是一个典型的流程,用于说明如何配置 Keycloak 以代理到 Okta SAML 2.0 提供商。

首先,在你的 Okta Dashboard/Applications 中创建一个新的 SAML2 集成:

okta create saml integration

例如,将其命名为 OktaSaml

okta saml general settings

接下来,将其配置为指向 Keycloak SAML 适配器端点。此时,您需要知道 Keycloak 领域名称,例如 quarkus,并假设 Keycloak SAML 适配器别名为 saml,则输入端点地址为 http:localhost:8081/realms/quarkus/broker/saml/endpoint。输入服务提供商 (SP) 实体 ID 为 http:localhost:8081/realms/quarkus,其中 http://localhost:8081 是 Keycloak 基本地址,saml 是适配器别名:

okta saml configuration

接下来,保存此 SAML 集成并记下其元数据 URL:

okta saml metadata

接下来,向 Keycloak 添加一个 SAML 提供商:

首先,像往常一样,创建一个新的领域或将现有领域导入到 Keycloak。在这种情况下,领域名称必须为 quarkus

现在,在 quarkus 领域属性中,导航到 Identity Providers 并添加一个新的 SAML 提供商:

keycloak add saml provider

请注意,别名设置为 samlRedirect URIhttp:localhost:8081/realms/quarkus/broker/saml/endpointService provider entity IDhttp:localhost:8081/realms/quarkus - 这些值与您在前一步创建 Okta SAML 集成时输入的值相同。

最后,将 Service entity descriptor 设置为指向您在上一步末尾记下的 Okta SAML 集成元数据 URL。

接下来,如果您愿意,您可以通过导航至 Authentication/browser/Identity Provider Redirector config 并将 AliasDefault Identity Provider 属性都设置为 saml 将此 Keycloak SAML 提供商注册为默认提供商。如果您没有将其配置为默认提供商,则在身份验证时,Keycloak 提供 2 个选项:

  • 使用 SAML 提供商进行身份验证

  • 使用名称和密码直接向 Keycloak 进行身份验证

现在,将 Quarkus OIDC web-app 应用程序配置为指向 Keycloak quarkus 领域、quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus。然后,您就可以使用由 Keycloak OIDC 和 Okta SAML 2.0 提供程序提供的 OIDC 到 SAML 桥接来开始验证您的 Quarkus 用户到 Okta SAML 2.0 提供程序。

您可以配置其他 OIDC 提供商来提供 SAML 桥接,方法与对 Keycloak 所做的一样。

Testing

当对一个单独的类似 OIDC 的服务器进行身份验证时,测试常常很棘手。Quarkus 提供从模拟到局部运行 OIDC 提供程序的若干选项。

首先将以下依赖项添加到测试项目:

pom.xml
<dependency>
    <groupId>org.htmlunit</groupId>
    <artifactId>htmlunit</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>*</artifactId>
       </exclusion>
    </exclusions>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("org.htmlunit:htmlunit")
testImplementation("io.quarkus:quarkus-junit5")

Wiremock

添加以下依赖项:

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

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

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

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

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

import org.htmlunit.SilentCssErrorHandler;
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlForm;
import org.htmlunit.html.HtmlPage;

import io.quarkus.test.common.WithTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.server.OidcWiremockTestResource;

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

    @Test
    public void testCodeFlow() throws Exception {
        try (final WebClient webClient = createWebClient()) {
            // the test REST endpoint listens on '/code-flow'
            HtmlPage page = webClient.getPage("http://localhost:8081/code-flow");

            HtmlForm form = page.getFormByName("form");
            // user 'alice' has the 'user' role
            form.getInputByName("username").type("alice");
            form.getInputByName("password").type("alice");

            page = form.getInputByValue("login").click();

            assertEquals("alice", page.getBody().asText());
        }
    }

    private WebClient createWebClient() {
        WebClient webClient = new WebClient();
        webClient.setCssErrorHandler(new SilentCssErrorHandler());
        return webClient;
    }
}

OidcWiremockTestResource 识别 aliceadmin 用户。用户 alice 仅默认具有 user 角色 - 它可以通过 quarkus.test.oidc.token.user-roles 系统属性进行自定义。用户 admin 默认具有 useradmin 角色 - 它可以通过 quarkus.test.oidc.token.admin-roles 系统属性进行自定义。

此外,OidcWiremockTestResource 将令牌颁发者和受众设置为 https://service.example.com,可以通过 quarkus.test.oidc.token.issuerquarkus.test.oidc.token.audience 系统属性进行自定义。

OidcWiremockTestResource 可用于模拟所有 OIDC 提供商。

Dev Services for Keycloak

推荐使用 Dev Services for Keycloak 对照 Keycloak 进行集成测试。Dev Services for Keycloak 将启动并初始化一个测试容器:它将创建一个 quarkus 域,一个 quarkus-app 客户端(secret 密钥),并添加 aliceadminuser 角色)和 bobuser 角色)用户,其中所有这些属性都可以自定义。

首先,准备 application.properties。你可以从一个完全空的 application.properties 文件开始,因为 Dev Services for Keycloak 也会注册 quarkus.oidc.auth-server-url,指向正在运行的测试容器以及 quarkus.oidc.client-id=quarkus-appquarkus.oidc.credentials.secret=secret

但是,如果你已经配置了所有必需的 quarkus-oidc 属性,那么你只需要将 quarkus.oidc.auth-server-urlprod 配置文件关联即可,以便 Dev Services for Keycloak 启动一个容器。例如:

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

如果在运行测试之前必须将自定义域文件导入到 Keycloak,则可以将 Dev Services for Keycloak 配置如下:

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

最后,按照 Wiremock 部分中描述的方式编写测试代码。唯一的区别是 @WithTestResource 不再需要了:

@QuarkusTest
public class CodeFlowAuthorizationTest {
}

Using KeycloakTestResourceLifecycleManager

仅在有充分理由不使用 Dev Services for Keycloak 时才在测试中使用 KeycloakTestResourceLifecycleManager。如果你需要对照 Keycloak 执行集成测试,那么建议你使用 Dev Services for Keycloak 执行此操作。

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

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

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

然后,像在 Wiremock 部分中所述那样配置 Maven Surefire 插件(在以本机映像进行测试时类似地配置 Maven Failsafe 插件):

<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>

现在,设置配置并以 Wiremock 部分中所述的方式编写测试代码。唯一的区别是 WithTestResource 的名称:

import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;

@QuarkusTest
@WithTestResource(KeycloakTestResourceLifecycleManager.class)
public class CodeFlowAuthorizationTest {
}

KeycloakTestResourceLifecycleManager 注册了 aliceadmin 用户。默认情况下,用户 alice 仅具有 user 角色,可以使用 keycloak.token.user-roles 系统属性进行自定义。用户 admin 默认具有 useradmin 角色,可以使用 keycloak.token.admin-roles 系统属性进行自定义。

默认情况下,KeycloakTestResourceLifecycleManager 使用 HTTPS 来初始化一个 Keycloak 实例,可以通过指定 keycloak.use.https=false 来禁用 HTTPS。默认域名称为 quarkus,客户端 ID 为 quarkus-web-app,如果需要,请设置 keycloak.realmkeycloak.web-app.client 系统属性以自定义这些值。

TestSecurity annotation

你可以使用 @TestSecurity 和 @OidcSecurity 注解来测试 web-app 应用程序端点代码,该代码依赖于以下依赖项之一或全部四项:

  • ID JsonWebToken

  • Access JsonWebToken

  • UserInfo

  • OidcConfigurationMetadata

Checking errors in the logs

要查看令牌验证错误的详细信息,你必须启用 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

要查看 OidcProvider 客户端初始化错误的详细信息,请启用 io.quarkus.oidc.runtime.OidcRecorder TRACE 级别日志记录:

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

quarkus dev 控制台键入 j 以更改应用程序全局日志级别。