since-v38.diff.txt

text/plain

Filename: since-v38.diff.txt
Type: text/plain
Part: 0
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER
1:  785add80157 ! 1:  3dc642d68c8 Add OAUTHBEARER SASL mechanism
    @@ doc/src/sgml/client-auth.sgml: host ... radius radiusservers="server1,server2" r
     +
     +    OAuth client support has to be enabled when <productname>PostgreSQL</productname>
     +    is built, see <xref linkend="installation"/> for more information.
    ++   </para>
     +
    -+    <itemizedlist>
    -+     <listitem>
    -+      <para>
    -+       Resource owner: The user or system who owns protected resources and can
    -+       grant access to them.
    -+      </para>
    -+     </listitem>
    -+     <listitem>
    -+      <para>
    -+       Client: The system which accesses the protected resources using access
    -+       tokens.  Applications using libpq are the clients in connecting to a
    -+       <productname>PostgreSQL</productname> cluster.
    -+      </para>
    -+     </listitem>
    -+     <listitem>
    -+      <para>
    -+       Authorization server: The system which receives requests from, and
    -+       issues access tokens to, the client after the authenticated resource
    -+       owner has given approval.
    -+      </para>
    -+     </listitem>
    ++   <para>
    ++    This documentation uses the following terminology when discussing the OAuth
    ++    ecosystem:
     +
    -+     <listitem>
    -+      <para>
    -+       Resource server: The system which hosts the protected resources which are
    -+       accessed by the client. The <productname>PostgreSQL</productname> cluster
    -+       being connected to is the resource server.
    -+      </para>
    -+     </listitem>
    ++    <variablelist>
     +
    -+    </itemizedlist>
    ++     <varlistentry>
    ++      <term>Resource Owner (or End User)</term>
    ++      <listitem>
    ++       <para>
    ++        The user or system who owns protected resources and can grant access to
    ++        them. This documentation also uses the term <emphasis>end user</emphasis>
    ++        when the resource owner is a person. When you use
    ++        <application>psql</application> to connect to the database using OAuth,
    ++        you are the resource owner/end user.
    ++       </para>
    ++      </listitem>
    ++     </varlistentry>
    ++
    ++     <varlistentry>
    ++      <term>Client</term>
    ++      <listitem>
    ++       <para>
    ++        The system which accesses the protected resources using access
    ++        tokens. Applications using libpq, such as <application>psql</application>,
    ++        are the OAuth clients when connecting to a
    ++        <productname>PostgreSQL</productname> cluster.
    ++       </para>
    ++      </listitem>
    ++     </varlistentry>
    ++
    ++     <varlistentry>
    ++      <term>Resource Server</term>
    ++      <listitem>
    ++       <para>
    ++        The system which hosts the protected resources which are
    ++        accessed by the client. The <productname>PostgreSQL</productname>
    ++        cluster being connected to is the resource server.
    ++       </para>
    ++      </listitem>
    ++     </varlistentry>
    ++
    ++     <varlistentry>
    ++      <term>Provider</term>
    ++      <listitem>
    ++       <para>
    ++        The organization, product vendor, or other entity which develops and/or
    ++        administers the OAuth servers and clients for a given application.
    ++        Different providers typically choose different implementation details
    ++        for their OAuth systems; a client of one provider is not generally
    ++        guaranteed to have access to the servers of another.
    ++       </para>
    ++       <para>
    ++        This use of the term "provider" is not standard, but it seems to be in
    ++        wide use colloquially. (It should not be confused with OpenID's similar
    ++        term "Identity Provider". While the implementation of OAuth in
    ++        <productname>PostgreSQL</productname> is intended to be interoperable
    ++        and compatible with OpenID Connect/OIDC, it is not itself an OIDC client
    ++        and does not require its use.)
    ++       </para>
    ++      </listitem>
    ++     </varlistentry>
    ++
    ++     <varlistentry>
    ++      <term>Authorization Server</term>
    ++      <listitem>
    ++       <para>
    ++        The system which receives requests from, and issues access tokens to,
    ++        the client after the authenticated resource owner has given approval.
    ++        <productname>PostgreSQL</productname> does not provide an authorization
    ++        server; it's obtained from the OAuth provider.
    ++       </para>
    ++      </listitem>
    ++     </varlistentry>
    ++
    ++     <varlistentry>
    ++      <term>Issuer</term>
    ++      <listitem>
    ++       <para>
    ++        An identifier for an authorization server, printed as an
    ++        <literal>https://</literal> URL, which provides a trusted "namespace"
    ++        for OAuth clients and applications. The issuer identifier allows a
    ++        single authorization server to talk to the clients of mutually
    ++        untrusting entities, as long as they maintain separate issuers.
    ++       </para>
    ++      </listitem>
    ++     </varlistentry>
    ++
    ++    </variablelist>
    ++
    ++    <note>
    ++     <para>
    ++      For small deployments, there may not be a meaningful distinction between
    ++      the "provider", "authorization server", and "issuer". However, for more
    ++      complicated setups, there may be a one-to-many (or many-to-many)
    ++      relationship: a provider may rent out multiple issuer identifiers to
    ++      separate tenants, then provide multiple authorization servers, possibly
    ++      with different supported feature sets, to interact with their clients.
    ++     </para>
    ++    </note>
     +   </para>
     +
     +   <para>
    @@ doc/src/sgml/client-auth.sgml: host ... radius radiusservers="server1,server2" r
     +    <ulink url="https://datatracker.ietf.org/doc/html/rfc6750">RFC 6750</ulink>,
     +    which are a type of access token used with OAuth 2.0 where the token is an
     +    opaque string.  The format of the access token is implementation specific
    -+    and is chosen by each authentication server.
    ++    and is chosen by each authorization server.
     +   </para>
     +
     +   <para>
    @@ doc/src/sgml/client-auth.sgml: host ... radius radiusservers="server1,server2" r
     +     </varlistentry>
     +
     +     <varlistentry>
    -+      <term><literal>trust_validator_authz</literal></term>
    ++      <term id="auth-oauth-delegate-ident-mapping" xreflabel="delegate_ident_mapping">
    ++       <literal>delegate_ident_mapping</literal>
    ++      </term>
     +      <listitem>
     +       <para>
     +        An advanced option which is not intended for common use.
     +       </para>
     +       <para>
    -+        When set to <literal>1</literal>, standard user mapping is skipped, and
    -+        the OAuth validator takes full responsibility for mapping end user
    -+        identities to database roles.  If the validator authorizes the token,
    -+        the server trusts that the user is allowed to connect under the
    -+        requested role, and the connection is allowed to proceed regardless of
    -+        the authentication status of the user.
    ++        When set to <literal>1</literal>, standard user mapping with
    ++        <filename>pg_ident.conf</filename> is skipped, and the OAuth validator
    ++        takes full responsibility for mapping end user identities to database
    ++        roles.  If the validator authorizes the token, the server trusts that
    ++        the user is allowed to connect under the requested role, and the
    ++        connection is allowed to proceed regardless of the authentication
    ++        status of the user.
     +       </para>
     +       <para>
     +        This parameter is incompatible with <literal>map</literal>.
     +       </para>
     +       <warning>
     +        <para>
    -+         <literal>trust_validator_authz</literal> provides additional
    ++         <literal>delegate_ident_mapping</literal> provides additional
     +         flexibility in the design of the authentication system, but it also
     +         requires careful implementation of the OAuth validator, which must
     +         determine whether the provided token carries sufficient end-user
    @@ doc/src/sgml/config.sgml: include_dir 'conf.d'
     +        <link linkend="auth-oauth"><literal>oauth</literal> HBA entries</link>
     +        must explicitly set a <literal>validator</literal> chosen from this
     +        list. If set to an empty string (the default), OAuth connections will be
    -+        refused. For more information on implementing OAuth validators see
    -+        <xref linkend="oauth-validators" />. This parameter can only be set in
    -+        the <filename>postgresql.conf</filename> file.
    ++        refused. This parameter can only be set in the
    ++        <filename>postgresql.conf</filename> file.
    ++       </para>
    ++       <para>
    ++        Validator modules must be implemented/obtained separately;
    ++        <productname>PostgreSQL</productname> does not ship with any default
    ++        implementations. For more information on implementing OAuth validators,
    ++        see <xref linkend="oauth-validators" />.
     +       </para>
     +      </listitem>
     +     </varlistentry>
    @@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
     +        setting in <link linkend="auth-oauth">the server's HBA configuration.</link>
     +       </para>
     +       <para>
    -+        As part of the standard authentication handshake, libpq will ask the
    -+        server for a <emphasis>discovery document:</emphasis> a URI providing a
    -+        set of OAuth configuration parameters. The server must provide a URI
    -+        that is directly constructed from the components of the
    ++        As part of the standard authentication handshake, <application>libpq</application>
    ++        will ask the server for a <emphasis>discovery document:</emphasis> a URI
    ++        providing a set of OAuth configuration parameters. The server must
    ++        provide a URI that is directly constructed from the components of the
     +        <literal>oauth_issuer</literal>, and this value must exactly match the
     +        issuer identifier that is declared in the discovery document itself, or
     +        the connection will fail. This is required to prevent a class of "mix-up
    @@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
     +        <xref linkend="libpq-connect-oauth-scope"/> be set as well, since the
     +        client will not have a chance to ask the server for a correct scope
     +        setting, and the default scopes for a token may not be sufficient to
    -+        connect.) libpq currently supports the following well-known endpoints:
    -+        <itemizedlist>
    ++        connect.) <application>libpq</application> currently supports the
    ++        following well-known endpoints:
    ++        <itemizedlist spacing="compact">
     +         <listitem><para><literal>/.well-known/openid-configuration</literal></para></listitem>
     +         <listitem><para><literal>/.well-known/oauth-authorization-server</literal></para></listitem>
     +        </itemizedlist>
    @@ doc/src/sgml/libpq.sgml: void PQinitSSL(int do_ssl);
     +   TODO
     +  </para>
     +
    -+  <para>
    -+   <variablelist>
    -+    <varlistentry id="libpq-PQsetAuthDataHook">
    -+     <term><function>PQsetAuthDataHook</function><indexterm><primary>PQsetAuthDataHook</primary></indexterm></term>
    ++  <sect2 id="libpq-oauth-authdata-hooks">
    ++   <title>Authdata Hooks</title>
     +
    -+     <listitem>
    -+      <para>
    -+       TODO
    ++   <para>
    ++    The behavior of the OAuth flow may be modified or replaced by a client using
    ++    the following hook API:
    ++
    ++    <variablelist>
    ++     <varlistentry id="libpq-PQsetAuthDataHook">
    ++      <term><function>PQsetAuthDataHook</function><indexterm><primary>PQsetAuthDataHook</primary></indexterm></term>
    ++
    ++      <listitem>
    ++       <para>
    ++        Sets the <symbol>PGauthDataHook</symbol>, overriding
    ++        <application>libpq</application>'s handling of one or more aspects of
    ++        its OAuth client flow.
     +<synopsis>
     +void PQsetAuthDataHook(PQauthDataHook_type hook);
     +</synopsis>
    -+      </para>
    ++        If <replaceable>hook</replaceable> is <literal>NULL</literal>, the
    ++        default handler will be reinstalled. Otherwise, the application passes
    ++        a pointer to a callback function with the signature:
    ++<programlisting>
    ++int hook_fn(PGauthData type, PGconn *conn, void *data);
    ++</programlisting>
    ++        which <application>libpq</application> will call when when action is
    ++        required of the application. <replaceable>type</replaceable> describes
    ++        the request being made, <replaceable>conn</replaceable> is the
    ++        connection handle being authenticated, and <replaceable>data</replaceable>
    ++        points to request-specific metadata. The contents of this pointer are
    ++        determined by <replaceable>type</replaceable>; see
    ++        <xref linkend="libpq-oauth-authdata-hooks-types"/> for the supported
    ++        list.
    ++       </para>
    ++       <para>
    ++        Hooks can be chained together to allow cooperative and/or fallback
    ++        behavior. In general, a hook implementation should examine the incoming
    ++        <replaceable>type</replaceable> (and, potentially, the request metadata
    ++        and/or the settings for the particular <replaceable>conn</replaceable>
    ++        in use) to decide whether or not to handle a specific piece of authdata.
    ++        If not, it should delegate to the previous hook in the chain
    ++        (retrievable via <function>PQgetAuthDataHook</function>).
    ++       </para>
    ++       <para>
    ++        Success is indicated by returning an integer greater than zero.
    ++        Returning a negative integer signals an error condition and abandons the
    ++        connection attempt. (A zero value is reserved for the default
    ++        implementation.)
    ++       </para>
    ++      </listitem>
    ++     </varlistentry>
    ++
    ++     <varlistentry id="libpq-PQgetAuthDataHook">
    ++      <term><function>PQgetAuthDataHook</function><indexterm><primary>PQgetAuthDataHook</primary></indexterm></term>
    ++
    ++      <listitem>
    ++       <para>
    ++        Retrieves the current value of <symbol>PGauthDataHook</symbol>.
    ++<synopsis>
    ++PQauthDataHook_type PQgetAuthDataHook(void);
    ++</synopsis>
    ++        At initialization time (before the first call to
    ++        <function>PQsetAuthDataHook</function>), this function will return
    ++        <symbol>PQdefaultAuthDataHook</symbol>.
    ++       </para>
    ++      </listitem>
    ++     </varlistentry>
    ++    </variablelist>
    ++   </para>
    ++
    ++   <sect3 id="libpq-oauth-authdata-hooks-types">
    ++    <title>Hook Types</title>
    ++    <para>
    ++     The following <symbol>PGauthData</symbol> types and their corresponding
    ++     <replaceable>data</replaceable> structures are defined:
    ++
    ++     <variablelist>
    ++      <varlistentry id="libpq-oauth-authdata-prompt-oauth-device">
    ++       <term>
    ++        <symbol>PQAUTHDATA_PROMPT_OAUTH_DEVICE</symbol>
    ++        <indexterm><primary>PQAUTHDATA_PROMPT_OAUTH_DEVICE</primary></indexterm>
    ++       </term>
    ++       <listitem>
    ++        <para>
    ++         Replaces the default user prompt during the builtin device
    ++         authorization client flow. <replaceable>data</replaceable> points to
    ++         an instance of <symbol>PGpromptOAuthDevice</symbol>:
    ++<synopsis>
    ++typedef struct _PGpromptOAuthDevice
    ++{
    ++    const char *verification_uri;   /* verification URI to visit */
    ++    const char *user_code;          /* user code to enter */
    ++} PGpromptOAuthDevice;
    ++</synopsis>
    ++        </para>
    ++        <para>
    ++         The OAuth Device Authorization flow included in <application>libpq</application>
    ++         requires the end user to visit a URL with a browser, then enter a code
    ++         which permits <application>libpq</application> to connect to the server
    ++         on their behalf. The default prompt simply prints the
    ++         <literal>verification_uri</literal> and <literal>user_code</literal>
    ++         on standard error. Replacement implementations may display this
    ++         information using any preferred method, for example with a GUI.
    ++        </para>
    ++        <para>
    ++         This callback is only invoked during the builtin device
    ++         authorization flow. If the application installs a
    ++         <link linkend="libpq-oauth-authdata-oauth-bearer-token">custom OAuth
    ++         flow</link>, this authdata type will not be used.
    ++        </para>
    ++       </listitem>
    ++      </varlistentry>
    ++
    ++      <varlistentry id="libpq-oauth-authdata-oauth-bearer-token">
    ++       <term>
    ++        <symbol>PQAUTHDATA_OAUTH_BEARER_TOKEN</symbol>
    ++        <indexterm><primary>PQAUTHDATA_OAUTH_BEARER_TOKEN</primary></indexterm>
    ++       </term>
    ++       <listitem>
    ++        <para>
    ++         Replaces the entire OAuth flow with a custom implementation. The hook
    ++         should either directly return a Bearer token for the current
    ++         user/issuer/scope combination, if one is available without blocking, or
    ++         else set up an asynchronous callback to retrieve one.
    ++        </para>
    ++        <para>
    ++         <replaceable>data</replaceable> points to an instance
    ++         of <symbol>PGoauthBearerRequest</symbol>, which should be filled in
    ++         by the implementation:
    ++<synopsis>
    ++typedef struct _PGoauthBearerRequest
    ++{
    ++    /* Hook inputs (constant across all calls) */
    ++    const char *const openid_configuration; /* OIDC discovery URI */
    ++    const char *const scope;                /* required scope(s), or NULL */
     +
    ++    /* Hook outputs */
    ++
    ++    /* Callback implementing a custom asynchronous OAuth flow. */
    ++    PostgresPollingStatusType (*async) (PGconn *conn,
    ++                                        struct _PGoauthBearerRequest *request,
    ++                                        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;
    ++</synopsis>
    ++        </para>
    ++        <para>
    ++         Two pieces of information are provided to the hook by
    ++         <application>libpq</application>:
    ++         <replaceable>openid_configuration</replaceable> contains the URL of an
    ++         OAuth discovery document describing the authorization server's
    ++         supported flows, and <replaceable>scope</replaceable> contains a
    ++         (possibly empty) space-separated list of OAuth scopes which are
    ++         required to access the server. Either or both may be
    ++         <literal>NULL</literal> to indicate that the information was not
    ++         discoverable. (In this case, implementations may be able to establish
    ++         the requirements using some other preconfigured knowledge, or they may
    ++         choose to fail.)
    ++        </para>
    ++        <para>
    ++         The final output of the hook is <replaceable>token</replaceable>, which
    ++         must point to a valid Bearer token for use on the connection. (This
    ++         token should be issued by the
    ++         <xref linkend="libpq-connect-oauth-issuer"/> and hold the requested
    ++         scopes, or the connection will be rejected by the server's validator
    ++         module.) The allocated token string must remain valid until
    ++         <application>libpq</application> is finished connecting; the hook
    ++         should set a <replaceable>cleanup</replaceable> callback which will be
    ++         called when <application>libpq</application> no longer requires it.
    ++        </para>
    ++        <para>
    ++         If an implementation cannot immediately produce a
    ++         <replaceable>token</replaceable> during the initial call to the hook,
    ++         it should set the <replaceable>async</replaceable> callback to handle
    ++         nonblocking communication with the authorization server.
    ++         <footnote>
    ++          <para>
    ++           Performing blocking operations during the
    ++           <symbol>PQAUTHDATA_OAUTH_BEARER_TOKEN</symbol> hook callback will
    ++           interfere with nonblocking connection APIs such as
    ++           <function>PQconnectPoll</function> and prevent concurrent connections
    ++           from making progress. Applications which only ever use the
    ++           synchronous connection primitives, such as
    ++           <function>PQconnectdb</function>, may synchronously retrieve a token
    ++           during the hook instead of implementing the
    ++           <replaceable>async</replaceable> callback, but they will necessarily
    ++           be limited to one connection at a time.
    ++          </para>
    ++         </footnote>
    ++         This will be called to begin the flow immediately upon return from the
    ++         hook. When the callback cannot make further progress without blocking,
    ++         it should return either <symbol>PGRES_POLLING_READING</symbol> or
    ++         <symbol>PGRES_POLLING_WRITING</symbol> after setting
    ++         <literal>*pgsocket</literal> to the file descriptor that will be marked
    ++         ready to read/write when progress can be made again. (This descriptor
    ++         is then provided to the top-level polling loop via
    ++         <function>PQsocket()</function>.) Return <symbol>PGRES_POLLING_OK</symbol>
    ++         after setting <replaceable>token</replaceable> when the flow is
    ++         complete, or <symbol>PGRES_POLLING_FAILED</symbol> to indicate failure.
    ++        </para>
    ++        <para>
    ++         Implementations may wish to store additional data for bookkeeping
    ++         across calls to the <replaceable>async</replaceable> and
    ++         <replaceable>cleanup</replaceable> callbacks. The
    ++         <replaceable>user</replaceable> pointer is provided for this purpose;
    ++         <application>libpq</application> will not touch its contents and the
    ++         application may use it at its convenience. (Remember to free any
    ++         allocations during token cleanup.)
    ++        </para>
    ++       </listitem>
    ++      </varlistentry>
    ++     </variablelist>
    ++    </para>
    ++   </sect3>
    ++  </sect2>
    ++
    ++  <sect2 id="libpq-oauth-debugging">
    ++   <title>Debugging and Developer Settings</title>
    ++
    ++   <para>
    ++    A "dangerous debugging mode" may be enabled by setting the environment
    ++    variable <envar>PGOAUTHDEBUG=UNSAFE</envar>. This functionality is provided
    ++    for ease of local development and testing only. It does several things that
    ++    you will not want a production system to do:
    ++
    ++    <itemizedlist spacing="compact">
    ++     <listitem>
     +      <para>
    -+       If <replaceable>hook</replaceable> is set to a null pointer instead of
    -+       a function pointer, the default hook will be installed.
    ++       permits the use of unencrypted HTTP during the OAuth provider exchange
     +      </para>
     +     </listitem>
    -+    </varlistentry>
    -+
    -+    <varlistentry id="libpq-PQgetAuthDataHook">
    -+     <term><function>PQgetAuthDataHook</function><indexterm><primary>PQgetAuthDataHook</primary></indexterm></term>
    -+
     +     <listitem>
     +      <para>
    -+       Retrieves the current value of <literal>PGauthDataHook</literal>.
    -+<synopsis>
    -+PQauthDataHook_type PQgetAuthDataHook(void);
    -+</synopsis>
    ++       allows the system's trusted CA list to be completely replaced using the
    ++       <envar>PGOAUTHCAFILE</envar> environment variable
     +      </para>
     +     </listitem>
    -+    </varlistentry>
    -+   </variablelist>
    -+  </para>
    -+
    ++     <listitem>
    ++      <para>
    ++       sprays HTTP traffic (containing several critical secrets) to standard
    ++       error during the OAuth flow
    ++      </para>
    ++     </listitem>
    ++     <listitem>
    ++      <para>
    ++       permits the use of zero-second retry intervals, which can cause the
    ++       client to busy-loop and pointlessly consume CPU
    ++      </para>
    ++     </listitem>
    ++    </itemizedlist>
    ++   </para>
    ++   <warning>
    ++    <para>
    ++     Do not share the output of the OAuth flow traffic with third parties. It
    ++     contains secrets that can be used to attack your clients and servers.
    ++    </para>
    ++   </warning>
    ++  </sect2>
     + </sect1>
     +
      
    @@ doc/src/sgml/oauth-validators.sgml (new)
     + <para>
     +  <productname>PostgreSQL</productname> provides infrastructure for creating
     +  custom modules to perform server-side validation of OAuth bearer tokens.
    ++  Because OAuth implementations vary so wildly, and bearer token validation is
    ++  heavily dependent on the issuing party, the server cannot check the token
    ++  itself; validator modules provide the glue between the server and the OAuth
    ++  provider in use.
     + </para>
     + <para>
    -+  OAuth validation modules must at least consist of an initialization function
    ++  OAuth validator modules must at least consist of an initialization function
     +  (see <xref linkend="oauth-validator-init"/>) and the required callback for
     +  performing validation (see <xref linkend="oauth-validator-callback-validate"/>).
     + </para>
    ++ <warning>
    ++  <para>
    ++   Since a misbehaving validator might let unauthorized users into the database,
    ++   correct implementation is critical. See
    ++   <xref linkend="oauth-validator-design"/> for design considerations.
    ++  </para>
    ++ </warning>
    ++
    ++ <sect1 id="oauth-validator-design">
    ++  <title>Safely Designing a Validator Module</title>
    ++  <warning>
    ++   <para>
    ++    Read and understand the entirety of this section before implementing a
    ++    validator module. A malfunctioning validator is potentially worse than no
    ++    authentication at all, both because of the false sense of security it
    ++    provides, and because it may contribute to attacks against other pieces of
    ++    an OAuth ecosystem.
    ++   </para>
    ++  </warning>
    ++
    ++  <sect2 id="oauth-validator-design-responsibilities">
    ++   <title>Validator Responsibilities</title>
    ++   <para>
    ++    TODO
    ++   </para>
    ++   <variablelist>
    ++    <varlistentry>
    ++     <term>Validate the Token</term>
    ++     <listitem>
    ++      <para>
    ++       The validator must first ensure that the presented token is in fact a
    ++       valid Bearer token for use in client authentication. The correct way to
    ++       do this depends on the provider, but it generally involves either
    ++       cryptographic operations to prove that the token was created by a trusted
    ++       party (offline validation), or the presentation of the token to that
    ++       trusted party so that it can perform validation for you (online
    ++       validation).
    ++      </para>
    ++      <para>
    ++       Online validation, usually implemented via
    ++       <ulink url="https://datatracker.ietf.org/doc/html/rfc7662">OAuth Token
    ++       Introspection</ulink>, requires fewer steps of a validator module and
    ++       allows central revocation of a token in the event that it is stolen
    ++       or misissued. However, it does require the module to make at least one
    ++       network call per authentication attempt (all of which must complete
    ++       within the configured <xref linkend="guc-authentication-timeout"/>).
    ++       Additionally, your provider may not provide introspection endpoints for
    ++       use by external resource servers.
    ++      </para>
    ++      <para>
    ++       Offline validation is much more involved, typically requiring a validator
    ++       to maintain a list of trusted signing keys for a provider and then
    ++       check the token's cryptographic signature along with its contents.
    ++       Implementations must follow the provider's instructions to the letter,
    ++       including any verification of issuer ("where is this token from?"),
    ++       audience ("who is this token for?"), and validity period ("when can this
    ++       token be used?"). Since there is no communication between the module and
    ++       the provider, tokens cannot be centrally revoked using this method;
    ++       offline validator implementations may wish to place restrictions on the
    ++       maximum length of a token's validity period.
    ++      </para>
    ++      <para>
    ++       If the token cannot be validated, the module should immediately fail.
    ++       Further authentication/authorization is pointless if the bearer token
    ++       wasn't issued by a trusted party.
    ++      </para>
    ++     </listitem>
    ++    </varlistentry>
    ++    <varlistentry>
    ++     <term>Authorize the Client</term>
    ++     <listitem>
    ++      <para>
    ++       Next the validator must ensure that the end user has given the client
    ++       permission to access the server on their behalf. This generally involves
    ++       checking the scopes that have been assigned to the token, to make sure
    ++       that they cover database access for the current HBA parameters.
    ++      </para>
    ++      <para>
    ++       The purpose of this step is to prevent an OAuth client from obtaining a
    ++       token under false pretenses. If the validator requires all tokens to
    ++       carry scopes that cover database access, the provider should then loudly
    ++       prompt the user to grant that access during the flow. This gives them the
    ++       opportunity to reject the request if the client isn't supposed to be
    ++       using their credentials to connect to databases.
    ++      </para>
    ++      <para>
    ++       While it is possible to establish client authorization without explicit
    ++       scopes by using out-of-band knowledge of the deployed architecture, doing
    ++       so removes the user from the loop, which prevents them from catching
    ++       deployment mistakes and allows any such mistakes to be exploited
    ++       silently. Access to the database must be tightly restricted to only
    ++       trusted clients
    ++       <footnote>
    ++        <para>
    ++         That is, "trusted" in the sense that the OAuth client and the
    ++         <productname>PostgreSQL</productname> server are controlled by the same
    ++         entity. Notably, the Device Authorization client flow supported by
    ++         libpq does not usually meet this bar, since it's designed for use by
    ++         public/untrusted clients.
    ++        </para>
    ++       </footnote>
    ++       if users are not prompted for additional scopes.
    ++      </para>
    ++     </listitem>
    ++    </varlistentry>
    ++    <varlistentry>
    ++     <term>Authenticate the End User</term>
    ++     <listitem>
    ++      <para>
    ++       Finally, the validator should determine a user identifier for the token,
    ++       either by asking the provider for this information or by extracting it
    ++       from the token itself, and return that identifier to the server (which
    ++       will then make a final authorization decision using the HBA
    ++       configuration). This identifier will be available within the session via
    ++       <link linkend="functions-info-session-table"><function>system_user</function></link>
    ++       and recorded in the server logs if <xref linkend="guc-log-connections"/>
    ++       is enabled.
    ++      </para>
    ++      <para>
    ++       Different providers may record a variety of different authentication
    ++       information for an end user, typically referred to as
    ++       <emphasis>claims</emphasis>. Providers usually document which of these
    ++       claims are trustworthy enough to use for authorization decisions and
    ++       which are not. (For instance, it would probably not be wise to use an
    ++       end user's full name as the identifier for authentication, since many
    ++       providers allow users to change their display names arbitrarily.)
    ++       Ultimately, the choice of which claim (or combination of claims) to use
    ++       comes down to the provider implementation and application requirements.
    ++      </para>
    ++      <para>
    ++       Note that anonymous/pseudonymous login is possible as well, by enabling
    ++       usermap delegation; see
    ++       <xref linkend="oauth-validator-design-usermap-delegation"/>.
    ++      </para>
    ++     </listitem>
    ++    </varlistentry>
    ++   </variablelist>
    ++  </sect2>
    ++
    ++  <sect2 id="oauth-validator-design-guidelines">
    ++   <title>General Coding Guidelines</title>
    ++   <para>
    ++    TODO
    ++   </para>
    ++   <variablelist>
    ++    <varlistentry>
    ++     <term>Token Confidentiality</term>
    ++     <listitem>
    ++      <para>
    ++       Modules should not write tokens, or pieces of tokens, into the server
    ++       log. This is true even if the module considers the token invalid; an
    ++       attacker who confuses a client into communicating with the wrong provider
    ++       should not be able to retrieve that (otherwise valid) token from the
    ++       disk.
    ++      </para>
    ++      <para>
    ++       Implementations that send tokens over the network (for example, to
    ++       perform online token validation with a provider) must authenticate the
    ++       peer and ensure that strong transport security is in use.
    ++      </para>
    ++     </listitem>
    ++    </varlistentry>
    ++    <varlistentry>
    ++     <term>Logging</term>
    ++     <listitem>
    ++      <para>
    ++       Modules may use the same <link linkend="error-message-reporting">logging
    ++       facilities</link> as standard extensions; however, the rules for emitting
    ++       log entries to the client are subtly different during the authentication
    ++       phase of the connection. Generally speaking, modules should log
    ++       verification problems at the <symbol>COMMERROR</symbol> level and return
    ++       normally, instead of using <symbol>ERROR</symbol>/<symbol>FATAL</symbol>
    ++       to unwind the stack, to avoid leaking information to unauthenticated
    ++       clients.
    ++      </para>
    ++     </listitem>
    ++    </varlistentry>
    ++    <varlistentry>
    ++     <term>Interruptibility</term>
    ++     <listitem>
    ++      <para>
    ++       Modules must remain interruptible by signals so that the server can
    ++       correctly handle authentication timeouts and shutdown signals from
    ++       <application>pg_ctl</application>. For example, a module receiving
    ++       <symbol>EINTR</symbol>/<symbol>EAGAIN</symbol> from a blocking call
    ++       should call <function>CHECK_FOR_INTERRUPTS()</function> before retrying.
    ++       The same should be done during any long-running loops. Failure to follow
    ++       this guidance may result in hung sessions.
    ++      </para>
    ++     </listitem>
    ++    </varlistentry>
    ++    <varlistentry>
    ++     <term>Testing</term>
    ++     <listitem>
    ++      <para>
    ++       The breadth of testing an OAuth system is well beyond the scope of this
    ++       documentation, but note that implementers should consider negative
    ++       testing to be mandatory. It's trivial to design a module that lets
    ++       authorized users in; the whole point of the system is to keep
    ++       unauthorized users out.
    ++      </para>
    ++     </listitem>
    ++    </varlistentry>
    ++   </variablelist>
    ++  </sect2>
    ++
    ++  <sect2 id="oauth-validator-design-usermap-delegation">
    ++   <title>Authorizing Users (Usermap Delegation)</title>
    ++   <para>
    ++    The standard deliverable of a validation module is the user identifier,
    ++    which the server will then compare to any configured
    ++    <link linkend="auth-username-maps"><filename>pg_ident.conf</filename>
    ++    mappings</link> and determine whether the end user is authorized to connect.
    ++    However, OAuth is itself an authorization framework, and tokens may carry
    ++    information about user privileges. For example, a token may be associated
    ++    with the organizational groups that a user belongs to, or list the roles
    ++    that a user may assume, and duplicating that knowledge into local usermaps
    ++    for every server may not be desirable.
    ++   </para>
    ++   <para>
    ++    To bypass username mapping entirely, and have the validator module assume
    ++    the additional responsibility of authorizing user connections, the HBA may
    ++    be configured with <xref linkend="auth-oauth-delegate-ident-mapping"/>.
    ++    The module may then use token scopes or an equivalent method to decide
    ++    whether the user is allowed to connect under their desired role. The user
    ++    identifier will still be recorded by the server, but it plays no part in
    ++    determining whether to continue the connection.
    ++   </para>
    ++   <para>
    ++    Using this scheme, authentication itself is optional. As long as the module
    ++    reports that the connection is authorized, login will continue even if there
    ++    is no recorded user identifier at all. This makes it possible to implement
    ++    anonymous or pseudonymous access to the database, where the third-party
    ++    provider performs all necessary authentication but does not provide any
    ++    user-identifying information to the server. (Some providers may create an
    ++    anonymized ID number that can be recorded instead, for later auditing.)
    ++   </para>
    ++   <para>
    ++    Usermap delegation provides the most architectural flexibility, but it turns
    ++    the validator module into a single point of failure for connection
    ++    authorization. Use with caution.
    ++   </para>
    ++  </sect2>
    ++ </sect1>
     +
     + <sect1 id="oauth-validator-init">
     +  <title>Initialization Functions</title>
    @@ doc/src/sgml/oauth-validators.sgml (new)
     +   validator module a function named
     +   <function>_PG_oauth_validator_module_init</function> must be provided. The
     +   return value of the function must be a pointer to a struct of type
    -+   <structname>OAuthValidatorCallbacks</structname> which contains all that
    -+   libpq need to perform token validation using the module. The returned
    ++   <structname>OAuthValidatorCallbacks</structname>, which contains pointers to
    ++   the module's token validation functions. The returned
     +   pointer must be of server lifetime, which is typically achieved by defining
     +   it as a <literal>static const</literal> variable in global scope.
     +<programlisting>
    @@ doc/src/sgml/oauth-validators.sgml (new)
     +  <title>OAuth Validator Callbacks</title>
     +  <para>
     +   OAuth validator modules implement their functionality by defining a set of
    -+   callbacks, libpq will call them as required to process the authentication
    -+   request from the user.
    ++   callbacks. The server will call them as required to process the
    ++   authentication request from the user.
     +  </para>
     +
     +  <sect2 id="oauth-validator-callback-startup">
    @@ src/backend/libpq/auth-oauth.c (new)
     +	.max_message_length = PG_MAX_AUTH_TOKEN_LENGTH,
     +};
     +
    -+
    ++/* Valid states for the oauth_exchange() machine. */
     +typedef enum
     +{
     +	OAUTH_STATE_INIT = 0,
    @@ src/backend/libpq/auth-oauth.c (new)
     +	OAUTH_STATE_FINISHED,
     +} oauth_state;
     +
    ++/* Mechanism callback state. */
     +struct oauth_ctx
     +{
     +	oauth_state state;
    @@ src/backend/libpq/auth-oauth.c (new)
     +static void generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen);
     +static bool validate(Port *port, const char *auth);
     +
    -+#define KVSEP 0x01
    -+#define AUTH_KEY "auth"
    -+#define BEARER_SCHEME "Bearer "
    ++/* Constants seen in an OAUTHBEARER client initial response. */
    ++#define KVSEP 0x01				/* separator byte for key/value pairs */
    ++#define AUTH_KEY "auth"			/* key containing the Authorization header */
    ++#define BEARER_SCHEME "Bearer " /* required header scheme (case-insensitive!) */
     +
    ++/*
    ++ * Retrieves the OAUTHBEARER mechanism list (currently a single item).
    ++ *
    ++ * For a full description of the API, see libpq/sasl.h.
    ++ */
     +static void
     +oauth_get_mechanisms(Port *port, StringInfo buf)
     +{
    @@ src/backend/libpq/auth-oauth.c (new)
     +	appendStringInfoChar(buf, '\0');
     +}
     +
    ++/*
    ++ * Initializes mechanism state and loads the configured validator module.
    ++ *
    ++ * For a full description of the API, see libpq/sasl.h.
    ++ */
     +static void *
     +oauth_init(Port *port, const char *selected_mech, const char *shadow_pass)
     +{
    @@ src/backend/libpq/auth-oauth.c (new)
     +	return ctx;
     +}
     +
    ++/*
    ++ * Implements the OAUTHBEARER SASL exchange (RFC 7628, Sec. 3.2). This pulls
    ++ * apart the client initial response and validates the Bearer token. It also
    ++ * handles the dummy error response for a failed handshake, as described in
    ++ * Sec. 3.2.3.
    ++ *
    ++ * For a full description of the API, see libpq/sasl.h.
    ++ */
     +static int
     +oauth_exchange(void *opaq, const char *input, int inputlen,
     +			   char **output, int *outputlen, const char **logdetail)
    @@ src/backend/libpq/auth-oauth.c (new)
     +	return NULL;
     +}
     +
    ++/*
    ++ * Builds the JSON response for failed authentication (RFC 7628, Sec. 3.2.2).
    ++ * This contains the required scopes for entry and a pointer to the OAuth/OpenID
    ++ * discovery document, which the client may use to conduct its OAuth flow.
    ++ */
     +static void
     +generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen)
     +{
    @@ src/backend/libpq/auth-oauth.c (new)
     +	return token;
     +}
     +
    ++/*
    ++ * Checks that the "auth" kvpair in the client response contains a syntactically
    ++ * valid Bearer token, then passes it along to the loaded validator module for
    ++ * authorization. Returns true if validation succeeds.
    ++ */
     +static bool
     +validate(Port *port, const char *auth)
     +{
    @@ src/backend/libpq/auth-oauth.c (new)
     +	before_shmem_exit(shutdown_validator_library, 0);
     +}
     +
    ++/*
    ++ * Call the validator module's shutdown callback, if one is provided. This is
    ++ * invoked via before_shmem_exit().
    ++ */
     +static void
     +shutdown_validator_library(int code, Datum arg)
     +{
    @@ src/backend/libpq/hba.c: parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
     +					errcode(ERRCODE_CONFIG_FILE_ERROR),
     +			/* translator: strings are replaced with hba options */
     +					errmsg("%s cannot be used in combination with %s",
    -+						   "map", "trust_validator_authz"),
    ++						   "map", "delegate_ident_mapping"),
     +					errcontext("line %d of configuration file \"%s\"",
     +							   line_num, file_name));
    -+			*err_msg = "map cannot be used in combination with trust_validator_authz";
    ++			*err_msg = "map cannot be used in combination with delegate_ident_mapping";
     +			return NULL;
     +		}
     +	}
    @@ src/backend/libpq/hba.c: parse_hba_auth_opt(char *name, char *val, HbaLine *hbal
     +		REQUIRE_AUTH_OPTION(uaOAuth, "validator", "oauth");
     +		hbaline->oauth_validator = pstrdup(val);
     +	}
    -+	else if (strcmp(name, "trust_validator_authz") == 0)
    ++	else if (strcmp(name, "delegate_ident_mapping") == 0)
     +	{
    -+		REQUIRE_AUTH_OPTION(uaOAuth, "trust_validator_authz", "oauth");
    ++		REQUIRE_AUTH_OPTION(uaOAuth, "delegate_ident_mapping", "oauth");
     +		if (strcmp(val, "1") == 0)
     +			hbaline->oauth_skip_usermap = true;
     +		else
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +/*-------------------------------------------------------------------------
     + *
     + * fe-auth-oauth-curl.c
    -+ *	   The libcurl implementation of OAuth/OIDC authentication.
    ++ *	   The libcurl implementation of OAuth/OIDC authentication, using the
    ++ *	   OAuth Device Authorization Grant (RFC 8628).
     + *
     + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
     + * Portions Copyright (c) 1994, Regents of the University of California
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     + */
     +
     +/* States for the overall async machine. */
    -+typedef enum
    ++enum OAuthStep
     +{
     +	OAUTH_STEP_INIT = 0,
     +	OAUTH_STEP_DISCOVERY,
     +	OAUTH_STEP_DEVICE_AUTHORIZATION,
     +	OAUTH_STEP_TOKEN_REQUEST,
     +	OAUTH_STEP_WAIT_INTERVAL,
    -+} OAuthStep;
    ++};
     +
     +/*
     + * The async_ctx holds onto state that needs to persist across multiple calls
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     + */
     +struct async_ctx
     +{
    -+	OAuthStep	step;			/* where are we in the flow? */
    ++	enum OAuthStep step;		/* where are we in the flow? */
     +
     +#ifdef HAVE_SYS_EPOLL_H
     +	int			timerfd;		/* a timerfd for signaling async timeouts */
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     + * JSON Parser Definitions
     + */
     +
    ++/*
    ++ * Parses authorization server metadata. Fields are defined by OIDC Discovery
    ++ * 1.0 and RFC 8414.
    ++ */
     +static bool
     +parse_provider(struct async_ctx *actx, struct provider *provider)
     +{
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +	return parsed;
     +}
     +
    ++/*
    ++ * Parses the Device Authorization Response (RFC 8628, Sec. 3.2).
    ++ */
     +static bool
     +parse_device_authz(struct async_ctx *actx, struct device_authz *authz)
     +{
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +	return true;
     +}
     +
    ++/*
    ++ * Parses the device access token error response (RFC 8628, Sec. 3.5, which
    ++ * uses the error response defined in RFC 6749, Sec. 5.2).
    ++ */
     +static bool
     +parse_token_error(struct async_ctx *actx, struct token_error *err)
     +{
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +	appendPQExpBuffer(&actx->errbuf, "(%s)", err->error);
     +}
     +
    ++/*
    ++ * Parses the device access token response (RFC 8628, Sec. 3.5, which uses the
    ++ * success response defined in RFC 6749, Sec. 5.1).
    ++ */
     +static bool
     +parse_access_token(struct async_ctx *actx, struct token *tok)
     +{
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +
     +	/*
     +	 * authorization_pending and slow_down are the only acceptable errors;
    -+	 * anything else and we bail.
    ++	 * anything else and we bail. These are defined in RFC 8628, Sec. 3.5.
     +	 */
     +	err = &tok.err;
     +	if (strcmp(err->error, "authorization_pending") != 0 &&
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +
     +	/*
     +	 * A slow_down error requires us to permanently increase our retry
    -+	 * interval by five seconds. RFC 8628, Sec. 3.5.
    ++	 * interval by five seconds.
     +	 */
     +	if (strcmp(err->error, "slow_down") == 0)
     +	{
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +prompt_user(struct async_ctx *actx, PGconn *conn)
     +{
     +	int			res;
    -+	PQpromptOAuthDevice prompt = {
    ++	PGpromptOAuthDevice prompt = {
     +		.verification_uri = actx->authz.verification_uri,
     +		.user_code = actx->authz.user_code,
     +		/* TODO: optional fields */
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +/*-------------------------------------------------------------------------
     + *
     + * fe-auth-oauth.c
    -+ *	   The front-end (client) implementation of OAuth/OIDC authentication.
    ++ *	   The front-end (client) implementation of OAuth/OIDC authentication
    ++ *	   using the SASL OAUTHBEARER mechanism.
     + *
     + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
     + * Portions Copyright (c) 1994, Regents of the University of California
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	oauth_free,
     +};
     +
    ++/*
    ++ * Initializes mechanism state for OAUTHBEARER.
    ++ *
    ++ * For a full description of the API, see libpq/fe-auth-sasl.h.
    ++ */
     +static void *
     +oauth_init(PGconn *conn, const char *password,
     +		   const char *sasl_mechanism)
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	if (!state)
     +		return NULL;
     +
    -+	state->state = FE_OAUTH_INIT;
    ++	state->step = FE_OAUTH_INIT;
     +	state->conn = conn;
     +
     +	return state;
     +}
     +
    ++/*
    ++ * Frees the state allocated by oauth_init().
    ++ */
    ++static void
    ++oauth_free(void *opaq)
    ++{
    ++	fe_oauth_state *state = opaq;
    ++
    ++	free(state->token);
    ++	if (state->async_ctx)
    ++		state->free_async_ctx(state->conn, state->async_ctx);
    ++
    ++	free(state);
    ++}
    ++
     +#define kvsep "\x01"
     +
     +/*
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	return response;
     +}
     +
    ++/*
    ++ * JSON Parser (for the OAUTHBEARER error result)
    ++ */
    ++
    ++/* Relevant JSON fields in the error result object. */
     +#define ERROR_STATUS_FIELD "status"
     +#define ERROR_SCOPE_FIELD "scope"
     +#define ERROR_OPENID_CONFIGURATION_FIELD "openid-configuration"
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	return issuer;
     +}
     +
    ++/*
    ++ * Parses the server error result (RFC 7628, Sec. 3.2.2) contained in msg and
    ++ * stores any discovered openid_configuration and scope settings for the
    ++ * connection. conn->oauth_want_retry will be set if the error status is
    ++ * suitable for a second attempt.
    ++ */
     +static bool
     +handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen)
     +{
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	return success;
     +}
     +
    -+static void
    -+free_request(PGconn *conn, void *vreq)
    -+{
    -+	PQoauthBearerRequest *request = vreq;
    -+
    -+	if (request->cleanup)
    -+		request->cleanup(conn, request);
    -+
    -+	free(request);
    -+}
    -+
    ++/*
    ++ * Callback implementation of conn->async_auth() for a user-defined OAuth flow.
    ++ * Delegates the retrieval of the token to the application's async callback.
    ++ *
    ++ * This will be called multiple times as needed; the application is responsible
    ++ * for setting an altsock to signal and returning the correct PGRES_POLLING_*
    ++ * statuses for use by PQconnectPoll().
    ++ */
     +static PostgresPollingStatusType
     +run_user_oauth_flow(PGconn *conn, pgsocket *altsock)
     +{
     +	fe_oauth_state *state = conn->sasl_state;
    -+	PQoauthBearerRequest *request = state->async_ctx;
    ++	PGoauthBearerRequest *request = state->async_ctx;
     +	PostgresPollingStatusType status;
     +
     +	if (!request->async)
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	return status;
     +}
     +
    ++/*
    ++ * Cleanup callback for the user flow. Delegates most of its job to the
    ++ * user-provided cleanup implementation.
    ++ */
    ++static void
    ++free_request(PGconn *conn, void *vreq)
    ++{
    ++	PGoauthBearerRequest *request = vreq;
    ++
    ++	if (request->cleanup)
    ++		request->cleanup(conn, request);
    ++
    ++	free(request);
    ++}
    ++
    ++/*
    ++ * Chooses an OAuth client flow for the connection, which will retrieve a Bearer
    ++ * token for presentation to the server.
    ++ *
    ++ * If the application has registered a custom flow handler using
    ++ * PQAUTHDATA_OAUTH_BEARER_TOKEN, it may either return a token immediately (e.g.
    ++ * if it has one cached for immediate use), or set up for a series of
    ++ * asynchronous callbacks which will be managed by run_user_oauth_flow().
    ++ *
    ++ * If the default handler is used instead, a Device Authorization flow is used
    ++ * for the connection if support has been compiled in. (See
    ++ * fe-auth-oauth-curl.c for implementation details.)
    ++ *
    ++ * If neither a custom handler nor the builtin flow is available, the connection
    ++ * fails here.
    ++ */
     +static bool
     +setup_token_request(PGconn *conn, fe_oauth_state *state)
     +{
     +	int			res;
    -+	PQoauthBearerRequest request = {
    ++	PGoauthBearerRequest request = {
     +		.openid_configuration = conn->oauth_discovery_uri,
     +		.scope = conn->oauth_scope,
     +	};
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request);
     +	if (res > 0)
     +	{
    -+		PQoauthBearerRequest *request_copy;
    ++		PGoauthBearerRequest *request_copy;
     +
     +		if (request.token)
     +		{
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	return true;
     +}
     +
    ++/*
    ++ * Implements the OAUTHBEARER SASL exchange (RFC 7628, Sec. 3.2).
    ++ *
    ++ * If the necessary OAuth parameters are set up on the connection, this will run
    ++ * the client flow asynchronously and present the resulting token to the server.
    ++ * Otherwise, an empty discovery response will be sent and any parameters sent
    ++ * back by the server will be stored for a second attempt.
    ++ *
    ++ * For a full description of the API, see libpq/sasl.h.
    ++ */
     +static SASLStatus
     +oauth_exchange(void *opaq, bool final,
     +			   char *input, int inputlen,
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	*output = NULL;
     +	*outputlen = 0;
     +
    -+	switch (state->state)
    ++	switch (state->step)
     +	{
     +		case FE_OAUTH_INIT:
     +			/* We begin in the initial response phase. */
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +					 */
     +					Assert(conn->async_auth);	/* should have been set
     +												 * already */
    -+					state->state = FE_OAUTH_REQUESTING_TOKEN;
    ++					state->step = FE_OAUTH_REQUESTING_TOKEN;
     +					return SASL_ASYNC;
     +				}
     +			}
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +				return SASL_FAILED;
     +
     +			*outputlen = strlen(*output);
    -+			state->state = FE_OAUTH_BEARER_SENT;
    ++			state->step = FE_OAUTH_BEARER_SENT;
     +
     +			return SASL_CONTINUE;
     +
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +			}
     +			*outputlen = strlen(*output);	/* == 1 */
     +
    -+			state->state = FE_OAUTH_SERVER_ERROR;
    ++			state->step = FE_OAUTH_SERVER_ERROR;
     +			return SASL_CONTINUE;
     +
     +		case FE_OAUTH_SERVER_ERROR:
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	return false;
     +}
     +
    -+static void
    -+oauth_free(void *opaq)
    -+{
    -+	fe_oauth_state *state = opaq;
    -+
    -+	free(state->token);
    -+	if (state->async_ctx)
    -+		state->free_async_ctx(state->conn, state->async_ctx);
    -+
    -+	free(state);
    -+}
    -+
     +/*
     + * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
     + */
    @@ src/interfaces/libpq/fe-auth-oauth.h (new)
     +#include "libpq-int.h"
     +
     +
    -+typedef enum
    ++enum fe_oauth_step
     +{
     +	FE_OAUTH_INIT,
     +	FE_OAUTH_REQUESTING_TOKEN,
     +	FE_OAUTH_BEARER_SENT,
     +	FE_OAUTH_SERVER_ERROR,
    -+} fe_oauth_state_enum;
    ++};
     +
     +typedef struct
     +{
    -+	fe_oauth_state_enum state;
    ++	enum fe_oauth_step step;
     +
     +	PGconn	   *conn;
     +	char	   *token;
    @@ src/interfaces/libpq/fe-auth-oauth.h (new)
     +extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock);
     +extern bool oauth_unsafe_debugging_enabled(void);
     +
    ++/* Mechanisms in fe-auth-oauth.c */
    ++extern const pg_fe_sasl_mech pg_oauth_mech;
    ++
     +#endif							/* FE_AUTH_OAUTH_H */
     
      ## src/interfaces/libpq/fe-auth-sasl.h ##
    @@ src/interfaces/libpq/fe-auth.c
      #include "common/scram-common.h"
      #include "fe-auth.h"
      #include "fe-auth-sasl.h"
    ++#include "fe-auth-oauth.h"
    + #include "libpq-fe.h"
    + 
    + #ifdef ENABLE_GSS
     @@ src/interfaces/libpq/fe-auth.c: pg_SSPI_startup(PGconn *conn, int use_negotiate, int payloadlen)
       * Initialize SASL authentication exchange.
       */
    @@ src/interfaces/libpq/fe-auth.c: PQchangePassword(PGconn *conn, const char *user,
     +}
     +
     +int
    -+PQdefaultAuthDataHook(PGAuthData type, PGconn *conn, void *data)
    ++PQdefaultAuthDataHook(PGauthData type, PGconn *conn, void *data)
     +{
     +	return 0;					/* handle nothing */
     +}
    @@ src/interfaces/libpq/fe-auth.h
      extern char *pg_fe_getusername(uid_t user_id, PQExpBuffer errorMessage);
      extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
      
    -@@ src/interfaces/libpq/fe-auth.h: extern char *pg_fe_scram_build_secret(const char *password,
    - 									  int iterations,
    - 									  const char **errstr);
    - 
    -+/* Mechanisms in fe-auth-oauth.c */
    -+extern const pg_fe_sasl_mech pg_oauth_mech;
    -+
    - #endif							/* FE_AUTH_H */
     
      ## src/interfaces/libpq/fe-connect.c ##
    +@@
    + #include "common/scram-common.h"
    + #include "common/string.h"
    + #include "fe-auth.h"
    ++#include "fe-auth-oauth.h"
    + #include "libpq-fe.h"
    + #include "libpq-int.h"
    + #include "mb/pg_wchar.h"
     @@ src/interfaces/libpq/fe-connect.c: static const internalPQconninfoOption PQconninfoOptions[] = {
      		"Load-Balance-Hosts", "", 8,	/* sizeof("disable") = 8 */
      	offsetof(struct pg_conn, load_balance_hosts)},
    @@ src/interfaces/libpq/libpq-fe.h: typedef enum
     +	PQAUTHDATA_PROMPT_OAUTH_DEVICE, /* user must visit a device-authorization
     +									 * URL */
     +	PQAUTHDATA_OAUTH_BEARER_TOKEN,	/* server requests an OAuth Bearer token */
    -+} PGAuthData;
    ++} PGauthData;
     +
      /* PGconn encapsulates a connection to the backend.
       * The contents of this struct are not supposed to be known to applications.
    @@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
      
      /* === in fe-auth.c === */
      
    -+typedef struct _PQpromptOAuthDevice
    ++typedef struct _PGpromptOAuthDevice
     +{
     +	const char *verification_uri;	/* verification URI to visit */
     +	const char *user_code;		/* user code to enter */
    -+} PQpromptOAuthDevice;
    ++} PGpromptOAuthDevice;
     +
    -+/* for _PQoauthBearerRequest.async() */
    ++/* for PGoauthBearerRequest.async() */
     +#ifdef WIN32
     +#define SOCKTYPE SOCKET
     +#else
     +#define SOCKTYPE int
     +#endif
     +
    -+typedef struct _PQoauthBearerRequest
    ++typedef struct _PGoauthBearerRequest
     +{
     +	/* Hook inputs (constant across all calls) */
     +	const char *const openid_configuration; /* OIDC discovery URI */
    @@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
     +	 * request->token must be set by the hook.
     +	 */
     +	PostgresPollingStatusType (*async) (PGconn *conn,
    -+										struct _PQoauthBearerRequest *request,
    ++										struct _PGoauthBearerRequest *request,
     +										SOCKTYPE *altsock);
     +
     +	/*
    @@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
     +	 * This is technically optional, but highly recommended, because there is
     +	 * no other indication as to when it is safe to free the token.
     +	 */
    -+	void		(*cleanup) (PGconn *conn, struct _PQoauthBearerRequest *request);
    ++	void		(*cleanup) (PGconn *conn, struct _PGoauthBearerRequest *request);
     +
     +	/*
     +	 * The hook should set this to the Bearer token contents for the
    @@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
     +	 * the cleanup callback.
     +	 */
     +	void	   *user;
    -+} PQoauthBearerRequest;
    ++} PGoauthBearerRequest;
     +
     +#undef SOCKTYPE
     +
    @@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
      extern char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm);
      extern PGresult *PQchangePassword(PGconn *conn, const char *user, const char *passwd);
      
    -+typedef int (*PQauthDataHook_type) (PGAuthData type, PGconn *conn, void *data);
    ++typedef int (*PQauthDataHook_type) (PGauthData type, PGconn *conn, void *data);
     +extern void PQsetAuthDataHook(PQauthDataHook_type hook);
     +extern PQauthDataHook_type PQgetAuthDataHook(void);
    -+extern int	PQdefaultAuthDataHook(PGAuthData type, PGconn *conn, void *data);
    ++extern int	PQdefaultAuthDataHook(PGauthData type, PGconn *conn, void *data);
     +
      /* === in encnames.c === */
      
      extern int	pg_char_to_encoding(const char *name);
     
      ## src/interfaces/libpq/libpq-int.h ##
    -@@ src/interfaces/libpq/libpq-int.h: typedef struct pg_conn_host
    - 								 * found in password file. */
    - } pg_conn_host;
    - 
    -+typedef PostgresPollingStatusType (*AsyncAuthFunc) (PGconn *conn, pgsocket *altsock);
    -+
    - /*
    -  * PGconn stores all the state data associated with a single connection
    -  * to a backend.
     @@ src/interfaces/libpq/libpq-int.h: struct pg_conn
      								 * cancel request, instead of being a normal
      								 * connection that's used for queries */
    @@ src/interfaces/libpq/libpq-int.h: struct pg_conn
      										 * know which auth response we're
      										 * sending */
      
    -+	AsyncAuthFunc async_auth;	/* callback for external async authentication */
    ++	/* Callback for external async authentication */
    ++	PostgresPollingStatusType (*async_auth) (PGconn *conn, pgsocket *altsock);
     +	pgsocket	altsock;		/* alternative socket for client to poll */
     +
     +
    @@ src/test/modules/oauth_validator/Makefile (new)
     +
     +endif
     
    + ## src/test/modules/oauth_validator/README (new) ##
    +@@
    ++Test programs and libraries for OAuth
    ++-------------------------------------
    ++
    ++This folder contains tests for the client- and server-side OAuth
    ++implementations. Most tests are run end-to-end to test both simultaneously. The
    ++tests in t/001_server use a mock OAuth authorization server, implemented jointly
    ++by t/OAuth/Server.pm and t/oauth_server.py, to run the libpq Device
    ++Authorization flow. The tests in t/002_client exercise custom OAuth flows and
    ++don't need an authorization server.
    ++
    ++Tests in this folder generally require 'oauth' to be present in PG_TEST_EXTRA,
    ++since localhost HTTP servers will be started. A Python installation is required
    ++to run the mock authorization server.
    +
      ## src/test/modules/oauth_validator/fail_validator.c (new) ##
     @@
     +/*-------------------------------------------------------------------------
    @@ src/test/modules/oauth_validator/fail_validator.c (new)
     +										 const char *token,
     +										 const char *role);
     +
    ++/* Callback implementations (we only need the main one) */
     +static const OAuthValidatorCallbacks validator_callbacks = {
     +	.validate_cb = fail_token,
     +};
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
     +/*-------------------------------------------------------------------------
     + *
     + * oauth_hook_client.c
    -+ *		Verify OAuth hook functionality in libpq
    ++ *		Test driver for t/002_client.pl, which verifies OAuth hook
    ++ *		functionality in libpq.
     + *
     + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
     + * Portions Copyright (c) 1994, Regents of the University of California
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
     +#include "getopt_long.h"
     +#include "libpq-fe.h"
     +
    -+static int	handle_auth_data(PGAuthData type, PGconn *conn, void *data);
    ++static int	handle_auth_data(PGauthData type, PGconn *conn, void *data);
     +
     +static void
     +usage(char *argv[])
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
     +	return 0;
     +}
     +
    ++/*
    ++ * PQauthDataHook implementation. Replaces the default client flow by handling
    ++ * PQAUTHDATA_OAUTH_BEARER_TOKEN.
    ++ */
     +static int
    -+handle_auth_data(PGAuthData type, PGconn *conn, void *data)
    ++handle_auth_data(PGauthData type, PGconn *conn, void *data)
     +{
    -+	PQoauthBearerRequest *req = data;
    ++	PGoauthBearerRequest *req = data;
     +
     +	if (no_hook || (type != PQAUTHDATA_OAUTH_BEARER_TOKEN))
     +		return 0;
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
      ## src/test/modules/oauth_validator/t/001_server.pl (new) ##
     @@
     +
    ++#
    ++# Tests the libpq builtin OAuth flow, as well as server-side HBA and validator
    ++# setup.
    ++#
     +# Copyright (c) 2021-2024, PostgreSQL Global Development Group
    ++#
     +
     +use strict;
     +use warnings FATAL => 'all';
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +use MIME::Base64 qw(encode_base64);
     +use PostgreSQL::Test::Cluster;
     +use PostgreSQL::Test::Utils;
    -+use PostgreSQL::Test::OAuthServer;
     +use Test::More;
     +
    ++use FindBin;
    ++use lib $FindBin::RealBin;
    ++
    ++use OAuth::Server;
    ++
     +if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\boauth\b/)
     +{
     +	plan skip_all =>
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +# Save a background connection for later configuration changes.
     +my $bgconn = $node->background_psql('postgres');
     +
    -+my $webserver = PostgreSQL::Test::OAuthServer->new();
    ++my $webserver = OAuth::Server->new();
     +$webserver->run();
     +
     +END
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +# things up; hardcode the discovery URI. (Scope is hardcoded to empty to cover
     +# that case as well.)
     +$common_connstr =
    -+  "user=test dbname=postgres oauth_issuer=$issuer/.well-known/openid-configuration oauth_scope=''";
    ++  "dbname=postgres oauth_issuer=$issuer/.well-known/openid-configuration oauth_scope='' oauth_client_id=f02c6361-0635";
     +
     +$bgconn->query_safe("ALTER SYSTEM SET oauth_validator.authn_id TO ''");
     +$node->reload;
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +  $node->wait_for_log(qr/reloading configuration files/, $log_start);
     +
     +if ($node->connect_fails(
    -+		"$common_connstr oauth_client_id=f02c6361-0635",
    ++		"$common_connstr user=test",
     +		"validator must set authn_id",
     +		expected_stderr => qr/OAuth bearer authentication failed/))
     +{
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +	$log_start = $log_end;
     +}
     +
    ++#
    ++# Test user mapping.
    ++#
    ++
    ++# Allow "user@example.com" to log in under the test role.
    ++unlink($node->data_dir . '/pg_ident.conf');
    ++$node->append_conf(
    ++	'pg_ident.conf', qq{
    ++oauthmap	user\@example.com	test
    ++});
    ++
    ++# test and testalt use the map; testparam uses ident delegation.
    ++unlink($node->data_dir . '/pg_hba.conf');
    ++$node->append_conf(
    ++	'pg_hba.conf', qq{
    ++local all test      oauth issuer="$issuer" scope="" map=oauthmap
    ++local all testalt   oauth issuer="$issuer" scope="" map=oauthmap
    ++local all testparam oauth issuer="$issuer" scope="" delegate_ident_mapping=1
    ++});
    ++
    ++# To start, have the validator use the role names as authn IDs.
    ++$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authn_id");
    ++
    ++$node->reload;
    ++$log_start =
    ++  $node->wait_for_log(qr/reloading configuration files/, $log_start);
    ++
    ++# The test and testalt roles should no longer map correctly.
    ++$node->connect_fails(
    ++	"$common_connstr user=test",
    ++	"mismatched username map (test)",
    ++	expected_stderr => qr/OAuth bearer authentication failed/);
    ++$node->connect_fails(
    ++	"$common_connstr user=testalt",
    ++	"mismatched username map (testalt)",
    ++	expected_stderr => qr/OAuth bearer authentication failed/);
    ++
    ++# Have the validator identify the end user as user@example.com.
    ++$bgconn->query_safe(
    ++	"ALTER SYSTEM SET oauth_validator.authn_id TO 'user\@example.com'");
    ++$node->reload;
    ++$log_start =
    ++  $node->wait_for_log(qr/reloading configuration files/, $log_start);
    ++
    ++# Now the test role can be logged into. (testalt still can't be mapped.)
    ++$node->connect_ok(
    ++	"$common_connstr user=test",
    ++	"matched username map (test)",
    ++	expected_stderr =>
    ++	  qr@Visit https://example\.com/ and enter the code: postgresuser@);
    ++$node->connect_fails(
    ++	"$common_connstr user=testalt",
    ++	"mismatched username map (testalt)",
    ++	expected_stderr => qr/OAuth bearer authentication failed/);
    ++
    ++# testparam ignores the map entirely.
    ++$node->connect_ok(
    ++	"$common_connstr user=testparam",
    ++	"delegated ident (testparam)",
    ++	expected_stderr =>
    ++	  qr@Visit https://example\.com/ and enter the code: postgresuser@);
    ++
     +$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authn_id");
     +$node->reload;
     +$log_start =
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     
      ## src/test/modules/oauth_validator/t/002_client.pl (new) ##
     @@
    -+
    ++#
    ++# Exercises the API for custom OAuth client flows, using the oauth_hook_client
    ++# test driver.
    ++#
     +# Copyright (c) 2021-2024, PostgreSQL Global Development Group
    ++#
     +
     +use strict;
     +use warnings FATAL => 'all';
    @@ src/test/modules/oauth_validator/t/002_client.pl (new)
     +	}
     +
     +	my @cmd = ("oauth_hook_client", @{$flags}, $common_connstr);
    -+	diag "running '" . join("' '", @cmd) . "'";
    ++	note "running '" . join("' '", @cmd) . "'";
     +
     +	my ($stdout, $stderr) = run_command(\@cmd);
     +
    @@ src/test/modules/oauth_validator/t/002_client.pl (new)
     +
     +done_testing();
     
    + ## src/test/modules/oauth_validator/t/OAuth/Server.pm (new) ##
    +@@
    ++
    ++# Copyright (c) 2024, PostgreSQL Global Development Group
    ++
    ++=pod
    ++
    ++=head1 NAME
    ++
    ++OAuth::Server - runs a mock OAuth authorization server for testing
    ++
    ++=head1 SYNOPSIS
    ++
    ++  use OAuth::Server;
    ++
    ++  my $server = OAuth::Server->new();
    ++  $server->run;
    ++
    ++  my $port = $server->port;
    ++  my $issuer = "http://localhost:$port";
    ++
    ++  # test against $issuer...
    ++
    ++  $server->stop;
    ++
    ++=head1 DESCRIPTION
    ++
    ++This is glue API between the Perl tests and the Python authorization server
    ++daemon implemented in t/oauth_server.py. (Python has a fairly usable HTTP server
    ++in its standard library, so the implementation was ported from Perl.)
    ++
    ++This authorization server does not use TLS (it implements a nonstandard, unsafe
    ++issuer at "http://localhost:<port>"), so libpq in particular will need to set
    ++PGOAUTHDEBUG=UNSAFE to be able to talk to it.
    ++
    ++=cut
    ++
    ++package OAuth::Server;
    ++
    ++use warnings;
    ++use strict;
    ++use Scalar::Util;
    ++use Test::More;
    ++
    ++=pod
    ++
    ++=head1 METHODS
    ++
    ++=over
    ++
    ++=item SSL::Server->new()
    ++
    ++Create a new OAuth Server object.
    ++
    ++=cut
    ++
    ++sub new
    ++{
    ++	my $class = shift;
    ++
    ++	my $self = {};
    ++	bless($self, $class);
    ++
    ++	return $self;
    ++}
    ++
    ++=pod
    ++
    ++=item $server->port()
    ++
    ++Returns the port in use by the server.
    ++
    ++=cut
    ++
    ++sub port
    ++{
    ++	my $self = shift;
    ++
    ++	return $self->{'port'};
    ++}
    ++
    ++=pod
    ++
    ++=item $server->run()
    ++
    ++Runs the authorization server daemon in t/oauth_server.py.
    ++
    ++=cut
    ++
    ++sub run
    ++{
    ++	my $self = shift;
    ++	my $port;
    ++
    ++	my $pid = open(my $read_fh, "-|", $ENV{PYTHON}, "t/oauth_server.py")
    ++	  or die "failed to start OAuth server: $!";
    ++
    ++	# Get the port number from the daemon. It closes stdout afterwards; that way
    ++	# we can slurp in the entire contents here rather than worrying about the
    ++	# number of bytes to read.
    ++	$port = do { local $/ = undef; <$read_fh> }
    ++	  // die "failed to read port number: $!";
    ++	chomp $port;
    ++	die "server did not advertise a valid port"
    ++	  unless Scalar::Util::looks_like_number($port);
    ++
    ++	$self->{'pid'} = $pid;
    ++	$self->{'port'} = $port;
    ++	$self->{'child'} = $read_fh;
    ++
    ++	note("OAuth provider (PID $pid) is listening on port $port\n");
    ++}
    ++
    ++=pod
    ++
    ++=item $server->stop()
    ++
    ++Sends SIGTERM to the authorization server and waits for it to exit.
    ++
    ++=cut
    ++
    ++sub stop
    ++{
    ++	my $self = shift;
    ++
    ++	note("Sending SIGTERM to OAuth provider PID: $self->{'pid'}\n");
    ++
    ++	kill(15, $self->{'pid'});
    ++	$self->{'pid'} = undef;
    ++
    ++	# Closing the popen() handle waits for the process to exit.
    ++	close($self->{'child'});
    ++	$self->{'child'} = undef;
    ++}
    ++
    ++=pod
    ++
    ++=back
    ++
    ++=cut
    ++
    ++1;
    +
      ## src/test/modules/oauth_validator/t/oauth_server.py (new) ##
     @@
     +#! /usr/bin/env python3
    ++#
    ++# A mock OAuth authorization server, designed to be invoked from
    ++# OAuth/Server.pm. This listens on an ephemeral port number (printed to stdout
    ++# so that the Perl tests can contact it) and runs as a daemon until it is
    ++# signaled.
    ++#
     +
     +import base64
     +import http.server
    @@ src/test/modules/oauth_validator/t/oauth_server.py (new)
     +
     +
     +class OAuthHandler(http.server.BaseHTTPRequestHandler):
    ++    """
    ++    Core implementation of the authorization server. The API is
    ++    inheritance-based, with entry points at do_GET() and do_POST(). See the
    ++    documentation for BaseHTTPRequestHandler.
    ++    """
    ++
     +    JsonObject = dict[str, object]  # TypeAlias is not available until 3.10
     +
     +    def _check_issuer(self):
    @@ src/test/modules/oauth_validator/t/oauth_server.py (new)
     +
     +
     +def main():
    ++    """
    ++    Starts the authorization server on localhost. The ephemeral port in use will
    ++    be printed to stdout.
    ++    """
    ++
     +    s = http.server.HTTPServer(("127.0.0.1", 0), OAuthHandler)
     +
     +    # Attach a "cache" dictionary to the server to allow the OAuthHandlers to
    @@ src/test/modules/oauth_validator/t/oauth_server.py (new)
     +    port = s.socket.getsockname()[1]
     +    print(port)
     +
    ++    # stdout is closed to allow the parent to just "read to the end".
     +    stdout = sys.stdout.fileno()
     +    sys.stdout.close()
     +    os.close(stdout)
    @@ src/test/modules/oauth_validator/validator.c (new)
     +											 const char *token,
     +											 const char *role);
     +
    ++/* Callback implementations (exercise all three) */
     +static const OAuthValidatorCallbacks validator_callbacks = {
     +	.startup_cb = validator_startup,
     +	.shutdown_cb = validator_shutdown,
    @@ src/test/modules/oauth_validator/validator.c (new)
     +
     +static char *authn_id = NULL;
     +
    ++/*---
    ++ * Extension entry point. Sets up GUCs for use by tests:
    ++ *
    ++ * - oauth_validator.authn_id	Sets the user identifier to return during token
    ++ *								validation. Defaults to the username in the
    ++ *								startup packet.
    ++ */
     +void
     +_PG_init(void)
     +{
    @@ src/test/modules/oauth_validator/validator.c (new)
     +	MarkGUCPrefixReserved("oauth_validator");
     +}
     +
    ++/*
    ++ * Validator module entry point.
    ++ */
     +const OAuthValidatorCallbacks *
     +_PG_oauth_validator_module_init(void)
     +{
    @@ src/test/modules/oauth_validator/validator.c (new)
     +
     +#define PRIVATE_COOKIE ((void *) 13579)
     +
    ++/*
    ++ * Startup callback, to set up private data for the validator.
    ++ */
     +static void
     +validator_startup(ValidatorModuleState *state)
     +{
     +	state->private_data = PRIVATE_COOKIE;
     +}
     +
    ++/*
    ++ * Shutdown callback, to tear down the validator.
    ++ */
     +static void
     +validator_shutdown(ValidatorModuleState *state)
     +{
    @@ src/test/modules/oauth_validator/validator.c (new)
     +			 state->private_data);
     +}
     +
    ++/*
    ++ * Validator implementation. Logs the incoming data and authorizes the token;
    ++ * the behavior can be modified via the module's GUC settings.
    ++ */
     +static ValidatorModuleResult *
     +validate_token(ValidatorModuleState *state, const char *token, const char *role)
     +{
    @@ src/test/perl/PostgreSQL/Test/Cluster.pm: sub connect_ok
      	$self->log_check($test_name, $log_location, %params);
      }
     
    - ## src/test/perl/PostgreSQL/Test/OAuthServer.pm (new) ##
    -@@
    -+#!/usr/bin/perl
    -+
    -+package PostgreSQL::Test::OAuthServer;
    -+
    -+use warnings;
    -+use strict;
    -+use Scalar::Util;
    -+use Socket;
    -+use IO::Select;
    -+use Test::More;
    -+
    -+local *server_socket;
    -+
    -+sub new
    -+{
    -+	my $class = shift;
    -+
    -+	my $self = {};
    -+	bless($self, $class);
    -+
    -+	return $self;
    -+}
    -+
    -+sub port
    -+{
    -+	my $self = shift;
    -+
    -+	return $self->{'port'};
    -+}
    -+
    -+sub run
    -+{
    -+	my $self = shift;
    -+	my $port;
    -+
    -+	my $pid = open(my $read_fh, "-|", $ENV{PYTHON}, "t/oauth_server.py")
    -+	  or die "failed to start OAuth server: $!";
    -+
    -+	read($read_fh, $port, 7) // die "failed to read port number: $!";
    -+	chomp $port;
    -+	die "server did not advertise a valid port"
    -+	  unless Scalar::Util::looks_like_number($port);
    -+
    -+	$self->{'pid'} = $pid;
    -+	$self->{'port'} = $port;
    -+	$self->{'child'} = $read_fh;
    -+
    -+	note("OAuth provider (PID $pid) is listening on port $port\n");
    -+}
    -+
    -+sub stop
    -+{
    -+	my $self = shift;
    -+
    -+	note("Sending SIGTERM to OAuth provider PID: $self->{'pid'}\n");
    -+
    -+	kill(15, $self->{'pid'});
    -+	$self->{'pid'} = undef;
    -+
    -+	# Closing the popen() handle waits for the process to exit.
    -+	close($self->{'child'});
    -+	$self->{'child'} = undef;
    -+}
    -+
    -+1;
    -
      ## src/tools/pgindent/pgindent ##
     @@ src/tools/pgindent/pgindent: sub pre_indent
      	# Protect wrapping in CATALOG()
    @@ src/tools/pgindent/pgindent: sub post_indent
      	# Undo change of dash-protected block comments
     
      ## src/tools/pgindent/typedefs.list ##
    -@@ src/tools/pgindent/typedefs.list: ArrayMetaState
    - ArraySubWorkspace
    - ArrayToken
    - ArrayType
    -+AsyncAuthFunc
    - AsyncQueueControl
    - AsyncQueueEntry
    - AsyncRequest
     @@ src/tools/pgindent/typedefs.list: CState
      CTECycleClause
      CTEMaterialize
    @@ src/tools/pgindent/typedefs.list: NumericDigit
      NumericSortSupport
      NumericSumAccum
      NumericVar
    -+OAuthStep
     +OAuthValidatorCallbacks
      OM_uint32
      OP
      OSAPerGroupState
    -@@ src/tools/pgindent/typedefs.list: PFN
    - PGAlignedBlock
    - PGAlignedXLogBlock
    - PGAsyncStatusType
    -+PGAuthData
    - PGCALL2
    - PGChecksummablePage
    - PGContextVisibility
    +@@ src/tools/pgindent/typedefs.list: PGVerbosity
    + PG_Locale_Strategy
    + PG_Lock_Status
    + PG_init_t
    ++PGauthData
    + PGcancel
    + PGcancelConn
    + PGcmdQueueEntry
    +@@ src/tools/pgindent/typedefs.list: PGconn
    + PGdataValue
    + PGlobjfuncs
    + PGnotify
    ++PGoauthBearerRequest
    + PGpipelineStatus
    ++PGpromptOAuthDevice
    + PGresAttDesc
    + PGresAttValue
    + PGresParamDesc
     @@ src/tools/pgindent/typedefs.list: PQArgBlock
      PQEnvironmentOption
      PQExpBuffer
    @@ src/tools/pgindent/typedefs.list: PQArgBlock
      PQcommMethods
      PQconninfoOption
      PQnoticeProcessor
    - PQnoticeReceiver
    -+PQoauthBearerRequest
    - PQprintOpt
    -+PQpromptOAuthDevice
    - PQsslKeyPassHook_OpenSSL_type
    - PREDICATELOCK
    - PREDICATELOCKTAG
     @@ src/tools/pgindent/typedefs.list: VacuumRelation
      VacuumStmt
      ValidIOData
    @@ src/tools/pgindent/typedefs.list: explain_get_index_name_hook_type
      fasthash_state
      fd_set
     +fe_oauth_state
    -+fe_oauth_state_enum
      fe_scram_state
      fe_scram_state_enum
      fetch_range_request
2:  28cc3463aaf ! 2:  566d90d30a7 DO NOT MERGE: Add pytest suite for OAuth
    @@ src/test/python/client/test_oauth.py (new)
     +PGRES_POLLING_OK = 3
     +
     +
    -+class PQPromptOAuthDevice(ctypes.Structure):
    ++class PGPromptOAuthDevice(ctypes.Structure):
     +    _fields_ = [
     +        ("verification_uri", ctypes.c_char_p),
     +        ("user_code", ctypes.c_char_p),
     +    ]
     +
     +
    -+class PQOAuthBearerRequest(ctypes.Structure):
    ++class PGOAuthBearerRequest(ctypes.Structure):
     +    pass
     +
     +
    -+PQOAuthBearerRequest._fields_ = [
    ++PGOAuthBearerRequest._fields_ = [
     +    ("openid_configuration", ctypes.c_char_p),
     +    ("scope", ctypes.c_char_p),
     +    (
    @@ src/test/python/client/test_oauth.py (new)
     +        ctypes.CFUNCTYPE(
     +            ctypes.c_int,
     +            ctypes.c_void_p,
    -+            ctypes.POINTER(PQOAuthBearerRequest),
    ++            ctypes.POINTER(PGOAuthBearerRequest),
     +            ctypes.POINTER(ctypes.c_int),
     +        ),
     +    ),
     +    (
     +        "cleanup",
    -+        ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PQOAuthBearerRequest)),
    ++        ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PGOAuthBearerRequest)),
     +    ),
     +    ("token", ctypes.c_char_p),
     +    ("user", ctypes.c_void_p),
    @@ src/test/python/client/test_oauth.py (new)
     +        handle_by_default = 0  # does an implementation have to be provided?
     +
     +        if typ == PQAUTHDATA_PROMPT_OAUTH_DEVICE:
    -+            cls = PQPromptOAuthDevice
    ++            cls = PGPromptOAuthDevice
     +            handle_by_default = 1
     +        elif typ == PQAUTHDATA_OAUTH_BEARER_TOKEN:
    -+            cls = PQOAuthBearerRequest
    ++            cls = PGOAuthBearerRequest
     +        else:
     +            return 0
     +
    @@ src/test/python/client/test_oauth.py (new)
     +    @ctypes.CFUNCTYPE(
     +        ctypes.c_int,
     +        ctypes.c_void_p,
    -+        ctypes.POINTER(PQOAuthBearerRequest),
    ++        ctypes.POINTER(PGOAuthBearerRequest),
     +        ctypes.POINTER(ctypes.c_int),
     +    )
     +    def get_token_wrapper(pgconn, p_request, p_altsock):
    @@ src/test/python/client/test_oauth.py (new)
     +            logging.error("Exception during async callback:\n" + traceback.format_exc())
     +            return PGRES_POLLING_FAILED
     +
    -+    @ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PQOAuthBearerRequest))
    ++    @ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PGOAuthBearerRequest))
     +    def cleanup(pgconn, p_request):
     +        """
     +        Should be called exactly once per connection.
    @@ src/test/python/server/test_oauth.py (new)
     +    ctx = Context()
     +    hba_lines = [
     +        f'host {ctx.dbname} {ctx.map_user}   samehost oauth issuer="{ctx.issuer}" scope="{ctx.scope}" map=oauth\n',
    -+        f'host {ctx.dbname} {ctx.authz_user} samehost oauth issuer="{ctx.issuer}" scope="{ctx.scope}" trust_validator_authz=1\n',
    ++        f'host {ctx.dbname} {ctx.authz_user} samehost oauth issuer="{ctx.issuer}" scope="{ctx.scope}" delegate_ident_mapping=1\n',
     +        f'host {ctx.dbname} all              samehost oauth issuer="{ctx.issuer}" scope="{ctx.scope}"\n',
     +    ]
     +    ident_lines = [r"oauth /^(.*)@example\.com$ \1"]