Protect Quarkus web application by using an Auth0 OpenID Connect provider

Quarkus Security通过它`quarkus-oidc`扩展提供了全面的 OpenID Connect (OIDC) 和 OAuth2 支持,支持Authorization code flowBearer token验证机制。

Quarkus Security provides comprehensive OpenId Connect (OIDC) and OAuth2 support with its quarkus-oidc extension, supporting both Authorization code flow and Bearer token authentication mechanisms.

使用 Quarkus,你可以轻松配置 OIDC 提供者,例如 KeycloakOktaAuth0和其他well-known social OIDC and OAuth2 providers

With Quarkus, you can easily configure OIDC providers such as Keycloak, Okta, Auth0, and other well-known social OIDC and OAuth2 providers.

了解如何将 Quarkus OpenID Connect 扩展 (quarkus-oidc) 与 Auth0OIDC 提供者结合使用来保护你的 API 端点。

Learn how to use the Quarkus OpenID Connect extension (quarkus-oidc) together with the Auth0 OIDC provider to protect your API endpoints.

Prerequisites

Create an Auth0 application

转到 Auth0 仪表板并创建一个常规 Web 应用程序。例如,创建一个名为 `QuarkusAuth0`的 Auth0 应用程序。

Go to the Auth0 dashboard and create a regular web application. For example, create an Auth0 application called QuarkusAuth0.

auth0 create application
Result

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

Result

Your Auth0 application gets created with a client ID, secret, and HTTPS-based domain. Make a note of these properties because you will need them to complete the Quarkus configuration in the next step.

auth0 created application

接下来,在仍然位于 Auth0 仪表板的情况下,将一些用户添加到你的应用程序中。

Next, while still in the Auth0 dashboard, add some users to your application.

auth0 add user

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

Now that you have successfully created and configured your Auth0 application, you are ready to start creating and configuring a Quarkus endpoint. In the steps that follow, you will continue to configure and update the Auth0 application as well.

Create a Quarkus application

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

Use the following Maven command to create a Quarkus REST (formerly RESTEasy Reactive) application that can be secured with the Quarkus OIDC extension.

Unresolved directive in security-oidc-auth0-tutorial.adoc - include::{includes}/devtools/create-app.adoc[]

创建应用程序工作区并将其导入你最喜欢的 IDE。我们添加一个只有经过身份验证的用户才能访问的 Jakarta REST 端点:

Create the application workspace and import it into your favorite IDE. Let’s add a Jakarta REST endpoint that can only be accessed by authenticated users:

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 The injected JsonWebToken (JWT) bean has an @IdToken qualifier, which means it represents not an access token but OIDC ID token. IdToken provides information in the form of claims about the current user authenticated during the OIDC authorization code flow and you can use JsonWebToken API to access these claims.
2 The io.quarkus.security.Authenticated annotation is added to the hello() method, which means that only authenticated users can access it.

授权代码流期间获取的访问令牌和 ID 令牌不会被端点直接使用,而是仅用于代表当前经过身份验证的用户访问下游服务。本教程稍后会详细介绍“访问令牌”的主题。

The access token acquired during the authorization code flow, alongside the ID token, is not used directly by the endpoint but is used only to access downstream services on behalf of the currently authenticated user. More to come on the topic of "access tokens", later in this tutorial.

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

Configure OIDC in the Quarkus application.properties file by using the properties from the Auth0 application that you created earlier.

# 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 授权代码流,但还有一些其他方法,稍后会在本教程中讨论这些方法。

In completing this step, you have just configured Quarkus to use the domain, client ID, and secret of your Auth0 application. Setting the property quarkus.oidc.application-type=web-app instructs Quarkus to use the OIDC authorization code flow, but there are also other methods, which are discussed later on in the tutorial.

端点地址将为 [role="bare"]http://localhost:8080/hello,这也必须在您的 Auth0 应用程序中注册为允许的回调 URL。

The endpoint address will be http://localhost:8080/hello, which must also be registered as an allowed callback URL in your Auth0 application.

auth0 allowed callback

完成此步骤后,当您从浏览器访问 Quarkus [role="bare"]http://localhost:8080/hello 端点时,Auth0 在完成身份验证后会将您重定向回同一地址。

After completing this step, when you access the Quarkus http://localhost:8080/hello endpoint from a browser, Auth0 redirects you back to the same address after the authentication is completed.

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

By default, Quarkus automatically uses the current request path as the callback path. But you can override the default behavior and configure a specific callback path by setting the Quarkus quarkus.oidc.authentication.redirect-path property.

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

In production, your application will most likely have a larger URL space, with multiple endpoint addresses available. In such cases, you can set a dedicated callback (redirect) path and register this URL in the provider’s dashboard, as outlined in the following configuration example: quarkus.oidc.authentication.redirect-path=/authenticated-welcome

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

In the example scenario, Quarkus calls /authenticated-welcome after accepting a redirect from Auth0, completing the authorization code flow, and creating the session cookie. Successfully authenticated users are also allowed to access other parts of the secured application space, without needing to authenticate again. For example, the endpoint callback method can use a JAX-RS API to redirect users to other parts of the secured application where a session cookie will be verified.

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

Now you are ready to start testing the endpoint.

Test the Quarkus endpoint

在开发模式下启动 Quarkus:

Start Quarkus in dev mode:

$ mvn quarkus:dev

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

This is the only time during this tutorial when you are expected to manually start Quarkus in dev mode. The configuration and code update steps in the remaining sections of this tutorial are automatically observed and processed by Quarkus without you needing to restart the application manually.

打开浏览器并访问 [role="bare"][role="bare"]http://localhost:8080/hello。

Open the browser and access [role="bare"]http://localhost:8080/hello.

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

You will be redirected to Auth0 and prompted to log in:

auth0 login

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

and authorize the QuarkusAuth0 application to access your account:

auth0 authorize

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

Finally, you will be redirected back to the Quarkus endpoint which will return the following response: Hello, auth0|60e5a305e8da5a006aef5471

请注意,当前用户名未返回。要了解为何会出现此行为,您可以在“OpenID Connect (OIDC) 的开发服务和 UI”指南中的 Dev UI for all OpenID Connect Providers 部分和以下部分中了解有关如何使用 OIDC Dev UI 的信息。

Notice that the current username does not get returned. To learn more about why this behavior occurs, you can use OIDC Dev UI as explained in the Dev UI for all OpenID Connect Providers section of the "Dev Services and UI for OpenID Connect (OIDC)" guide and the following section.

Looking at Auth0 tokens in the OIDC Dev UI

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

Quarkus provides a great Dev UI experience. Specifically, Quarkus offers built-in support for developing and testing OIDC endpoints with a Keycloak container. DevService for Keycloak is automatically started and used if the address of the OIDC provider is not specified for the Quarkus quarkus.oidc.auth-server-url configuration property.

当提供商已配置时,你可以继续使用 Quarkus OIDC 开发 UI。使用以下说明来更新你的配置:

You can continue using the Quarkus OIDC Dev UI when the provider is already configured. Use the following instructions to update your configuration:

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

First, change your Quarkus application type from web-app to hybrid, as follows:

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 Application type is changed to hybrid because OIDC Dev UI currently supports SPA (single-page application) mode only. OIDC Dev UI single-page application, using its own Java Script, authenticates users to the OIDC provider and uses the access token as a Bearer token to access the Quarkus endpoint as a service.

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

Typically, Quarkus must be configured with quarkus.oidc.application-type=service to support Bearer token authentication, but it also supports a hybrid application type, which means it can support both the authorization code and bearer token flows at the same time.

你还需要配置 Auth0 应用程序,以允许回调到 OIDC 开发 UI。使用以下 URL 格式:

You also need to configure the Auth0 application to allow the callbacks to the OIDC Dev UI. Use the following URL format:

  • Where in this example, the ${provider-name} is auth0

auth0 allowed callbacks

现在,你可以与 Auth0 一起使用 OIDC 开发 UI。

Now you are ready to use OIDC Dev UI with Auth0.

在浏览器会话中打开 [role="bare"][role="bare"]http://localhost:8080/q/dev/ 。一个 OpenId Connect 卡显示,该卡链接到一个 Auth0 提供商 SPA,如下所示:

Open [role="bare"]http://localhost:8080/q/dev/ in a browser session. An OpenId Connect card that links to an Auth0 provider SPA displays, as follows:

auth0 devui

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

Click Auth0 provider followed by Login into Single Page Application:

auth0 devui login to spa

你将被重定向到 Auth0 以登录。然后,你将被重定向到 OIDC 开发 UI 仪表板,如下所示:

You will be redirected to Auth0 to log in. You will then be redirected to the OIDC Dev UI dashboard, as follows:

auth0 devui dashboard without name

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

Here, you can look at both ID and access tokens in the encoded and decoded formats, copy them to the clipboard or use them to test the service endpoint. We will test the endpoint later but for now let’s check the ID token:

auth0 idtoken without name

正如你所看到的,它没有任何代表用户名的声明,但如果你检查其 sub (主题)声明,你将看到它的值与你直接从浏览器访问 Quarkus 终结点时在响应中获得的值相匹配, auth0|60e5a305e8da5a006aef5471

As you can see it does not have any claim representing a user name but if you check its sub (subject) claim you will see its value matches what you got in the response when you accessed the Quarkus endpoint directly from the browser, auth0|60e5a305e8da5a006aef5471.

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

Fix it by configuring Quarkus to request a standard OIDC profile scope during the authentication process which should result in the ID token including more information:

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 Request profile scope in addition to the default openid scope.

返回 [role="bare"][role="bare"]http://localhost:8080/q/dev/,重复登录 Auth0 的过程,然后再次检查 ID 令牌,现在你应该看到 ID 令牌包含 name 声明:

Go back to [role="bare"]http://localhost:8080/q/dev/, repeat the process of logging in to Auth0 and check the ID token again, now you should see the ID token containing the name claim:

auth0 idtoken with name

当你直接从浏览器访问 Quarkus 终结点时,你应该会获得该名称。清除浏览器 cookie 缓存,访问 [role="bare"][role="bare"]http://localhost:8080/hello,你仍然会收到 Hello, auth0|60e5a305e8da5a006aef5471 返回。嗯,出了什么问题?

You should get the name when you access the Quarkus endpoint directly from the browser. Clear the browser cookie cache, access [role="bare"]http://localhost:8080/hello and yet again, you get Hello, auth0|60e5a305e8da5a006aef5471 returned. Hmm, what is wrong ?

答案在于 org.eclipse.microprofile.jwt.JsonWebToken#getName() 实现的具体内容,根据 MicroProfile MP JWT RBAC specification,它检查一个 MP JWT 特定的 upn 声明,然后尝试 preferred_username 最后尝试 sub,这解释了为什么即使 ID 令牌包含 name 声明你也会收到 Hello, auth0|60e5a305e8da5a006aef5471 答案。我们可以通过将终结点 hello() 方法的实现更改为返回特定声明值来轻松解决此问题:

The answer lies with the specifics of the org.eclipse.microprofile.jwt.JsonWebToken#getName() implementation, which, according to the MicroProfile MP JWT RBAC specification, checks an MP JWT specific upn claim, trying preferred_username next and finally sub which explains why you get the Hello, auth0|60e5a305e8da5a006aef5471 answer even with the ID token containing the name claim. We can fix it easily by changing the endpoint hello() method’s implementation to return a specific claim value:

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

现在清除浏览器缓存,访问 [role="bare"][role="bare"]http://localhost:8080/hello,最后返回用户名。

Now clear the browser cache, access [role="bare"]http://localhost:8080/hello and finally the user name is returned.

Logout support

在 Auth0 的帮助下让用户登录 Quarkus 后,您可能希望支持用户发起的注销。Quarkus 支持 RP-initiated and other standard OIDC logout mechanisms, as well as the local session logout

Now that you have the users signing in to Quarkus with the help of Auth0, you probably want to support a user-initiated logout. Quarkus supports RP-initiated and other standard OIDC logout mechanisms, as well as the local session logout.

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

Currently, Auth0 does not support the standard OIDC RP-initiated logout and does not provide an end session endpoint URL in its discoverable metadata, but it provides its own logout mechanism which works nearly exactly the same as the standard one.

使用 Quarkus OIDC 可以很容易地支持它。您必须配置一个 Auth0 会话结束端点 URL,并让 Quarkus 在发往 Auth0 的 RP 发起的注销重定向请求中包含 `client-id`查询参数和注销后 URL 作为 `returnTo`查询参数:

It is easy to support it with Quarkus OIDC. You must configure an Auth0 end session endpoint URL and have Quarkus include both the client-id query parameter and the post logout URL as the returnTo query parameter in the RP-initated logout redirect request to Auth0:

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 does not include the end session URL in its metadata, so complement it with manually configuring the Auth0 end session endpoint URL.
2 Auth0 will not recognize a standard post_logout_redirect_uri query parameter and expects a parameter returnTo instead.
3 Auth0 expects client-id in the logout request.
4 Authenticated requests to /logout path will be treated as RP-inititated logout requests.
5 This is a public resource to where the logged out user should be returned to.
6 Make sure the /logout path is protected.

在这里,我们自定义了 Auth0 会话结束端点 URL,并向 Quarkus 进行了指示,表示一个 http://localhost:8080/logout`请求必须触发当前经过身份验证的用户的注销。关于 `/logout`路径的有趣之处在于它是一个 `virtual,并且不被 JAX-RS 端点中的任何方法支持,因此,为了使 Quarkus OIDC 能够对 /logout`请求做出反应,我们将一个 `authenticated HTTP security policy直接附加到配置中的此路径。

Here we have customized the Auth0 end session endpoint URL and indicated to Quarkus that an http://localhost:8080/logout request must trigger a logout of the currently authenticated user. An interesting thing about the /logout path is that it is virtual, it is not supported by any method in the JAX-RS endpoint, so for Quarkus OIDC to be able to react to /logout requests we attach an authenticated HTTP security policy to this path directly in the configuration.

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

We also have configured Quarkus to return the logged out user to the public /hello/post-logout resource, and this path is included in the logout request as the Auth0 specific returnTo query parameter. Finally, the Quarkus application’s client-id is included in the logout URL as well.

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

Update the endpoint to accept the post logout redirects:

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`资源方法。

Note the addition of the public /hello/post-logout resource method.

在我们测试注销之前,请确保配置了 `Auth0`应用程序,允许用户注销后将此注销后重定向返回到 Quarkus:

Before we test the logout, make sure the Auth0 application is configured to allow this post logout redirect back to Quarkus after the user has been logged out:

auth0 allowed logout

现在,清除浏览器 Cookie 缓存,访问 [role="bare"][role="bare"]http://localhost:8080/hello,使用 Auth0 登录 Quarkus,获得返回的用户名,然后转到 http://localhost:8080/logout。您会在浏览器中看到显示的 `You were logged out`消息。

Now, clear the browser cookie cache, access [role="bare"]http://localhost:8080/hello, login to Quarkus with Auth0, get the user name returned, and go to http://localhost:8080/logout. You’ll see the You were logged out message displayed in the browser.

下一步,转到 [role="bare"][role="bare"]http://localhost:8080/q/dev/,使用 Dev UI SPA 从 Auth0 登录,并注意您现在也可以从 OIDC Dev UI 注销,请参阅表示注销的符号,该符号显示在 `Logged in as Sergey Beryozkin`文本旁边:

Next, go to the [role="bare"]http://localhost:8080/q/dev/, login to Auth0 from the Dev UI SPA and notice you can now logout from the OIDC Dev UI too, see the symbol representing the logout next to the Logged in as Sergey Beryozkin text:

auth0 devui dashboard with name

为了从 OIDC DevUI 执行注销,Auth0 应用程序的允许注销回调列表必须更新为包括 OIDC DevUI 端点:

For the logout to work from OIDC DevUI, the Auth0 application’s list of allowed logout callbacks has to be updated to include the OIDC DevUI endpoint:

auth0 allowed logouts

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

Now logout directly from OIDC Dev UI and login as a new user - add more users to the registered Auth0 application if required.

Role-based access control

我们已经确认,经过 `Auth0`身份验证的用户可以访问 Quarkus 端点。

We have confirmed that the Quarkus endpoint can be accessed by users who have authenticated with the help of Auth0.

下一步是引入基于角色的访问控制 (RBAC),让特定角色中的用户(例如 admin)能够访问端点。

The next step is to introduce role-based access control (RBAC) to have users in a specific role only, such as admin, be able to access the endpoint.

另请参见下面的 Permission Based Access Control 部分。

See also the Permission Based Access Control section below.

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

Auth0 tokens do not include any claims containing roles by default, so, first, you must customize the Login flow of the Auth0 application with a custom action which will add the roles to tokens. Select Actions/Flows/Login in the Auth0 dashboard, choose Add Action/Build Custom, name it as AddRoleClaim:

auth0 add role action

向其中添加以下操作脚本:

Add the following action script to it:

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"。查看我们在前面的部分中分析的身份令牌内容,您将看到此声明的表示方式,例如:

Note a custom Auth0 claim has to be namespace qualified, so the claim which will contain roles will be named as "https://quarkus-security.com/roles". Have a look at the ID token content we analyzed in the previous sections and you will see how this claim is represented, for example:

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

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

The Auth0 Login Flow diagram should look like this now:

auth0 login flow

您必须向在 Auth0 应用程序中注册的用户添加角色,例如 admin

You must add a role such as admin to the users registered in the Auth0 application.

创建 admin 角色:

Create an admin role:

auth0 create role

并将其添加到已注册用户:

and add it to the registered user:

auth0 add role to user

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

Next, update the Quarkus endpoint to require that only users with the admin role can access the endpoint:

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

打开 [role="bare"][role="bare"]http://localhost:8080/hello,向 Auth0 进行身份验证并获取 403。您获得 403 的原因是 Quarkus OIDC 无法识别 Auth0 令牌中的哪个声明表示角色信息,默认情况下会检查 groups 声明,而 Auth0 令牌现在必须具有 "https://quarkus-security.com/roles" 声明。

Open [role="bare"]http://localhost:8080/hello, authenticate to Auth0 and get 403. The reason you get 403 is because Quarkus OIDC does not know which claim in the Auth0 tokens represents the roles information, by default a groups claim is checked, while Auth0 tokens are now expected to have an "https://quarkus-security.com/roles" claim.

通过告诉 Quarkus OIDC 必须检查哪个声明来实施 RBAC,来解决此问题:

Fix it by telling Quarkus OIDC which claim must be checked to enforce 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 Point to the custom roles claim. The path to the roles claim is in double quotes because the claim is namespace qualified.

现在,清除浏览器 Cookie 缓存,再次访问 [role="bare"][role="bare"]http://localhost:8080/hello,向 Auth0 进行身份验证并获取预期用户名。

Now, clear the browser cookie cache, access [role="bare"]http://localhost:8080/hello again, authenticate to Auth0 and get an expected user name.

Access Quarkus with opaque Auth0 access tokens

此部分的主要目的是说明如何调整 Quarkus 以接受 opaque 持有者 Auth0 访问令牌,而不是 Auth0 JWT 访问令牌,因为在授权代码流期间颁发的 Auth0 访问令牌默认情况下是不可见的,并且只能用来请求 UserInfo,除此外还包括身份令牌中已包含的有关当前用户的信息。学习如何验证不可见令牌非常有用,因为许多 OIDC 和 OAuth2 供应商只会颁发不可见访问令牌。

The main goal of this section is to explain how Quarkus can be tuned to accept opaque bearer Auth0 access tokens as opposed to Auth0 JWT access tokens because Auth0 access tokens issued during the authorization code flow are opaque by default and they can only be used to request UserInfo in addition to the information about the current user which is already available in ID token. Learning how to verify opaque tokens can be useful because many OIDC and OAuth2 providers will issue opaque access tokens only.

有关如何配置 Auth0 和 Quarkus 以在 JWT 格式中颁发授权代码访问令牌并将其传播到服务端点的更多信息,请参阅以下 Propagate access tokens to microservicesAccess tokens in JWT format 部分。

For more information on how to configure Auth0 and Quarkus to have authorization code access tokens issued in the JWT format and propagated to service endpoints, see the following Propagate access tokens to microservices and Access tokens in JWT format sections.

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

So far we have only tested the Quarkus endpoint using OIDC authorization code flow. In this flow you use the browser to access the Quarkus endpoint, Quarkus itself manages the authorization code flow, a user is redirected to Auth0, logs in, is redirected back to Quarkus, Quarkus completes the flow by exchanging the code for the ID, access, and refresh tokens, and works with the ID token representing the successful user authentication. The access token is not relevant at the moment. As mentioned earlier, in the authorization code flow, Quarkus will only use the access token to access downstream services on behalf of the currently authenticated user.

不过,让我们想象一下,我们开发的 Quarkus 端点还必须接受 Bearer 访问令牌:它可能是将访问令牌传播到此端点的另一个 Quarkus 端点,或者可能是使用访问令牌来访问 Quarkus 端点的 SPA。我们已经用于分析身份令牌的 Quarkus OIDC DevUI SPA 非常适合于使用可供 SPA 使用的访问令牌来测试 Quarkus 端点。

Let’s imagine though that the Quarkus endpoint we have developed has to accept Bearer access tokens too: it may be that the other Quarkus endpoint which is propagating it to this endpoint or it can be SPA which uses the access token to access the Quarkus endpoint. And Quarkus OIDC DevUI SPA which we already used to analyze the ID token fits perfectly for using the access token available to SPA to test the Quarkus endpoint.

我们再次转到 [role="bare"][role="bare"]http://localhost:8080/q/dev-ui,选择 OpenId Connect 卡片,登录到 Auth0,并检查访问令牌内容:

Let’s go again to [role="bare"]http://localhost:8080/q/dev-ui, select the OpenId Connect card, login to Auth0, and check the Access token content:

auth0 devui accesstoken

与我们之前查看的 ID 令牌相反,Quarkus 无法直接验证此访问令牌。这是因为访问令牌采用 JWE(加密)格式,而未使用 JWS(签名)格式。您可从解码令牌头中看到,它已直接通过仅 Auth0 知道的一个密钥进行加密,因此 Quarkus 无法解密其内容。从 Quarkus 的角度看,此访问令牌是一个 opaque,Quarkus 无法使用公共 Auth0 非对称验证密钥对其进行验证。

This access token, as opposed to the ID token we looked at earlier, cannot be verified by Quarkus directly. This is because the access token is in JWE (encrypted) as opposed to JWS (signed) format. You can see from the decoded token headers that it has been encrypted directly with a secret key known to Auth0 only, and therefore its content cannot be decrypted by Quarkus. From the Quarkus’s perspective this access token is an opaque one, Quarkus cannot use public Auth0 asymmetric verification keys to verify it.

为确认这一点,请在 Test Service 区域中将 /hello 作为 Service Address 输入,然后按 With Access Token,您将获得 HTTP 401 状态:

To confirm it, enter /hello as the Service Address in the Test Service area and press With Access Token and you will get the HTTP 401 status:

auth0 devui test accesstoken 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 没有内省端点:

For Quarkus be able to accept such access tokens, one of the two options should be available. The first option is to introspect the opaque token remotely using a provider’s introspection endpoint. Token introspection is typically supported at the OAuth2 level, and since OIDC is built on top of OAuth2, some OIDC providers such as Keycloak support the token introspection as well. However, Auth0 does not support the token introspection, you can check it by looking at the publicly available Auth0 metadata, add /.well-known/openid-configuration to the address of your configured Auth0 provider, and open the resulting URL, https://dev-3ve0cgn7.us.auth0.com/.well-known/openid-configuration. You will see that Auth0 does not have an introspection endpoint:

auth0 well known config

因此,可以使用另一个选项,即间接访问令牌验证,其中访问令牌用于从 Auth0 中获取 UserInfo,以接受并验证不透明的 Auth0 令牌。此选项有效,因为 OIDC 提供商必须在他们发出 UserInfo 之前验证访问令牌,而且 Auth0 拥有 UserInfo 端点。

Therefore the other option, indirect access token verification, where the access token is used to acquire UserInfo from Auth0 can be used to accept and verify opaque Auth0 tokens. This option works because OIDC providers have to verify access tokens before they can issue UserInfo and Auth0 has a UserInfo endpoint.

现在让我们配置 Quarkus 以请求使用它们获取 UserInfo 来验证访问令牌:

So lets configure Quarkus to request that the access tokens must be verified by using them to acquire 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 Verify access tokens indirectly by using them to request UserInfo.

更新端点代码以期待 UserInfo 而不是 ID token

Update the endpoint code to expect UserInfo as opposed to ID token:

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

此代码现在同时适用于授权代码和持有者访问令牌流。

This code will now work both for the authorization code and bearer access token flows.

让我们转到 OIDC Dev UI,我们在其中查看了访问令牌,在 Test Service 区域中将 /hello 输入为 Service Address,并按下 With Access Token,您将得到 200

Let’s go to the OIDC Dev UI where we looked at the access token, enter /hello as the Service Address in the Test Service area and press With Access Token and you will get 200:

auth0 devui test accesstoken 200

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

To confirm that it really does work, update the test endpoint to allow a user role only with @RolesAllowed("user"). Try to access the endpoint from OIDC Dev UI again, and you will get the HTTP 403 error. Revert the code back to @RolesAllowed("admin") to get the reassuring HTTP 200 status again.

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

When verifying the opaque access token indirectly, by using it to request UserInfo, Quarkus will use UserInfo as the source of the roles information, if any. As it happens, Auth0 includes the custom role claim which was created earlier in the UserInfo response as well.

正如本节导言中已经提到的,本节的主要目标是解释 Quarkus 如何验证不透明访问令牌。通常,应该避免向其唯一目的是允许检索 UserInfo 到服务的访问令牌传播,除非前端 JAX-RS 端点或 SPA 更喜欢将 UserInfo 检索委托给可信服务。

As has already been mentioned in the introduction to this section, the main goal of this section is to explain how Quarkus can verify opaque access tokens. In general, propagating access tokens whose only purpose is to allow retrieving UserInfo to services should be avoided unless the front-end JAX-RS endpoint or SPA prefers to delegate UserInfo retrieval to the trusted service.

有关使用 Auth0 访问令牌的推荐方法,请参阅以下 Propagate access tokens to microservicesAccess tokens in JWT format 部分。

For a recommended approach of working with Auth0 access tokens, see the following Propagate access tokens to microservices and Access tokens in JWT format sections.

通常,人们使用访问令牌访问远程服务,但 OIDC DevUI SPA 仪表板也提供了一个使用 ID 令牌进行测试的选项。此选项仅适用于模拟 SPA 将一些信息从 ID 令牌中委托给端点以进行验证和检索的情况供 SPA 使用的情况 - 但 ID 令牌仍将作为持有者令牌发送到端点由 OIDC DevUI。在大多数情况下,更喜欢用访问令牌进行测试。

Typically one uses access tokens to access remote services but OIDC DevUI SPA dashboard also offers an option to test with the ID token. This option is only available to emulate the cases where SPA delegates to the endpoint to verify and retrieve some information from the ID token for SPA to use - but ID token will still be sent to the endpoint as Bearer token by OIDC DevUI. Prefer testing with the access token in most cases.

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

You can use SwaggerUI or GraphQL from OIDC DevUI for testing the service, instead of manually entering the service path to test. For example, if you add

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

到您的应用程序的 pom,那么您将在 OIDC Dev UI 中看到一个 Swagger 链接:

to your application’s pom then you will see a Swagger link in OIDC Dev UI: image::auth0-devui-testservice-swagger.png[]

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

Click the Swagger link and start testing the service.

Propagate access tokens to microservices

在我们设法使用 OIDC 授权码流并使用 ID 令牌和 UserInfo 来访问用户的信息后,下一个典型任务是传播当前 Auth0 访问令牌,以代表当前已验证的用户访问下游服务。

Now that we have managed to use OIDC authorization code flow and used both ID token and UserInfo to access the user information, the next typical task is to propagate the current Auth0 access token to access the downstream service on behalf of the currently authenticated user.

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

In fact, the last code example, showing the injected UserInfo, is a concrete example of the access token propagation, in this case, Quarkus propagates the Auth0 access token to the Auth0 UserInfo endpoint to acquire UserInfo. Quarkus does it without users having to do anything themselves.

但如何将访问令牌传播到某些自定义服务?在 Quarkus 中,实现这一点非常容易,无论是对于授权码还是持有者令牌流。你需要做的就是创建 REST Client 接口以调用需要持有者令牌访问的服务,并使用 `@AccessToken`对其进行注释,并且到达前端端点的访问令牌(作为 Auth0 持有者访问令牌或在完成 Auth0 授权码流后由 Quarkus 获取)将被传播到目标微服务。这非常简单。

But what about propagating access tokens to some custom services ? It is very easy to achieve in Quarkus, both for the authorization code and bearer token flows. All you need to do is to create a REST Client interface for calling the service requiring a Bearer token access and annotate it with @AccessToken and the access token arriving to the front-end endpoint as the Auth0 Bearer access token or acquired by Quarkus after completing the Auth0 authorization code flow, will be propagated to the target microservice. This is as easy as it can get.

有关传播访问令牌的示例,请参阅本教程中的以下部分。有关令牌传播的更多信息,请参阅 OIDC token propagation

For examples of propagating access tokens, see the following sections in this tutorial. For more information about token propagation, see OIDC token propagation.

Access tokens in JWT format

我们已经详细研究了 Quarkus OIDC 如何处理 Access Quarkus with opaque Auth0 access tokens,但我们不希望将 Auth0 不透明令牌传播到代表当前经过验证的用户执行一些有用操作的微服务,而不仅仅是检查其 UserInfo。

We have already looked in detail at how Quarkus OIDC can handle Access Quarkus with opaque Auth0 access tokens, but we don’t want to propagate Auth0 opaque tokens to micro services which do something useful on behalf on the currently authenticated user, beyond checking its UserInfo.

前端 Quarkus 应用程序将通过将授权码流访问令牌传播到它而访问的微服务 在 Auth0 仪表板中表示为 API。我们将其添加到 `Applications/APIs`中:

A microservice which the front-end Quarkus application will access by propagating authorization code flow access tokens to it is represented in the Auth0 dashboard as an API. Let’s add it in the Applications/APIs:

auth0 api

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

The https://quarkus-auth0 identifier of the created QuarkusAuth0API will serve as this API’s audience. Providing this audience as a query parameter in the authorization code flow redirect to Auth0 will ensure that Auth0 issues access tokens in the JWT format.

API microservice

将以下依赖项添加到项目,以支持 OIDC 令牌传播和 REST 客户端:

Add the following dependencies to the project to support OIDC token propagation and REST clients:

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`服务类:

Create ApiEchoService service class:

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 获取公共验证密钥。此微服务的配置应该只有一行:

And configure it as an OIDC service application which will only fetch public verification keys from Auth0. The configuration for this microservice should only have a single line:

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

这是 OIDC `service`应用程序获取 Auth0 公共验证密钥并使用它们以 JWT 格式验证 Auth0 访问令牌所需的一切。

which is all what is needed for the OIDC service application to fetch Auth0 public verification keys and use them to verify Auth0 access tokens in JWT format.

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

In this tutorial you have already configured the OIDC hybrid application which can handle both authorization code and bearer token authentication flows. In production you will run microservices as separate servers but for the sake of simplicity ApiEchoService will not have to be started as a second server with its own configuration containing quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com only, and therefore the current configuration which already has the Auth0 dev tenant address configured will be reused.

`hybrid`OIDC 应用程序类型将确保对 `GreetingResource`的 `http://localhost:8080/hello`请求发起授权码流,而由 `GreetingResource`发起对 `ApiEchoService`的 `http://localhost:8080/echo`请求将导致 `ApiEchoService`以持有者 JWT 访问令牌的形式传播和接受授权码流令牌。

The hybrid OIDC application type will ensure that http://localhost:8080/hello requests to GreetingResource initiate an Authorization code flow while http://localhost:8080/echo requests to ApiEchoService, initiated by GreetingResource, will lead to the authorization code flow tokens being propagated and accepted by ApiEchoService as bearer JWT access tokens.

接下来,添加表示 `ApiEchoService`的 REST 客户端接口:

Next, add a REST client interface representing ApiEchoService:

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.AccessToken;

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

    @POST
    @Produces(MediaType.TEXT_PLAIN)
    String echoUserName(String username);
}
1 Propagate access token as an HTTP Authorization: Bearer accesstoken header

并更新为早期创建的 Quarkus 前端应用程序的配置 GreetingResource,要求授权码流访问令牌(而不是 ID 令牌)包括针对 ApiEchoService`的 `aud(受众)声明,以及配置 `ApiEchoService`REST 客户端的基本 URL:

And update the configuration for the Quarkus front-end application, GreetingResource, which has been created earlier, to request that an authorization code flow access token (as opposed to ID token) includes an aud (audience) claim targeting ApiEchoService, as well as configure the base URL for the ApiEchoService REST client:

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=http://localhost:${port} 2

quarkus.test.native-image-profile=test
%prod.port=8080
%dev.port=8080
%test.port=8081
1 Pass an extra audience query parameter to the Auth0 authorization endpoint during the authorization code flow redirect from Quarkus to Auth0. It will ensure that the access token is issued in the JWT format and includes an aud (audience) claim which will contain https://quarkus-auth0.
2 Point ApiEchoServiceClient to the ApiEchoService endpoint. HTTP port in the org.acme.ApiEchoServiceClient/mp-rest/url=http://localhost:${port} property is parameterized to ensure the correct URL is built while using the dev, test and prod modes.

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

Finally update GreetingResource to request that ApiEchoService echoes a user name:

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 Inject ApiEchoServiceClient REST client
2 Use ApiEchoServiceClient to echo the user name.

打开浏览器,访问 [role="bare"][role="bare"]http://localhost:8080/hello,然后在浏览器中显示你的姓名。

Open a browser, access [role="bare"]http://localhost:8080/hello and get your name displayed in the browser.

让我们转到 [role="naked"][role="bare"]http://localhost:8080/q/dev-ui,选择 OpenId Connect 卡,登录到 Auth0,然后查看访问令牌内容:

Let’s go to [role="bare"]http://localhost:8080/q/dev-ui, select the OpenId Connect card, login to Auth0, and check the Access token content:

auth0 devui jwt accesstoken

如您所见,访问令牌不再像 Access Quarkus with opaque Auth0 access tokens 部分中显示的那样进行加密,而实际上它现在采用 JWT 格式。

As you can see, the access token is no longer encrypted as shown in the Access Quarkus with opaque Auth0 access tokens section and indeed it is in the JWT format now.

Permission Based Access Control

我们已在 Role-based access control 部分中讨论了如何获取 Quarkus 以检查包含用户角色的命名空间限定声明,以及如何使用此信息来强制基于角色的访问控制。您已配置 Auth0 以将自定义角色声明同时添加到 ID 和访问令牌中。

We have discussed in the Role-based access control section how to get Quarkus to check a namespace qualified claim containing user roles and use this information to enforce role-based access control. You have configured Auth0 to add the custom roles claim to both ID and access tokens.

但是,基于权限的访问控制更适合于以下情况:访问令牌由前端端点传播到微服务,该微服务将检查给定的访问令牌是否已被授权此服务执行具体操作,而不是此令牌担保用户处于特定角色中。例如,处于管理员角色并不一定意味着该用户被允许读写此微服务的部分内容。

However, Permission Based Access Control is better suited to the case where an access token is propagated by the front-end endpoint to a microservice which will check if a given access token has been authorized for this service to perform a concrete action, as opposed to this token vouching for a user be in a specific role. For example, being in the admin role does not necessarily mean the user is allowed to have a read and write access to some of this microservice’s content.

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

Let’s see how Permission Based Access Control constraints can be applied to ApiEchoService.

转到 Auth0 仪表板,向 QuarkusAuth0API API 添加 echo:name 权限:

Go to the Auth0 dashboard, add an echo:name permission to the QuarkusAuth0API API:

auth0 api permissions

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

The echo:name permission will be included in the access token as a standard OAuth2 scope claim value if this scope will also be requested during the authorization code flow. Update the configuration as follows:

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=http://localhost:8080
1 An extra echo:name scope will be requested during the authorization code flow.

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

Now update ApiEchoService to enforce Permission Based Access Control:

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 声明值作为权限与当前安全身份关联。

This is all what is needed as Quarkus OIDC automatically associates scope claim values as permissions with the current security identity.

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

You can enforce both Role Based and Permission Based Access Controls in Quarkus by combining @RolesAllowed and @PermissionsAllowed annotations.

打开浏览器,访问 [role="bare"][role="bare"]http://localhost:8080/hello,并在浏览器中显示该名称。

Open a browser, access [role="bare"]http://localhost:8080/hello and get the name displayed in the browser.

为确认已正确强制权限,请将其更改为 echo.name: @PermissionsAllowed("echo.name")。清除浏览器缓存,再次访问 [role="naked"][role="bare"]http://localhost:8080/hello 您将收到 ApiEchoService 报告的 403。现在将其恢复为 @PermissionsAllowed("echo:name")

To confirm the permission is correctly enforced, change it to echo.name: @PermissionsAllowed("echo.name"). Clear the browser cache, access [role="bare"]http://localhost:8080/hello again and you will get 403 reported by ApiEchoService. Now revert it back to @PermissionsAllowed("echo:name").

Integration testing

你已经使用 OIDC DevUI SPA 登录到 Auth0 并通过访问令牌测试了 Quarkus 端点,同时更新了端点代码。

You have already used OIDC DevUI SPA to login to Auth0 and test the Quarkus endpoint with the access token, updating the endpoint code along the way.

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

However, running tests is also essential, lets see how we can test the endpoint and configuration which you have developed during the course of this tutorial, using Quarkus Continuous Testing feature.

从以下测试代码开始:

Start with the following test code :

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 窗口中看到以下内容:

If you recall, when the application was started in dev mode, the following could be seen in the CLI window:

auth0 devmode started

按“r”,然后注意此测试失败,结果为“403”,这是预期的,因为测试未向端点发送令牌:

Press r and notice this test failing with 403 which is expected because the test does not send a token to the endpoint:

auth0 test failure 403

在修复测试之前,让我们回顾一下用于测试受 OIDC 保护的 Quarkus 端点的可用选项。这些选项可能有所不同,具体取决于您的应用程序支持的流程以及您首选的测试方式。可以使用“one of these options”测试使用 OIDC 授权代码流程的端点,可以使用“one of these options”测试使用 Bearer 令牌认证的端点。

Before fixing the test, let’s review the options available for testing Quarkus endpoints secured by OIDC. These options might vary, depending on which flow your application supports and how you prefer to test. Endpoints which use OIDC authorization code flow can be tested using one of these options and endpoints which use Bearer token authentication can be tested using one of these options.

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

As you can see, testing of the endpoints secured with Auth0 can be done with the help of Wiremock, or @TestSecurity annotation. Experiment with writing such tests on your own and reach out if you encounter any problems.

不过,在本教程中,我们将使用最新添加的“OidcTestClient”来支持测试使用真实 Auth0 开发租户的端点。

In this tutorial though, we will use a recently added OidcTestClient to support testing endpoints which use live Auth0 development tenants.

以下是配置的相关片段:

Here is a related fragment of the configuration:

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.”限定符区分 prod 和测试级别配置。让我们假设上述配置实际上将在您的真实应用程序中以“%test.”为前缀,该配置还包括“%prod.”限定的 Auth0 生产租户配置。

In production, you will distinguish between prod and test level configuration with %prod. and %test. qualifiers. Let’s assume that the above configuration will indeed be prefixed with %test. in your real application, with this configuration also including the %prod. qualified Auth0 production tenant configuration.

使用“OidcTestClient”测试此类配置时,需要使用 OAuth2 “password”或“client_credentials”授予来从 Auth0 开发租户获取令牌,我们将尝试“password”授予。确保在 Auth0 仪表板中注册的应用程序允许“password”授予:

Using OidcTestClient to test such configuration requires acquiring a token from the Auth0 dev tenant, using either OAuth2 password or client_credentials grant, we will try a password grant. Make sure the application registered in the Auth0 dashboard allows the password grant:

auth0 password grant

重要的是要澄清,我们不建议在生产中使用已弃用的 OAuth2 “password”令牌授予。但是,使用它有助于测试从真实开发 Auth0 租户获取的令牌来测试端点。

It is important to clarify that we do not recommend using the deprecated OAuth2 password token grant in production. However using it can help testing the endpoint with tokens acquired from the live dev Auth0 tenant.

OidcTestClient”应用于测试接受 Bearer 令牌的应用程序,该应用程序适用于本教程中开发的端点,因为它同时支持授权代码流程和 Bearer 令牌认证。如果您仅支持授权代码流程,则需要直接针对 Auth0 开发租户使用 OIDC WireMock 或“HtmlUnit”,在这种情况下,“HtmlUnit”测试代码必须符合 Auth0 要求用户输入其凭证的方式。如果您愿意,可以从文档中复制“HtmlUnit test fragment”并对其进行实验。

OidcTestClient should be used to test applications accepting bearer tokens which will work for the endpoint developed in this tutorial as it supports both authorization code flow and bearer token authentication. You would need to use OIDC WireMock or HtmlUnit directly against the Auth0 dev tenant if only the authorization code flow was supported - in the latter case HtmlUnit test code would have to be aligned with how Auth0 challenges users to enter their credentials. If you like, you can copy the HtmlUnit test fragment from the documentation and experiment with it.

在此期间,我们将继续使用“OidcTestClient”修复当前失败的测试。

In meantime we will now proceed with fixing the currently failing test using OidcTestClient.

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

First you must add the following dependency:

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 支持 - 如果需要,请查看文档了解如何将其用于测试)。

which provides a utility class io.quarkus.test.oidc.client.OidcTestClient which can be used in tests for acquiring access tokens (This dependency also offers an OIDC WireMock support - review the documentation how to use it for testing if you want).

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

Now update the test code like this:

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 is used to acquire an access token, using one of the registered user’s name and password, as well as the audience and scope parameters.

OidcTestClient”本身将找出“Auth0”令牌端点地址、客户端 ID 和机密。

OidcTestClient will itself find out the Auth0 token endpoint address, client id and secret.

再次按“r”,使测试通过:

Press r again and have the test passing:

auth0 test success

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

By the way, if you like, you can run the tests in Continuous mode directly from DevUI:

auth0 continuous testing

Production mode

在开发模式下开发、测试了通过 Auth0 保护的 Quarkus 端点。下一步是运行应用程序的生产模式。选择 JVM 和原生模式。

You have developed and tested the Quarkus endpoint secured with Auth0 in the development mode. The next step is to run your application in the production mode. Choose between JVM and native modes.

Run the Application in JVM mode

编译应用程序:

Compile the application:

Unresolved directive in security-oidc-auth0-tutorial.adoc - include::{includes}/devtools/build.adoc[]

运行应用程序:

Run the application:

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

打开浏览器,访问 [role="bare"][role="bare"]http://localhost:8080/hello,并在浏览器中显示该名称。

Open a browser, access [role="bare"]http://localhost:8080/hello and get the name displayed in the browser.

Run the application in native mode

可以编译这个相同演示的原生模式,无需进行任何修改。这意味着你不再需要在生产环境中安装 JVM 了。运行时技术包含在生成二进制文件中,优化为以所需的最低资源运行。

You can compile this same demo into native mode without needing any modifications. This implies that you no longer need to install a JVM on your production environment. The runtime technology is included in the produced binary and optimized to run with minimal resources required.

编译花费的时间稍微长一点,所以默认情况下会禁用此步骤。

Compilation takes a bit longer, so this step is disabled by default.

通过启用 native 配置文件重新构建你的应用程序:

Build your application again by enabling the native profile:

Unresolved directive in security-oidc-auth0-tutorial.adoc - include::{includes}/devtools/build-native.adoc[]

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

Next run the following binary directly:

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

打开浏览器,访问 [role="bare"][role="bare"]http://localhost:8080/hello,并在浏览器中显示该名称。

Open a browser, access [role="bare"]http://localhost:8080/hello and get the name displayed in the browser.

Troubleshooting

本教程中的步骤按教程描述的一样工作。如果你已经在完成身份验证,访问更新的 Quarkus 端点时可能需要清除浏览器 Cookie。你可能需要手动重启开发模式中的 Quarkus 应用程序,但这不常见。如果你需要帮助完成本教程,你可以联系 Quarkus 团队。

The steps described in this tutorial should work exactly as the tutorial describes. You might have to clear the browser cookies when accessing the updated Quarkus endpoint if you have already completed the authentication. You might need to restart the Quarkus application manually in dev mode but it is not expected. If you need help completing this tutorial, you can get in touch with the Quarkus team.

Summary

本教程演示了如何使用授权代码和持有者令牌身份验证流,通过 `quarkus-oidc`扩展和 Auth0 来保护 Quarkus 端点,这两种流都受同一端点代码支持。在不编写任何代码行的情况下,你增加了对自定义 Auth0 注销流的支持,并使用自定义 Auth0 名称空间限定声明启用了基于角色的访问控制。通过将 `@AccessToken`注释添加到微服务 REST 客户端,实现了从前端端点到微服务端点的令牌传播。微服务端点使用 `@PermissionsAllowed`注释激活了基于许可的访问控制。你使用 Quarkus 开发模式来更新代码和配置,而无需重启端点;并使用 OIDC Dev UI 来显示和测试 Auth0 令牌。你使用 Quarkus 的持续测试功能,利用对实时 Auth0 开发租户的集成测试来补充 OIDC Dev UI 测试。最后,你以 JVM 和原生模式运行应用程序。

This tutorial demonstrated how Quarkus endpoints can be secured with the quarkus-oidc extension and Auth0 using Authorization code and Bearer token authentication flows, with both flows being supported by the same endpoint code. Without writing a single line of code, you have added support for the custom Auth0 logout flow and enabled role-based access control with a custom Auth0 namespace qualified claim. Token propagation from the front-end endpoint to the microservice endpoint has been achieved by adding the @AccessToken annotation to the microservice REST client. Microservice endpoint activated the permission-based access control with the @PermissionsAllowed annotation. You used Quarkus dev mode to update the code and configuration without restarting the endpoint, and you also used the OIDC Dev UI to visualize and test Auth0 tokens. You used the continuous testing feature of Quarkus to complement OIDC Dev UI tests with integration tests against the live Auth0 development tenant. Finally, you have run the application in JVM and native modes.

享受!

Enjoy!