使用 OIDC 代理将 OIDC 服务端点与自定义 GPT 集成

简介

Quarkus OIDC 代理是一个新的 Quarkiverse 扩展,可以帮助将 OIDC 服务端点与外部单页应用程序 (SPA) 集成。

SPA 在浏览器中运行,并使用 OIDC 授权码流程,但不依赖 Quarkus,来验证当前用户的身份,并代表经过身份验证的用户使用访问令牌访问 Quarkus OIDC 服务端点。以下是一个简单的流程图,展示了这个过程的工作原理,从 OIDC Bearer token 指南复制到这篇文章中,方便您参考

SPA and Quarkus Service

如上图所示,OIDC 提供商验证当前用户的身份。 SPA 接收 ID、访问令牌,以及可能的刷新令牌,作为授权码流程的结果,并使用访问令牌来访问 Quarkus OIDC 服务端点。

SPA 与 OIDC 提供商交互。因此,它必须知道提供商的连接详细信息,包括注册的 OIDC 应用程序的客户端 ID 和成功完成授权码流程所需的其他 OIDC 特定详细信息。您还必须在注册的 OIDC 应用程序中提供一个*回调* URL,但这可能并不总是可以接受的。

Quarkus OIDC 代理扩展简化了整个设置。它充当 SPA 和 Quarkus OIDC 服务端点之间的代理,并委托给真正的 OIDC 提供商来支持授权码流程。它允许将 OIDC 服务端点与 SPA 集成,而无需将内部 OIDC 连接详细信息暴露给此 SPA,因此,将所有必需的详细信息发送到用户浏览器。

OIDC 代理的另一个用例是支持多个 Quarkus OIDC web-app 端点,以在使用相同的 OIDC 代理配置访问 OIDC 服务端点之前验证用户身份。

这个 OIDC 代理实际上是如何工作的?我们稍后会介绍,但首先,让我们谈谈自定义 GPT 操作。

自定义 GPT 操作

ChatGPT 引入了 操作,可用于创建自定义 GPT。例如,您可以创建一个自定义 GPT,通过将其连接到您的 API 端点来增强您的 ChatGPT 对话体验。

将自定义 GPT 与您的 API 连接时面临的挑战之一是身份验证,即如何对您的自定义 GPT 进行 身份验证,以允许其访问 API。OAuth 选项是当您需要用户特定的权限来访问 API 时的最佳选择,而这正是 Quarkus OIDC 代理将帮助您实现的目标,而无需暴露所有 OIDC/OAuth2 连接详细信息。

请注意,目前,自定义 GPT 操作只能通过 ChatGPT Plus 和企业版订阅创建。

Quarkus 健身顾问

好的,让我们更精确地看看它是如何工作的。为了说明这一点,我们将创建一个 Quarkus 健身顾问,这是一个自定义 GPT,用于分析记录在 Strava 和其他跟踪体育锻炼的社交提供商中的活动。

我们将通过注册一个 Strava API 应用程序,创建一个 Strava OAuth2 服务端点,使用 OIDC 代理对其进行代理,使用 NGrok 提供 HTTPS 隧道,最后,创建一个自定义 GPT,该 GPT 使用 OIDC 代理对 GPT 用户进行 Strava 身份验证,并使用访问令牌访问 Quarkus Strava OIDC 服务端点来分析记录的活动。

步骤 1 - Strava 应用程序注册

我们将首先在 Strava 中注册一个新的 Quarkus 健身顾问应用程序

Strava Application Registration

请注意,Authorization Callback Domain 指向您的免费 NGrok(或生产环境中的真实)域,该域表示 OIDC 代理可用的域,可能也是您的 Quarkus 微服务托管的域。这是 Quarkus OIDC 代理的一个重要功能,因为它允许 OIDC 提供商管理员指向受信任的域,而不是第三方域。

另请注意,只有域才能被接受作为回调选项,这对于 Strava 应用程序注册过程来说是特有的。通常建议仅允许特定的绝对回调 URL,并且 Quarkus Strava OAuth2 集成强制仅允许单个回调 URL。

完成应用程序注册后,记下生成的客户端 ID 和密钥。我们稍后会用到它们。

步骤 2 - Quarkus Strava 服务

Quarkus OIDC 集成了 Strava OAuth2 提供商,并封装了所有 Strava OAuth2 特定的详细信息。您只需要在配置文件中添加一行:quarkus.oidc.provider=strava

Strava 提供商主要符合 OAuth2 标准。但是,它使用 HTTP 查询参数来完成授权码流程 POST 令牌请求,而使用表单参数是一种常见的选择。当在初始重定向到 Strava 期间请求多个作用域时,它还使用逗号 , 分隔符,而空格“ ”是典型的分隔字符。

Quarkus OIDC 代理可以处理它,因为它依赖于 Quarkus OIDC 知识。应该注意的是,自定义 GPT 不支持具有其内置 OAuth 身份验证选项的这些选项。幸运的是,代理将为我们处理这个问题。

好了,是时候编写该应用程序了。首先,您需要将一些 Maven 依赖项添加到您的项目中

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-client-oidc-token-propagation</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>

您需要 Quarkus 3.9.0+

接下来,我们创建 OIDC 配置

quarkus.oidc.provider=strava
quarkus.oidc.application-type=service

quarkus.oidc.client-id=${strava-client-id}
quarkus.oidc.credentials.secret=${strava-client-secret}
quarkus.oidc.authentication.extra-params.scope=profile:read_all,activity:read_all

默认情况下,quarkus.oidc.provider=strava 启用了一个 Quarkus OIDC web-app 应用程序类型,该类型可以支持授权码流程。但是我们的端点充当 Quarkus OIDC service,它接受来自自定义 GPT 的 bearer 访问令牌。因此,我们将应用程序类型覆盖为 service。相反,OIDC 代理将管理授权码流程。

请注意,额外的 Strava API 作用域是如何添加到 quarkus.oidc.provider=strava 已经启用的作用域中的,而不是覆盖它们。有关更多信息,请参阅 提供程序作用域

客户端 ID、密钥和额外的作用域实际上不是 OIDC 服务端点所必需的。设置这些属性是为了支持 OIDC 代理,OIDC 代理需要知道如何正确处理来自外部 SPA 的 OIDC 授权码流程请求。

我们还添加以下属性

quarkus.rest-client.strava-client.url=https://www.strava.com/api/v3

quarkus.smallrye-openapi.operation-id-strategy=method
quarkus.smallrye-openapi.auto-add-security=false
quarkus.smallrye-openapi.servers=https://<your-free-ngrok-domain>.app

首先,我们配置 REST 客户端以指向 Strava API 基本端点。然后,我们稍微调整一下 Quarkus 生成 OpenAPI 文档的方式,使其可以被自定义 GPT 配置过程接受。

现在我们已经完成了配置,我们需要定义调用 Strava API 的 REST 客户端接口。它自动 传播 Strava 访问令牌以访问用户特定的 Strava 数据

package org.acme.security.openid.connect.plugin;

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

import io.quarkus.oidc.token.propagation.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@RegisterRestClient(configKey="strava-client")
@AccessToken
@Path("/")
public interface StravaClient {

	@GET
	@Path("athlete/activities")
	@Produces(MediaType.APPLICATION_JSON)
	String athleteActivities();

	@GET
	@Path("activities/{id}")
	@Produces(MediaType.APPLICATION_JSON)
	String athleteActivity(@PathParam("id") long activityId);

	@GET
	@Path("athletes/{id}/stats")
	@Produces(MediaType.APPLICATION_JSON)
	String athleteStats(@PathParam("id") long athleteId);

	// Etc for other Strava API
}

现在,让我们实现我们应用程序的主要端点,它公开与 Strava 相同的 API。它接受来自自定义 GPT 的访问令牌,并使用 REST 客户端将它们转发到 Strava

package org.acme.security.openid.connect.plugin;

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

import io.quarkus.logging.Log;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;

@Path("/athlete")
@Authenticated (1)
public class FitnessAdviserService {

    @Inject
    UserInfo athlete;

    @Inject
    @RestClient
    StravaClient stravaClient;

    @GET
    @Produces("application/json")
    public String athlete() {
        Log.info("Fitness adviser: athlete");
        return athlete.getJsonObject().toString();
    }

    @GET
    @Produces("application/json")
    @Path("/activities")
    public String activities() {
        Log.info("Fitness adviser: activities");
        return stravaClient.athleteActivities();
    }

    @GET
    @Produces("application/json")
    @Path("/activity/{id}")
    public String activity(@PathParam("id") long activityId) {
        Log.infof("Fitness adviser: activity %d", activityId);
        return stravaClient.athleteActivity(activityId);
    }

    @GET
    @Produces("application/json")
    @Path("/stats")
    public String stats() {
        Log.info("Fitness adviser: stats");
        return stravaClient.athleteStats(athlete.getLong("id"));
    }

    // Etc for other Strava API
}
1 访问 FitnessAdviserService 端点需要经过验证的访问令牌。

请注意,为了接受二进制 Strava 访问令牌,此端点通过在令牌身份验证过程中从 Strava 请求 UserInfo 来间接验证它们,这由 quarkus.oidc.provider=strava 声明启用。在这种情况下,UserInfo 代表 Strava 运动员的个人资料,在它发出出站 REST 客户端调用时,该个人资料已经可用于该端点。例如,FitnessAdviserService 端点将 UserInfo 运动员 id 属性传递给 StravaClient,以请求当前经过身份验证的运动员的统计信息。

如果它是 Keycloak 或 Auth0 等提供商颁发的访问令牌,那么它将在本地使用 Keycloak 或 Auth0 公共验证密钥进行验证,并 直接作为 JsonWebToken 注入

步骤 3 - OIDC 代理

最后,让我们谈谈 OIDC 代理。我们有我们的 OIDC Strava 服务端点调用 Stava API。现在是时候使用 OIDC 代理和授权码流程身份验证过程使其可以访问外部 SPA 了。

我们所需要做的就是添加以下依赖项

<dependency>
    <groupId>io.quarkiverse.oidc-proxy</groupId>
    <artifactId>quarkus-oidc-proxy</artifactId>
    <version>0.1.1</version>
</dependency>

它公开了 OIDC /q/oidc/authorize 端点以接受自定义 GPT 身份验证重定向,以及 /q/oidc/token 端点以交换授权码和令牌。

现在让我们更新应用程序配置以设置我们的代理

quarkus.oidc.authentication.redirect-path=/callback (1)
quarkus.oidc-proxy.external-redirect-uri=https://chat.openai.com/aip/g-2faf163d359505ecb63596f17baa3dfe53ea3cb9/oauth/callback (2)
quarkus.oidc.authentication.force-redirect-https-scheme=true (3)
quarkus.oidc-proxy.root-path=/oidc
quarkus.oidc-proxy.external-client-id=external-client-id (4)
quarkus.oidc-proxy.external-client-secret=external-client-secret (4)
1 请求 OIDC 代理创建一个端点,该端点将支持来自实际 OIDC 提供商的重定向。正如在 步骤 1 - Strava 应用程序注册 部分中解释的那样,在 OIDC 提供商的仪表板中注册已知的、受信任的域 URL 可能很有帮助。默认情况下,此属性已设置为具有 Strava 提供商的 /strava,以限制可能的回调 URL,如 步骤 1 - Strava 应用程序注册 部分中所述;此示例显示了如何对其进行自定义。您不必使用 quarkus.oidc.authentication.redirect-path,但请注意此属性。
2 OIDC 代理在接受 quarkus.oidc.authentication.redirect-path 回调后将用户重定向到的外部回调 URL。
3 NGrok 将在调用基于 HTTP 的端点之前终止 HTTPS 连接,因此原始 HTTPS 方案必须用于构建外部重定向 URL。
4 设置将与第三方 SPA 集成期间使用的外部客户端 ID 和密钥。如果您不想将真实的客户端 ID 和密钥暴露给 SPA,请使用这些属性。

我们完成了!让我们运行它

mvn clean install
java target/quarkus-app/quarkus-run.jar

如果您更喜欢使用 Quarkus dev 模式,那么,为了允许从外部 SPA 重定向到 OIDC 代理授权端点,您必须禁用 DevUI CORS 控制

%dev.quarkus.dev-ui.cors.enabled=false

步骤 4 - NGrok

第三方 SPA 最有可能需要基于 HTTPS 的 OIDC 提供商端点,因此,要使 OIDC 代理端点在 localhost 上使用 HTTPS 方案,使用 NGrok 是最简单的方法。

请注意

ngrok http --domain <your-free-ngrok-domain> 8080

不会阻止 NGrok 警告该网站是免费从 NGrok 提供的,这会混淆自定义 GPT 的 OAuth 授权码流程支持。在这种情况下,您应该启用 HTTP 隧道,如此 Stack Overflow 帖子中所述,例如

ngrok tunnel --label edge=<ngrok-tunnel-id> https://:8080

步骤 5 - 创建自定义 GPT

正如在 自定义 GPT 操作 部分中指出的那样,自定义 GPT 操作只能通过 ChatGPT Plus 和企业版订阅创建。有关使用 OIDC 代理进行实验的其他建议,请参阅下面的 后续步骤 部分。

登录到您的 ChatGPT 帐户,并在 My GPTs 中选择 Create

Create custom GPT

将其命名为 Quarkus 健身顾问 并提供其描述

Custom GPT description

接下来,选择一个 OAuth 身份验证选项

Custom GPT OAuth option

并设置 OAuth2 授权和令牌端点地址,请记住您的免费 步骤 4 - NGrok 域名,并且您已在 步骤 3 - OIDC 代理 部分中将 OIDC 代理根地址设置为 /oidc

custom GPT OAuth configuration

将客户端 ID 和密钥设置为您在 步骤 3 - OIDC 代理 部分中配置的外部客户端 ID 和外部客户端密钥属性。

现在您可以看到,此自定义 GPT 的 OAuth 设置已完成,而无需共享 Quarkus OIDC 服务端点中与 Strava 提供商配置相关的任何详细信息。您也不需要设置作用域,OIDC 代理从 Quarkus OIDC 端点配置中了解它们。

接下来,通过选择 Import from URL 选项并输入 http://<your-free-ngrok-domain>/q/openapi 来导入 OpenAPI 模式

Custom GPT Import OpenAPI

此时,您已准备好保存此 GPT 并开始使用它。

请注意此 GPT 的回调,这是您在 步骤 3 - OIDC 代理 部分中配置的外部回调 URI 值

Custom GPT callback

您必须决定是否要共享此 GPT。很可能,在测试它之后,您会更喜欢与您的团队共享以进行测试,并最终与您的客户共享。

在这种情况下,您首先要做的是向 ChatGPT 询问典型的隐私政策文本,如果您还没有,并在根据需要进行修改后,将其保存在例如 privacy.txt 文档中的 src/main/resources/META-INF/resources/步骤 2 - Quarkus Strava 服务 应用程序中,并在 Privacy Policy 配置字段中链接到它,如 http://<your-free-ngrok-domain>/privacy.txt。最后,使用 Anyone with a link 选项发布它。

Quarkus 健身顾问 现在已准备就绪

Custom GPT is ready

步骤 6 - 使用自定义 GPT

让我们首先要求 Quarkus 健身顾问 检查运动员个人资料

Custom GPT Sign In

当您向 GPT 提出第一个问题时,它将尝试使用 OAuth 身份验证选项登录您。选择 Sign in 选项,您将被重定向到 OIDC 代理,该代理将反过来重定向到 Strava 进行身份验证

Strava Login

输入您的 Strava 名称和密码并继续。仅当使用授权码流程获取的访问令牌已过期时,您才会被要求再次进行身份验证。

成功进行身份验证后,系统会要求您授权在 步骤 1 - Strava 应用程序注册 部分中注册的 Quarkus 健身顾问 应用程序

Strava Authorization

步骤 2 - Quarkus Strava 服务 配置的 Strava API 作用域会影响您将被要求授权的内容。

现在,您将被重定向到具有授权码的自定义 GPT,该授权码将使用 OIDC 代理交换为访问令牌和刷新令牌。 GPT 现在将想与 Quarkus API 通信并要求您批准

Custom GPT Approve Action

批准它,Quarkus 健身顾问 将提供第一个答案

Custom GPT Profile Overview

它还提供有关您的自行车、跑鞋的信息,并提供一些初步建议。您现在可以询问有关平衡骑自行车和游泳、跑步等方面的建议。

接下来,让我们询问有关最新活动的信息

Custom GPT Latest Activity

要求它更具体地说明最新活动并提供一些建议。 Quarkus 健身顾问回应

Custom GPT Activity Recommendation

并总结出一个良好的建议,即要好好休息和恢复。

最后,让我们要求它再次检查个人资料并提供更多建议。 Quarkus 健身顾问 很高兴为您提供帮助,并提供了我个人八个个性化建议,我只会显示响应的开头

Custom GPT More Profile Recommendations

以及结尾

Custom GPT Enjoy the Ride

我们将在本文后面的部分回到此建议。

让我们以说 谢谢 结束

Custom GPT Final Message

后续步骤

到目前为止,Quarkus 健身顾问 已经帮助分析了经过身份验证的运动员的个人资料和活动。请通过检查路线、区域和 Strava API 支持的其他健身数据来进一步尝试创建更高级的 Quarkus 健身顾问 版本。

借助 Quarkus 中支持的任何其他众所周知的社交提供商创建一个新的自定义 GPT。

另请注意,您的 Quarkus OIDC 服务端点不必传播访问令牌。例如,如果您使用 Keycloak 或 Auth0,则这些符合 OIDC 标准的提供商颁发的 JWT 格式的访问令牌可以由 Quarkus OIDC 验证,以提供基于角色或基于权限的自定义 GPT 请求的访问控制,服务 端点返回数据库中的数据等。

还鼓励您密切关注 Quarkus LangChain4j 项目,该项目提供了 Quarkus 和 LangChain4j 库之间的顶级集成。

创建一个自定义 GPT 怎么样,该 GPT 将使用 OIDC 代理来验证自定义 GPT 用户到 Keycloak 或 Auth0 或 Azure 的身份,并访问由 Quarkus LangChain4j 驱动的 Quarkus OIDC 服务端点?请尝试一下!

如果您没有 ChatGPT Plus 或企业版订阅怎么办?

没问题,OIDC 代理将与任何实现授权码流程并希望具有 OIDC 提供商中立集成的 SPA 配合使用,请使用此类 SPA 测试 OIDC 代理。

或者,尝试配置 Quarkus OIDC web-app 应用程序,使用 OIDC 代理在调用 OIDC 服务端点之前验证用户身份。例如,假设三个不同的 Quarkus OIDC web-app 应用程序使用相同的 Keycloak 领域来通过授权码流程验证用户身份,并将访问令牌传播到相同的 OIDC service 应用程序。现在,您可以尝试将 OIDC 代理添加到 OIDC 服务端点并配置 OIDC web-app 应用程序以使用 OIDC 代理,而不是在所有 OIDC web-app 应用程序中设置 Keycloak 特定详细信息。

安全注意事项

您已经在 步骤 3 - OIDC 代理 部分中看到了几个 OIDC 代理安全功能。

常规 OIDC 代理功能是隐藏来自 SPA 的所有真实 OIDC 提供商特定详细信息,包括所有 OAuth2 或 OIDC 提供商特定详细信息,以及在身份验证重定向到提供商期间请求的额外作用域。

OIDC 代理允许您在 OIDC 提供商中注册的允许的回调 URI 中设置受信任的域,并启用真实 OIDC 提供商和外部 SPA 之间的回调桥。

您可以从外部 SPA 隐藏 OIDC 代理必须使用的真实客户端 ID 和客户端密钥。

您可以请求 OIDC 代理不要从授权码令牌交换返回刷新令牌和/或 ID 令牌到 SPA。

刷新令牌是最强大的令牌,通常具有很长的生命周期。如果 SPA 泄漏它,以及客户端 ID 和密钥,攻击者可以刷新和使用访问令牌来访问 API 很长时间。因此,如果您担心 SPA(例如自定义 GPT)可能会泄漏此信息,请将 quarkus.oidc-proxy.allow-refresh-token=false 添加到配置中,以请求 OIDC 代理从授权码流程响应中删除刷新令牌值,该响应即将返回到 GPT。这不会阻止给定的自定义 GPT 使用 Quarkus API,只会要求此 GPT 在访问令牌过期时重新验证用户身份,而不是刷新它。

ID 令牌包含有关当前经过身份验证的用户的信息。如果您知道 SPA 不需要 ID 令牌,例如仅使用访问令牌和刷新令牌的自定义 GPT,那么建议使用 quarkus.oidc-proxy.allow-id-token=false 来阻止返回它。

结论

在这篇文章中,我们研究了 Quarkus OIDC 代理 如何帮助将 OIDC 服务端点与 SPA 集成,而无需暴露内部 OIDC 连接详细信息。我们构建了 Quarkus 健身顾问,这是一个 自定义 GPT,它使用 OIDC 代理通过 Strava 验证用户身份,并通过从 Quarkus OIDC Strava 服务读取经过身份验证的特定于用户的数据来提供健身建议。

享受 Quarkus,正如 Quarkus 健身顾问 建议的那样,享受旅程!