PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

32.20. OAuth 认证支持 #

32.20.1. 认证数据钩子
32.20.2. 调试与开发者选项

libpq 作为一个可选模块, 实现了对 OAuth v2 设备授权客户端流程的支持, 该流程记录于 RFC 8628。 有关如何启用把设备授权作为内建流程的支持, 请参见 安装文档。

当支持被启用且可选模块已安装时, 如果服务器在身份验证期间 请求一个 bearer 令牌, libpq 默认会使用内建流程。 即使运行客户端应用程序的系统没有可用的网页浏览器 (例如通过 SSH 运行客户端), 也可以利用这个流程。

默认情况下, 内建流程会打印一个要访问的 URL 以及要在那里输入的用户码:

$ psql 'dbname=postgres oauth_issuer=https://example.com oauth_client_id=...'
Visit https://example.com/device and enter the code: ABCD-EFGH

(这个提示可以 定制。) 然后用户将登录到他们的 OAuth 提供方, 后者会询问是否允许 libpq 和服务器代表他们执行操作。 在继续之前, 仔细审查所显示的 URL 和权限, 确保它们符合预期, 始终是一个好主意。 不应当向不受信任的第三方授予权限。

客户端应用程序可以实现自己的流程, 以定制与应用程序的交互和集成。 有关如何向 libpq 添加自定义流程的更多信息, 请参见 Section 32.20.1。

要使一个 OAuth 客户端流程可用, 连接字符串必须至少包含 oauth_issuer 和 oauth_client_id。 (这些设置由你的组织的 OAuth 提供方决定。) 内建流程还要求 OAuth 授权服务器发布一个设备授权端点。

Note

Windows 目前不支持内建的设备授权流程。 仍然可以实现自定义的客户端流程。

32.20.1. 认证数据钩子 #

客户端可以使用以下钩子 API 来修改或替换 OAuth 流程的行为:

PQsetAuthDataHook #

设置 PGauthDataHook, 覆盖 libpq 对其 OAuth 客户端流程的一个或多个方面的处理。

void PQsetAuthDataHook(PQauthDataHook_type hook);

如果 hook 为 NULL, 则会重新安装默认处理器。 否则, 应用程序传递一个指向具有如下签名形式的回调函数的指针:

int hook_fn(PGauthData type, PGconn *conn, void *data);

当需要应用程序执行某个动作时, libpq 将调用它。 type 描述被做出的请求, conn 是正在进行身份验证的连接句柄, 而 data 指向请求特有的元数据。 这个指针的内容由 type 决定; 受支持的列表请参见 Section 32.20.1.1。

钩子可以被串联起来, 以允许协作和/或后备行为。 一般而言, 钩子实现应当检查传入的 type (以及可能的请求元数据和/或正在使用的特定 conn 的设置), 来决定是否处理某一块 authdata。 如果不处理, 它应当委托给链中的上一个钩子 (可通过 PQgetAuthDataHook 检索)。

通过返回一个大于零的整数来指示成功。 返回一个负整数表示一种错误情况, 并放弃连接尝试。 (零值被保留给默认实现。)

PQgetAuthDataHook #

检索 PGauthDataHook 的当前值。

PQauthDataHook_type PQgetAuthDataHook(void);

在初始化时 (在第一次调用 PQsetAuthDataHook 之前), 这个函数会返回 PQdefaultAuthDataHook。

32.20.1.1. 钩子类型 #

以下 PGauthData 类型及其相应的 data 结构被定义:

PQAUTHDATA_PROMPT_OAUTH_DEVICE #

在内建设备授权客户端流程期间取代默认的用户提示。 data 指向 PGpromptOAuthDevice 的一个实例:

typedef struct _PGpromptOAuthDevice
{
    const char *verification_uri;   /* verification URI to visit */
    const char *user_code;          /* user code to enter */
    const char *verification_uri_complete;  /* optional combination of URI and
                                             * code, or NULL */
    int         expires_in;         /* seconds until user code expires */
} PGpromptOAuthDevice;

可以 被包含在 libpq 中的 OAuth 设备授权流程要求最终用户用浏览器访问一个 URL, 然后输入一个允许 libpq 代表他们连接到服务器的码。 默认的提示只是把 verification_uri 和 user_code 打印到标准错误。 替换实现可以用任何偏好的方式显示这些信息, 例如用 GUI。

这个回调只在内建设备授权流程期间被调用。 如果应用程序安装了一个 自定义 OAuth 流程, 或者 libpq 构建时没有带内建流程的支持, 这个 authdata 类型将不会被使用。

如果提供了一个非 NULL 的 verification_uri_complete, 它可以选择性地用于非文本的验证 (例如通过显示一个二维码)。 在这种情况下, 仍然应当向最终用户显示该 URL 和用户码, 因为该码会由提供方手动确认, 而这个 URL 让用户在无法使用非文本方法的情况下也能继续。 更多信息请参见 RFC 8628 的 3.3.1 节。

PQAUTHDATA_OAUTH_BEARER_TOKEN #

添加一个流程的自定义实现, 如果该流程 已安装, 则替换内建的流程。 如果当前用户/颁发者/作用域组合的 Bearer 令牌可以在不阻塞的情况下获得, 钩子应当直接返回它, 否则应设置一个异步回调来检索它。

data 指向 PGoauthBearerRequest 的一个实例, 它应当由实现来填充:

typedef struct PGoauthBearerRequest
{
    /* Hook inputs (constant across all calls) */
    const char *openid_configuration; /* OIDC discovery URL */
    const char *scope;                /* required scope(s), or NULL */

    /* Hook outputs */

    /*
     * Callback implementing a custom asynchronous OAuth flow. The signature is
     * platform-dependent: PQ_SOCKTYPE is SOCKET on Windows, and int everywhere
     * else.
     */
    PostgresPollingStatusType (*async) (PGconn *conn,
                                        struct PGoauthBearerRequest *request,
                                        PQ_SOCKTYPE *altsock);

    /* Callback to clean up custom allocations. */
    void        (*cleanup) (PGconn *conn, struct PGoauthBearerRequest *request);

    char       *token;   /* acquired Bearer token */
    void       *user;    /* hook-defined allocated data */
} PGoauthBearerRequest;

libpq 向钩子提供两块信息: openid_configuration 包含一份描述授权服务器所支持流程的 OAuth 发现文档的 URL, 而 scope 包含访问服务器所需的 OAuth 作用域的一个 (可能为空的) 以空格分隔的列表。 其中任何一个或两者都可以是 NULL, 表示无法发现该信息。 (在这种情况下, 实现也许能够使用某些其他预先配置的知识来确定这些要求, 或者它们可能选择失败。)

钩子的最终输出是 token, 它必须指向一个有效的 Bearer 令牌供连接使用。 (这个令牌应当由 oauth_issuer 颁发, 并且持有所需的作用域, 否则连接将被服务器的验证器模块拒绝。) 被分配的令牌字符串必须在 libpq 完成连接之前保持有效; 钩子应当设置一个 cleanup 回调, 当 libpq 不再需要它时该回调会被调用。

如果一个实现无法在钩子的初始调用期间立即产生一个 token, 它应当设置 async 回调来处理与授权服务器的非阻塞通信。 [11] 这个回调会在从钩子返回之后被立即调用, 以开始该流程。 当回调在不阻塞的情况下无法取得进一步进展时, 它应当在把 *altsock 设置为将来可以再次取得进展时会被标记为可读/可写的文件描述符 之后, 返回 PGRES_POLLING_READING 或 PGRES_POLLING_WRITING。 (这个描述符随后会通过 PQsocket() 提供给顶层轮询循环。) 当流程完成时, 在设置好 token 之后返回 PGRES_POLLING_OK; 若要指示失败, 则返回 PGRES_POLLING_FAILED。

实现可能希望在多次调用 async 和 cleanup 回调之间存储一些额外的数据用于簿记。 为此提供了 user 指针; libpq 不会触碰它的内容, 应用程序可以在方便时使用它。 (记住在令牌清理期间释放任何分配。)

32.20.2. 调试与开发者选项 #

可以通过设置环境变量 PGOAUTHDEBUG=UNSAFE 来启用一种 “危险的调试模式”。 提供这个功能只是为了便于本地开发和测试。 它会做一些你不希望生产系统去做的事情:

  • 允许在 OAuth 提供方交换期间使用未加密的 HTTP

  • 允许用 PGOAUTHCAFILE 环境变量完全替换系统受信任的 CA 列表

  • 在 OAuth 流程期间把 HTTP 流量 (包含若干关键秘密) 打印到标准错误

  • 允许使用零秒的重试间隔, 这可能导致客户端陷入忙循环并无谓地消耗 CPU

Warning

不要与第三方分享 OAuth 流程流量的输出。 它包含可以被用来攻击你的客户端和服务器的秘密。



[11] 在 PQAUTHDATA_OAUTH_BEARER_TOKEN 钩子回调期间执行阻塞操作, 会干扰诸如 PQconnectPoll 之类的非阻塞连接 API, 并阻止并发连接取得进展。 只使用同步连接原语 (例如 PQconnectdb) 的应用程序, 可以改为在钩子期间同步地检索一个令牌, 而不实现 async 回调, 但它们必然会被限制为一次一条连接。