编辑此页面

使用 Auth0 OpenID Connect 提供程序保护 Quarkus Web 应用程序

Quarkus 的 quarkus-oidc 扩展提供了全面的 OpenId Connect (OIDC) 和 OAuth2 支持,支持 授权码流Bearer 令牌 两种身份验证机制。

借助 Quarkus,您可以轻松配置 OIDC 提供程序,例如 KeycloakOktaAuth0 以及其他 知名的社交 OIDC 和 OAuth2 提供程序

了解如何将 Quarkus OpenID Connect 扩展 (quarkus-oidc) 与 Auth0 OIDC 提供程序结合使用来保护您的 API 端点。

创建 Auth0 应用程序

前往 Auth0 控制面板并创建一个常规 Web 应用程序。例如,创建一个名为 QuarkusAuth0 的 Auth0 应用程序。

Create Auth0 application
结果

您的 Auth0 应用程序将创建,并附带客户端 ID、密钥和基于 HTTPS 的域名。请记下这些属性,因为您将在下一步中使用它们来完成 Quarkus 配置。

Created Auth0 application

接下来,在 Auth0 控制面板中,向您的应用程序添加一些用户。

Add Auth0 application users

现在您已成功创建并配置了 Auth0 应用程序,可以开始创建和配置 Quarkus 端点。在接下来的步骤中,您还将继续配置和更新 Auth0 应用程序。

创建 Quarkus 应用程序

使用以下 Maven 命令创建一个 Quarkus REST (以前称为 RESTEasy Reactive) 应用程序,该应用程序可以使用 Quarkus OIDC 扩展进行保护。

CLI
quarkus create app org.acme:quarkus-auth0 \
    --extension='rest,oidc' \
    --no-code
cd quarkus-auth0

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=quarkus-auth0 \
    -Dextensions='rest,oidc' \
    -DnoCode
cd quarkus-auth0

要创建 Gradle 项目,请添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

对于 Windows 用户

  • 如果使用 cmd,(不要使用反斜杠 \ 并将所有内容放在同一行上)

  • 如果使用 Powershell,请将 -D 参数用双引号括起来,例如 "-DprojectArtifactId=quarkus-auth0"

创建应用程序工作区并将其导入您喜欢的 IDE。让我们添加一个只能由已认证用户访问的 Jakarta REST 端点。

package org.acme;

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/hello")
public class GreetingResource {

    @Inject
    @IdToken                                        (1)
    JsonWebToken idToken;

    @GET
    @Authenticated                                  (2)
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello, " + idToken.getName();
    }
}
1 注入的 JsonWebToken (JWT) bean 具有 @IdToken 限定符,这意味着它代表的是 OIDC ID 令牌而不是访问令牌。IdToken 以声明的形式提供有关在 OIDC 授权码流中已认证的当前用户的信息,您可以使用 JsonWebToken API 来访问这些声明。
2 io.quarkus.security.Authenticated 注解已添加到 hello() 方法,这意味着只有已认证用户才能访问它。

在授权码流期间获取的访问令牌,以及 ID 令牌,不会被端点直接使用,而仅用于代表当前已认证用户访问下游服务。在本教程稍后部分将详细介绍“访问令牌”。

通过使用您之前创建的 Auth0 应用程序的属性,在 Quarkus application.properties 文件中配置 OIDC。

# Make sure the application domain is prefixed with 'https://'
quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}

完成此步骤后,您已成功配置 Quarkus 使用您的 Auth0 应用程序的域名、客户端 ID 和密钥。设置属性 quarkus.oidc.application-type=web-app 可指示 Quarkus 使用 OIDC 授权码流,但也有其他方法,将在教程稍后讨论。

端点地址将是 https://:8080/hello,该地址也必须在您的 Auth0 应用程序中注册为允许的回调 URL。

Auth0 allowed callback URL

完成此步骤后,当您从浏览器访问 Quarkus https://:8080/hello 端点时,身份验证完成后 Auth0 会将您重定向回同一地址。

默认情况下,Quarkus 会自动将当前请求路径用作回调路径。但您可以通过设置 Quarkus quarkus.oidc.authentication.redirect-path 属性来覆盖默认行为并配置特定的回调路径。

在生产环境中,您的应用程序很可能拥有更大的 URL 空间,并提供多个端点地址。在这种情况下,您可以设置一个专用的回调 (重定向) 路径,并将此 URL 注册到提供程序的控制面板中,如下面的配置示例所示

quarkus.oidc.authentication.redirect-path=/authenticated-welcome

在示例场景中,Quarkus 在接受来自 Auth0 的重定向、完成授权码流并创建会话 cookie 后,会调用 /authenticated-welcome。成功认证的用户还可以访问受保护应用程序空间的其他部分,而无需再次进行身份验证。例如,端点回调方法可以使用 JAX-RS API 将用户重定向到受保护应用程序的其他部分,其中会验证会话 cookie。

现在您可以开始测试端点了。

测试 Quarkus 端点

以开发模式启动 Quarkus

$ mvn quarkus:dev

这是本教程中唯一需要手动启动 Quarkus 开发模式的情况。本教程其余部分的配置和代码更新步骤将由 Quarkus 自动观察和处理,无需手动重新启动应用程序。

打开浏览器并访问 https://:8080/hello

您将被重定向到 Auth0 并提示登录

Auth0 Login

并授权 QuarkusAuth0 应用程序访问您的帐户

Auth0 Authorize

最后,您将被重定向回 Quarkus 端点,该端点将返回以下响应:Hello, auth0|60e5a305e8da5a006aef5471

请注意,当前用户名未返回。要了解此行为发生的原因,您可以使用 OIDC Dev UI,如“Quarkus Dev Services 和 OIDC (OIDC)”指南的“所有 OIDC 提供程序的 Dev UI”部分和以下部分所述。

在 OIDC Dev UI 中查看 Auth0 令牌

Quarkus 提供了出色的 Dev UI 体验。特别是,Quarkus 为使用 Keycloak 容器开发和测试 OIDC 端点提供了内置支持。如果未为 Quarkus quarkus.oidc.auth-server-url 配置属性指定 OIDC 提供程序的地址,则 Keycloak 的 DevService 会自动启动并使用。

当提供程序已配置时,您可以继续使用 Quarkus OIDC Dev UI。请按照以下说明更新您的配置

首先,将您的 Quarkus 应用程序类型从 web-app 更改为 hybrid,如下所示

quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid (1)
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
1 应用程序类型已更改为 hybrid,因为 OIDC Dev UI 目前仅支持 SPA (单页应用程序) 模式。OIDC Dev UI 单页应用程序使用其自己的 JavaScript,将用户认证到 OIDC 提供程序,并使用访问令牌作为 Bearer 令牌来访问 Quarkus 端点作为服务。

通常,Quarkus 必须配置为 quarkus.oidc.application-type=service 才能支持 Bearer 令牌身份验证,但它也支持 hybrid 应用程序类型,这意味着它可以同时支持授权码和 bearer 令牌流。

您还需要将 Auth0 应用程序配置为允许回调到 OIDC Dev UI。请使用以下 URL 格式

  • 在此示例中,${provider-name}auth0

Auth0 Allowed Callbacks

现在您可以使用 OIDC Dev UI 和 Auth0 了。

在浏览器会话中打开 https://:8080/q/dev/。将显示一个 OpenId Connect 卡片,链接到 Auth0 提供程序 SPA,如下所示

Auth0 DevUI

点击 Auth0 provider,然后点击 Login into Single Page Application

Auth0 DevUI Login to SPA

您将被重定向到 Auth0 进行登录。然后您将被重定向到 OIDC Dev UI 控制面板,如下所示

Auth0 DevUI Dashboard Without Name

在这里,您可以查看编码和解码格式的 ID 和访问令牌,将它们复制到剪贴板,或使用它们来测试服务。我们将在稍后测试端点,但现在先检查 ID 令牌

Auth0 IdToken without name

正如您所见,它没有表示用户名的声明,但如果您查看其 sub (subject) 声明,您会发现其值与您直接从浏览器访问 Quarkus 端点时获得的值匹配,即 auth0|60e5a305e8da5a006aef5471

通过配置 Quarkus 在身份验证过程中请求标准的 OIDC profile 范围来解决此问题,这应该会导致 ID 令牌包含更多信息。

quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}

quarkus.oidc.authentication.scopes=profile (1)
1 除了默认的 openid 范围外,还请求 profile 范围。

返回到 https://:8080/q/dev/,重复登录 Auth0 的过程,然后再次检查 ID 令牌,现在您应该会看到 ID 令牌包含 name 声明。

Auth0 IdToken with name

当您直接从浏览器访问 Quarkus 端点时,您应该会获得用户名。清除浏览器 cookie 缓存,访问 https://:8080/hello,仍然会返回 Hello, auth0|60e5a305e8da5a006aef5471。嗯,哪里出错了?

答案在于 org.eclipse.microprofile.jwt.JsonWebToken#getName() 实现的细节,根据 MicroProfile MP JWT RBAC 规范,它会检查 MP JWT 特定的 upn 声明,然后尝试 preferred_username,最后是 sub,这解释了为什么即使 ID 令牌包含 name 声明,您也会得到 Hello, auth0|60e5a305e8da5a006aef5471 的答案。我们可以通过更改端点 hello() 方法的实现来返回特定的声明值来轻松修复它。

package org.acme;

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/hello")
public class GreetingResource {

    @Inject
    @IdToken
    JsonWebToken idToken;

    @GET
    @Authenticated
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello, " + idToken.getClaim("name");
    }
}

现在清除浏览器缓存,访问 https://:8080/hello,最终会返回用户名。

注销支持

现在您已通过 Auth0 帮助用户登录 Quarkus,您可能希望支持用户发起的注销。Quarkus 支持 RP 发起的和其他标准的 OIDC 注销机制,以及本地会话注销

目前,Auth0 不支持标准的 OIDC RP 发起的注销,并且在其可发现的元数据中不提供结束会话端点 URL,但它提供了自己的注销机制,该机制的工作方式几乎与标准机制完全相同。

通过 Quarkus OIDC 可以轻松支持它。您必须配置一个 Auth0 结束会话端点 URL,并让 Quarkus 在 RP 发起的注销重定向请求到 Auth0 时包含 client-id 查询参数和注销后的 URL 作为 returnTo 查询参数。

quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
quarkus.oidc.authentication.scopes=openid,profile

quarkus.oidc.end-session-path=v2/logout (1)
quarkus.oidc.logout.post-logout-uri-param=returnTo (2)
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id} (3)
quarkus.oidc.logout.path=/logout (4)
quarkus.oidc.logout.post-logout-path=/hello/post-logout (5)

quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated (6)
1 Auth0 不会在元数据中包含结束会话 URL,因此请通过手动配置 Auth0 结束会话端点 URL 来补充它。
2 Auth0 不会识别标准的 post_logout_redirect_uri 查询参数,而是期望 returnTo 参数。
3 Auth0 在注销请求中期望 client-id
4 /logout 路径的已认证请求将被视为 RP 发起的注销请求。
5 这是已注销用户应该返回的公共资源。
6 确保 /logout 路径受到保护。

在这里,我们自定义了 Auth0 结束会话端点 URL,并指示 Quarkus https://:8080/logout 请求必须触发当前已认证用户的注销。关于 /logout 路径的一个有趣之处在于它是 虚拟 的,它不被任何 JAX-RS 端点中的方法支持,因此为了让 Quarkus OIDC 能够响应 /logout 请求,我们直接在配置中为此路径附加了一个 authenticated HTTP 安全策略

我们还配置 Quarkus 将已注销的用户返回到公共 /hello/post-logout 资源,并且此路径作为 Auth0 特定的 returnTo 查询参数包含在注销请求中。最后,Quarkus 应用程序的 client-id 也包含在注销 URL 中。

更新端点以接受注销后的重定向

package org.acme;

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/hello")
public class GreetingResource {

    @Inject
    @IdToken
    JsonWebToken idToken;

    @GET
    @Authenticated
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello, " + idToken.getClaim("name");
    }

    @GET
    @Path("post-logout")
    @Produces(MediaType.TEXT_PLAIN)
    public String postLogout() {
        return "You were logged out";
    }
}

请注意添加了公共 /hello/post-logout 资源方法。

在我们测试注销之前,请确保 Auth0 应用程序已配置为允许此注销重定向回 Quarkus,用户注销后。

Auth0 Allowed Logout

现在,清除浏览器 cookie 缓存,访问 https://:8080/hello,使用 Auth0 登录 Quarkus,获取返回的用户名,然后转到 https://:8080/logout。您将在浏览器中看到 You were logged out 消息。

接下来,转到 https://:8080/q/dev/,从 Dev UI SPA 登录 Auth0,并注意您现在也可以从 OIDC Dev UI 注销,在 Logged in as Sergey Beryozkin 文本旁边看到代表注销的符号。

Auth0 Dashboard with name and Logout

为了让注销从 OIDC DevUI 工作,Auth0 应用程序的允许注销回调列表必须更新为包含 OIDC DevUI 端点。

Auth0 Allowed Logouts

现在直接从 OIDC Dev UI 注销,并以新用户身份登录 - 如果需要,请向已注册的 Auth0 应用程序添加更多用户。

基于角色的访问控制

我们已确认 Quarkus 端点可以通过 Auth0 进行身份验证的用户访问。

下一步是引入基于角色的访问控制 (RBAC),以便只有特定角色的用户,例如 admin,才能访问端点。

另请参阅下面的 基于权限的访问控制 部分。

Auth0 令牌默认不包含任何包含角色的声明,因此,首先,您必须自定义 Auth0 应用程序的 Login 流,并使用自定义操作将角色添加到令牌。在 Auth0 控制面板中选择 Actions/Flows/Login,选择 Add Action/Build Custom,并将其命名为 AddRoleClaim

Auth0 Add Role Action

向其中添加以下操作脚本

exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https://quarkus-security.com';
  if (event.authorization) {
    api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
    api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
  }
};

请注意,自定义 Auth0 声明必须进行命名空间限定,因此包含角色的声明将被命名为“https://quarkus-security.com/roles”。查看我们在前面部分分析过的 ID 令牌内容,您将看到此声明是如何表示的,例如

{
  "https://quarkus-security.com/roles": [
      "admin"
  ]
}

Auth0 登录流图现在应该如下所示

Auth0 Login Flow

您必须将 admin 等角色添加到 Auth0 应用程序中已注册的用户。

创建一个 admin 角色

Auth0 Create Role

并将其添加到已注册用户

Auth0 Add Role to User

接下来,更新 Quarkus 端点,要求只有具有 admin 角色的用户才能访问端点。

package org.acme;

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.oidc.IdToken;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/hello")
public class GreetingResource {

    @Inject
    @IdToken
    JsonWebToken idToken;

    @GET
    @RolesAllowed("admin")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello, " + idToken.getClaim("name");
    }

    @GET
    @Path("post-logout")
    @Produces(MediaType.TEXT_PLAIN)
    public String postLogout() {
        return "You were logged out";
    }
}

打开 https://:8080/hello,向 Auth0 进行身份验证并获得 403。您获得 403 的原因是 Quarkus OIDC 不知道 Auth0 令牌中的哪个声明代表角色信息,默认情况下会检查 groups 声明,而 Auth0 令牌现在期望有一个“https://quarkus-security.com/roles”声明。

通过告知 Quarkus OIDC 必须检查哪个声明来强制执行 RBAC 来解决此问题

quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.authentication.scopes=profile
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}

quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles" (1)

# Logout
quarkus.oidc.end-session-path=v2/logout
quarkus.oidc.logout.post-logout-uri-param=returnTo
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
quarkus.oidc.logout.path=/logout
quarkus.oidc.logout.post-logout-path=/hello/post-logout
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated
1 指向自定义角色声明。由于声明已进行命名空间限定,因此角色声明的路径用双引号括起来。

现在,清除浏览器 cookie 缓存,再次访问 https://:8080/hello,向 Auth0 进行身份验证,并获得预期的用户名。

使用不透明的 Auth0 访问令牌访问 Quarkus

本节的主要目标是解释 Quarkus 如何调整为接受 不透明 的 bearer Auth0 访问令牌,而不是 Auth0 JWT 访问令牌,因为授权码流期间颁发的 Auth0 访问令牌默认是不透明的,除了 ID 令牌中已有的关于当前用户信息之外,它们只能用于请求 UserInfo。了解如何验证不透明令牌可能很有用,因为许多 OIDC 和 OAuth2 提供程序只颁发不透明访问令牌。

有关如何配置 Auth0 和 Quarkus 以使授权码访问令牌以 JWT 格式颁发并传播到服务端点的更多信息,请参阅本教程的以下 传播访问令牌到微服务JWT 格式的访问令牌 部分。

到目前为止,我们只测试了使用 OIDC 授权码流的 Quarkus 端点。在此流中,您使用浏览器访问 Quarkus 端点,Quarkus 本身管理授权码流,用户被重定向到 Auth0,登录,然后重定向回 Quarkus,Quarkus 通过交换代码以获取 ID、访问和刷新令牌来完成流,并使用代表成功用户身份验证的 ID 令牌。此时访问令牌无关紧要。如前所述,在授权码流中,Quarkus 仅使用访问令牌来代表当前已认证用户访问下游服务。

但让我们设想一下,我们开发的 Quarkus 端点也必须接受 Bearer 访问令牌:可能是其他 Quarkus 端点正在将其传播到此端点,或者可能是使用访问令牌访问 Quarkus 端点的 SPA。我们已经使用过的用于分析 ID 令牌的 Quarkus OIDC DevUI SPA 非常适合使用 SPA 可用的访问令牌来测试 Quarkus 端点。

让我们再次转到 https://:8080/q/dev-ui,选择 OpenId Connect 卡片,登录到 Auth0,并检查访问令牌内容。

Auth0 DevUI Access Token

此访问令牌与我们之前查看的 ID 令牌不同,Quarkus 无法直接验证它。这是因为访问令牌是 JWE (加密) 格式,而不是 JWS (签名) 格式。从解码的令牌头中可以看到,它已直接使用仅 Auth0 已知的密钥进行了加密,因此 Quarkus 无法解密其内容。从 Quarkus 的角度来看,此访问令牌是 不透明 的,Quarkus 无法使用公开的 Auth0 非对称验证密钥来验证它。

为了确认这一点,在 Test Service 区域将 /hello 输入为 Service Address,然后按 With Access Token,您将收到 HTTP 401 状态。

Auth0 Dev UI Test Access token 401

为了让 Quarkus 能够接受此类访问令牌,应提供以下两个选项之一。第一个选项是使用提供程序的内省端点远程内省不透明令牌。令牌内省通常在 OAuth2 级别支持,并且由于 OIDC 构建在 OAuth2 之上,因此一些 OIDC 提供程序 (如 Keycloak) 也支持令牌内省。但是,Auth0 不支持令牌内省,您可以通过查看公开的 Auth0 元数据来检查它,将 /.well-known/openid-configuration 添加到您已配置的 Auth0 提供程序的地址,然后打开生成的 URL https://dev-3ve0cgn7.us.auth0.com/.well-known/openid-configuration。您将看到 Auth0 没有内省端点。

Auth0 Well Known Config

因此,可以使用第二个选项,即间接访问令牌验证,其中访问令牌用于从 Auth0 获取 UserInfo,以接受和验证不透明的 Auth0 令牌。此选项有效,因为 OIDC 提供程序必须先验证访问令牌才能颁发 UserInfo,而 Auth0 具有 UserInfo 端点。

因此,让我们配置 Quarkus,使其通过使用访问令牌来获取 UserInfo 来请求验证访问令牌。

quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.authentication.scopes=profile
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}

# Point to the custom roles claim
quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles"

# Logout
quarkus.oidc.end-session-path=v2/logout
quarkus.oidc.logout.post-logout-uri-param=returnTo
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
quarkus.oidc.logout.path=/logout
quarkus.oidc.logout.post-logout-path=/hello/post-logout
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated

quarkus.oidc.token.verify-access-token-with-user-info=true (1)
1 通过使用访问令牌来获取 UserInfo 来间接验证访问令牌。

更新端点代码以期望 UserInfo 而不是 ID 令牌

package org.acme;

import io.quarkus.oidc.UserInfo;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/hello")
public class GreetingResource {

    @Inject
    UserInfo userInfo;

    @GET
    @RolesAllowed("admin")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello, " + userInfo.getName();
    }

    @GET
    @Path("post-logout")
    @Produces(MediaType.TEXT_PLAIN)
    public String postLogout() {
        return "You were logged out";
    }
}

此代码现在将同时适用于授权码和 bearer 访问令牌流。

让我们转到我们查看过访问令牌的 OIDC Dev UI,在 Test Service 区域将 /hello 输入为 Service Address,然后按 With Access Token,您将收到 200

Auth0 Dev UI Test Access token

为了确认它确实有效,请更新测试端点以仅允许 user 角色,使用 @RolesAllowed("user")。再次尝试从 OIDC Dev UI 访问端点,您将收到 HTTP 403 错误。将代码恢复为 @RolesAllowed("admin") 以再次获得令人放心的 HTTP 200 状态。

当通过使用访问令牌来获取 UserInfo 来间接验证不透明访问令牌时,Quarkus 将使用 UserInfo 作为角色信息的来源 (如果存在)。碰巧的是,Auth0 也会在 UserInfo 响应中包含之前创建的自定义角色声明。

如本节开头所述,本节的主要目标是解释 Quarkus 如何验证不透明访问令牌。通常,应避免将仅用于检索 UserInfo 的访问令牌传播到服务,除非前端 JAX-RS 端点或 SPA 倾向于将 UserInfo 检索委托给受信任的服务。

有关处理 Auth0 访问令牌的推荐方法,请参阅本教程的以下 传播访问令牌到微服务JWT 格式的访问令牌 部分。

通常,我们使用访问令牌来访问远程服务,但 OIDC DevUI SPA 控制面板还提供了使用 ID 令牌进行测试的选项。此选项仅用于模拟 SPA 将端点委托以验证和检索 ID 令牌中的某些信息供 SPA 使用的情况 - 但 OIDC DevUI 仍会将 ID 令牌作为 Bearer 令牌发送到端点。在大多数情况下,优先使用访问令牌进行测试。

您可以从 OIDC DevUI 使用 SwaggerUI 或 GraphQL 来测试服务,而不是手动输入服务路径进行测试。例如,如果您在应用程序的 pom 中添加

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>

您将在 OIDC Dev UI 中看到 Swagger 链接。

Auth0 Dev UI Test with Swagger

单击 Swagger 链接并开始测试服务。

传播访问令牌到微服务

既然我们已经成功使用了 OIDC 授权码流,并使用了 ID 令牌和 UserInfo 来访问用户信息,那么下一个典型的任务是将当前的 Auth0 访问令牌传播到下游服务,以代表当前已认证用户进行访问。

实际上,最后一个代码示例,显示注入的 UserInfo,是访问令牌传播的一个具体示例,在这种情况下,Quarkus 将 Auth0 访问令牌传播到 Auth0 UserInfo 端点以获取 UserInfo。Quarkus 在用户无需执行任何操作的情况下即可完成此操作。

但是,对于将访问令牌传播到某些自定义服务呢?在 Quarkus 中,这很容易实现,对于授权码和 bearer 令牌流都是如此。您需要做的就是创建一个 REST Client 接口,用于调用需要 Bearer 令牌访问的服务,并使用 @AccessToken 进行注解,然后到达前端端点的访问令牌,无论是 Auth0 Bearer 访问令牌还是 Quarkus 在完成 Auth0 授权码流后获得的令牌,都将被传播到目标微服务。这非常简单。

有关传播访问令牌的示例,请参阅本教程的以下各节。有关令牌传播的更多信息,请参阅 OIDC 令牌传播

JWT 格式的访问令牌

我们已经详细介绍了 Quarkus OIDC 如何处理 使用不透明的 Auth0 访问令牌访问 Quarkus,但我们不希望将 Auth0 不透明令牌传播到为当前已认证用户执行有意义操作的微服务,除了检查其 UserInfo。

前端 Quarkus 应用程序将通过传播授权码流访问令牌来访问的微服务在 Auth0 控制面板中表示为 API。让我们在 Applications/APIs 中添加它。

Auth0 API

创建的 QuarkusAuth0APIhttps://quarkus-auth0 标识符将作为此 API 的 audience。在授权码流重定向到 Auth0 时,将此 audience 作为查询参数提供,将确保 Auth0 以 JWT 格式颁发访问令牌。

API 微服务

向项目添加以下依赖项以支持 OIDC 令牌传播和 REST Client。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-rest-client-oidc-token-propagation</artifactId>
</dependency>

创建 ApiEchoService 服务类

package org.acme;

import io.quarkus.security.Authenticated;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/echo")
public class ApiEchoService {

    @POST
    @Authenticated
    @Produces(MediaType.TEXT_PLAIN)
    public String echoUserName(String username) {
        return username;
    }
}

并将其配置为 OIDC service 应用程序,该应用程序将仅从 Auth0 获取公共验证密钥。此微服务的配置应只有一行。

quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com

这对于 OIDC service 应用程序获取 Auth0 公共验证密钥并使用它们来验证 JWT 格式的 Auth0 访问令牌就足够了。

在本教程中,您已经配置了 OIDC hybrid 应用程序,它可以处理授权码和 bearer 令牌身份验证流。在生产环境中,您将把微服务作为单独的服务器运行,但为了简单起见,ApiEchoService 不需要作为第二个服务器启动,其自己的配置包含 quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com,因此将重用当前已配置 Auth0 开发租户地址的配置。

hybrid OIDC 应用程序类型将确保对 GreetingResourcehttps://:8080/hello 请求会启动授权码流,而 GreetingResource 发起的对 ApiEchoServicehttps://:8080/echo 请求将导致授权码流令牌被传播并被 ApiEchoService 作为 bearer JWT 访问令牌接受。

接下来,添加一个表示 ApiEchoService 的 REST Client 接口。

package org.acme;

import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;

@RegisterRestClient
@AccessToken (1)
@Path("/echo")
public interface ApiEchoServiceClient {

    @POST
    @Produces(MediaType.TEXT_PLAIN)
    String echoUserName(String username);
}
1 将访问令牌作为 HTTP Authorization: Bearer accesstoken 头进行传播

并更新 Quarkus 前端应用程序 GreetingResource 的配置,该应用程序已在此之前创建,以请求授权码流访问令牌 (而非 ID 令牌) 包含针对 ApiEchoServiceaud (audience) 声明,并配置 ApiEchoService REST Client 的基本 URL。

quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.authentication.scopes=profile
quarkus.oidc.authentication.extra-params.audience=https://quarkus-auth0 (1)
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}

# Point to the custom roles claim
quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles"

# Logout
quarkus.oidc.end-session-path=v2/logout
quarkus.oidc.logout.post-logout-uri-param=returnTo
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
quarkus.oidc.logout.path=/logout
quarkus.oidc.logout.post-logout-path=/hello/post-logout
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated

quarkus.oidc.token.verify-access-token-with-user-info=true

org.acme.ApiEchoServiceClient/mp-rest/url=https://:${port} (2)

quarkus.test.native-image-profile=test
%prod.port=8080
%dev.port=8080
%test.port=8081
1 在从 Quarkus 到 Auth0 的授权码流重定向过程中,将额外的 audience 查询参数传递给 Auth0 授权端点。这将确保颁发的访问令牌是 JWT 格式,并包含一个 aud (audience) 声明,该声明将包含 https://quarkus-auth0
2 ApiEchoServiceClient 指向 ApiEchoService 端点。org.acme.ApiEchoServiceClient/mp-rest/url=https://:${port} 属性中的 HTTP 端口是参数化的,以确保在开发、测试和生产模式下构建正确的 URL。

最后,更新 GreetingResource 以请求 ApiEchoService 回显用户名。

package org.acme;

import io.quarkus.oidc.UserInfo;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import org.eclipse.microprofile.rest.client.inject.RestClient;

@Path("/hello")
public class GreetingResource {
    @Inject
    @RestClient
    ApiEchoServiceClient echoClient; (1)

    @Inject
    UserInfo userInfo;

    @GET
    @RolesAllowed("admin")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello, " + echoClient.echoUserName(userInfo.getName()); (2)
    }

    @GET
    @Path("post-logout")
    @Produces(MediaType.TEXT_PLAIN)
    public String postLogout() {
        return "You were logged out";
    }
}
1 注入 ApiEchoServiceClient REST Client
2 使用 ApiEchoServiceClient 回显用户名。

打开浏览器,访问 https://:8080/hello,然后在浏览器中显示您的姓名。

转到 https://:8080/q/dev-ui,选择 OpenId Connect 卡片,登录到 Auth0,并检查访问令牌内容。

Auth0 DevUI JWT Access Token

正如您所见,访问令牌不再像 使用不透明的 Auth0 访问令牌访问 Quarkus 部分所示那样加密,实际上它现在是 JWT 格式的。

基于权限的访问控制

我们在 基于角色的访问控制 部分讨论了如何让 Quarkus 检查包含用户角色的命名空间限定声明,并使用此信息来强制执行基于角色的访问控制。您已将 Auth0 配置为将自定义角色声明添加到 ID 和访问令牌。

然而,基于权限的访问控制更适合这种情况:前端端点将访问令牌传播到一个微服务,该微服务将检查给定的访问令牌是否已被授权为该服务执行具体操作,而不是此令牌证明用户属于某个特定角色。例如,管理员角色并不一定意味着用户可以对该微服务的某些内容具有读写访问权限。

让我们看看如何将基于权限的访问控制约束应用于 ApiEchoService

转到 Auth0 控制面板,向 QuarkusAuth0API API 添加 echo:name 权限。

Auth0 API permissions

如果在此范围也要求在授权码流期间,echo:name 权限将作为标准的 OAuth2 scope 声明值包含在访问令牌中。更新配置如下。

quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.authentication.scopes=profile,echo:name (1)
quarkus.oidc.authentication.extra-params.audience=https://quarkus-auth0
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}

# Point to the custom roles claim
quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles"

# Logout
quarkus.oidc.end-session-path=v2/logout
quarkus.oidc.logout.post-logout-uri-param=returnTo
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
quarkus.oidc.logout.path=/logout
quarkus.oidc.logout.post-logout-path=/hello/post-logout
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated

quarkus.oidc.token.verify-access-token-with-user-info=true

org.acme.ApiEchoServiceClient/mp-rest/url=https://:8080
1 在授权码流期间将请求额外的 echo:name 范围。

现在更新 ApiEchoService 以强制执行基于权限的访问控制。

package org.acme;

import io.quarkus.security.PermissionsAllowed;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/echo")
public class ApiEchoService {

    @POST
    @PermissionsAllowed("echo:name")
    @Produces(MediaType.TEXT_PLAIN)
    String echoUserName(String username) {
        return username;
    }
}

这一切都是必需的,因为 Quarkus OIDC 会自动将 scope 声明值作为权限与当前安全身份相关联。

您可以通过组合 @RolesAllowed@PermissionsAllowed 注解来强制执行 Quarkus 中的基于角色和基于权限的访问控制。

打开浏览器,访问 https://:8080/hello,然后在浏览器中显示名称。

为了确认权限已正确强制执行,请将其更改为 echo.name@PermissionsAllowed("echo.name")。清除浏览器缓存,再次访问 https://:8080/helloApiEchoService 将报告 403。现在将其恢复为 @PermissionsAllowed("echo:name")

集成测试

您已经使用 OIDC DevUI SPA 登录到 Auth0,并使用访问令牌测试了 Quarkus 端点,在此过程中更新了端点代码。

但是,运行测试也很重要,让我们看看如何使用 Quarkus Continuous Testing 功能来测试在本教程过程中开发的端点和配置。

从以下测试代码开始。

package org.acme;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("Hello, Sergey Beryozkin"));
    }

}

如果您还记得,当应用程序以开发模式启动时,您可以在 CLI 窗口中看到以下内容。

Auth0 DevMode started

r,注意此测试因 403 而失败,这是预期的,因为测试没有向端点发送令牌。

Auth0 test failure 403

在修复测试之前,让我们回顾一下用于测试 Quarkus 端点的选项,这些端点受 OIDC 保护。这些选项可能因您的应用程序支持的流以及您偏好的测试方式而异。使用 OIDC 授权码流的端点可以使用 这些选项之一 进行测试,而使用 Bearer 令牌身份验证的端点可以使用 这些选项之一 进行测试。

如您所见,可以使用 Wiremock@TestSecurity 注解来测试受 Auth0 保护的端点。尝试自己编写此类测试,如果您遇到任何问题,请联系我们。

在本教程中,我们将使用最近添加的 OidcTestClient 来支持测试使用实时 Auth0 开发租户的端点。

这是相关的配置片段。

quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.authentication.scopes=profile
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}

在生产环境中,您将使用 %prod.%test. 限定符来区分生产和测试级别的配置。假设上述配置在您的实际应用程序中确实带有 %test. 前缀,并且此配置还包括 %prod. 限定的 Auth0 生产租户配置。

使用 OidcTestClient 测试此类配置需要使用 OAuth2 passwordclient_credentials 授予来从 Auth0 开发租户获取令牌,我们将尝试 password 授予。确保 Auth0 控制面板中注册的应用程序允许 password 授予。

Auth0 password grant

重要的是要明确,我们不建议在生产环境中使用已弃用的 OAuth2 password 令牌授予。但是,使用它可以通过从实时 Auth0 开发租户获取的令牌来帮助测试端点。

OidcTestClient 应用于测试接受 bearer 令牌的应用程序,这将适用于本教程中开发的端点,因为它同时支持授权码流和 bearer 令牌身份验证。如果您只想支持授权码流,则需要直接使用 OIDC WireMock 或 HtmlUnit 来处理 Auth0 开发租户,在后一种情况下,HtmlUnit 测试代码必须与 Auth0 如何提示用户输入凭据保持一致。如果您愿意,可以从文档中复制 HtmlUnit 测试片段 并进行实验。

同时,我们将继续修复当前失败的测试,方法是使用 OidcTestClient

首先,您必须添加以下依赖项。

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

它提供了一个实用类 io.quarkus.test.oidc.client.OidcTestClient,可以在测试中用于获取访问令牌 (此依赖项还提供 OIDC WireMock 支持 - 如果您想,请查看文档了解如何使用它进行测试)。

现在像这样更新测试代码。

package org.acme;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

import java.util.Map;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.client.OidcTestClient;

@QuarkusTest
public class GreetingResourceTest {

    static OidcTestClient oidcTestClient = new OidcTestClient();

    @AfterAll
    public static void close() {
        client.close();
    }

    @Test
    public void testHelloEndpoint() {
        given()
          .auth().oauth2(getAccessToken(`sberyozkin@gmail.com`, "userpassword"))
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("Hello, Sergey Beryozkin"));
    }

    private String getAccessToken(String name, String secret) {
        return oidcTestClient.getAccessToken(name, secret, (1)
            Map.of("audience", "https://quarkus-auth0",
	           "scope", "openid profile"));
    }
}
1 OidcTestClient 用于获取访问令牌,使用已注册用户的用户名和密码,以及 audiencescope 参数。

OidcTestClient 将自行查找 Auth0 令牌端点地址、客户端 ID 和密钥。

再次按 r,让测试通过。

Auth0 test success

顺便说一句,如果您愿意,可以直接从 DevUI 在连续模式下运行测试。

Auth0 Continuous testing

生产模式

您已经在开发模式下开发和测试了受 Auth0 保护的 Quarkus 端点。下一步是在生产模式下运行您的应用程序。在 JVM 和本机模式之间进行选择。

以 JVM 模式运行应用程序

编译应用程序

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

运行应用程序

java -jar target/quarkus-app/quarkus-run.jar

打开浏览器,访问 https://:8080/hello,然后在浏览器中显示名称。

在原生模式下运行应用程序

您无需进行任何修改即可将相同的演示编译为本机模式。这意味着您不再需要在生产环境中安装 JVM。运行时技术包含在生成的二进制文件中,并针对以最少资源运行进行了优化。

编译需要一段时间,因此默认情况下禁用此步骤。

通过启用 native 配置文件再次构建您的应用程序

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.native.enabled=true

接下来直接运行以下二进制文件。

./target/quarkus-auth0-1.0.0-SNAPSHOT-runner

打开浏览器,访问 https://:8080/hello,然后在浏览器中显示名称。

故障排除

本教程中描述的步骤应与教程描述的完全相同。您可能需要清除浏览器 cookie 来访问已更新的 Quarkus 端点 (如果您已完成身份验证)。您可能需要在开发模式下手动重新启动 Quarkus 应用程序,但这并非预期情况。如果您在完成本教程时需要帮助,可以与 Quarkus 团队联系。

总结

本教程演示了如何使用 quarkus-oidc 扩展和 Auth0,通过授权码和 Bearer 令牌身份验证流来保护 Quarkus 端点,这两种流都由相同的端点代码支持。无需编写任何代码,您就添加了对自定义 Auth0 注销流的支持,并使用自定义 Auth0 命名空间限定声明启用了基于角色的访问控制。通过将 @AccessToken 注解添加到微服务 REST Client,实现了从前端端点到微服务端的令牌传播。微服务 endpoint 使用 @PermissionsAllowed 注解激活了基于权限的访问控制。您使用 Quarkus 开发模式在不重新启动端点的情况下更新代码和配置,并且还使用 OIDC Dev UI 来可视化和测试 Auth0 令牌。您使用 Quarkus 的连续测试功能,通过与实时 Auth0 开发租户的集成测试来补充 OIDC Dev UI 测试。最后,您在 JVM 和本机模式下运行了应用程序。

享受!