since-v40.diff.txt

text/plain

Filename: since-v40.diff.txt
Type: text/plain
Part: 0
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER
-:  ----------- > 1:  386e7c4df31 Move PG_MAX_AUTH_TOKEN_LENGTH to libpq/auth.h
2:  de155343c81 ! 2:  b829f7a8ac7 squash! Add OAUTHBEARER SASL mechanism
    @@ Metadata
     Author: Jacob Champion <jacob.champion@enterprisedb.com>
     
      ## Commit message ##
    -    squash! Add OAUTHBEARER SASL mechanism
    +    require_auth: prepare for multiple SASL mechanisms
     
    -    Add require_auth=oauth support.
    +    Prior to this patch, the require_auth implementation assumed that the
    +    AuthenticationSASL protocol message was synonymous with SCRAM-SHA-256.
    +    In preparation for the OAUTHBEARER SASL mechanism, split the
    +    implementation into two tiers: the first checks the acceptable
    +    AUTH_REQ_* codes, and the second checks acceptable mechanisms if
    +    AUTH_REQ_SASL et al are permitted.
     
    - ## doc/src/sgml/libpq.sgml ##
    -@@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
    -           </listitem>
    -          </varlistentry>
    - 
    -+         <varlistentry>
    -+          <term><literal>oauth</literal></term>
    -+          <listitem>
    -+           <para>
    -+            The server must request an OAuth bearer token from the client.
    -+           </para>
    -+          </listitem>
    -+         </varlistentry>
    -+
    -          <varlistentry>
    -           <term><literal>none</literal></term>
    -           <listitem>
    +    conn->allowed_sasl_mechs is the list of pointers to acceptable
    +    mechanisms. (Since we'll support only a small number of mechanisms, this
    +    is an array of static length to minimize bookkeeping.) pg_SASL_init()
    +    will bail if the selected mechanism isn't contained in this array.
     
    - ## src/interfaces/libpq/fe-auth-oauth.c ##
    -@@ src/interfaces/libpq/fe-auth-oauth.c: oauth_exchange(void *opaq, bool final,
    - 			*outputlen = strlen(*output);
    - 			state->step = FE_OAUTH_BEARER_SENT;
    - 
    -+			/*
    -+			 * For the purposes of require_auth, our side of authentication is
    -+			 * done at this point; the server will either accept the
    -+			 * connection or send an error. Unlike SCRAM, there is no
    -+			 * additional server data to check upon success.
    -+			 */
    -+			conn->client_finished_auth = true;
    - 			return SASL_CONTINUE;
    - 
    - 		case FE_OAUTH_BEARER_SENT:
    +    Since there's only one mechansism supported right now, one branch of the
    +    second tier cannot be exercised yet (it's marked with Assert(false)).
    +    This assertion will need to be removed when the next mechanism is added.
     
      ## src/interfaces/libpq/fe-auth.c ##
    -@@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen, bool *async)
    +@@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
      		goto error;
      	}
      
    @@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen, bool
     +
     +		if (!allowed)
     +		{
    ++			/*
    ++			 * TODO: this is dead code until a second SASL mechanism is added;
    ++			 * the connection can't have proceeded past check_expected_areq()
    ++			 * if no SASL methods are allowed.
    ++			 */
    ++			Assert(false);
    ++
     +			libpq_append_conn_error(conn, "authentication method requirement \"%s\" failed: server requested %s authentication",
     +									conn->require_auth, selected_mechanism);
     +			goto error;
    @@ src/interfaces/libpq/fe-connect.c: libpq_prng_init(PGconn *conn)
     +fill_allowed_sasl_mechs(PGconn *conn)
     +{
     +	/*---
    -+	 * We only support two mechanisms at the moment, so rather than deal with a
    ++	 * We only support one mechanism at the moment, so rather than deal with a
     +	 * linked list, conn->allowed_sasl_mechs is an array of static length. We
     +	 * rely on the compile-time assertion here to keep us honest.
     +	 *
    @@ src/interfaces/libpq/fe-connect.c: libpq_prng_init(PGconn *conn)
     +	 * - handle the new mechanism name in the require_auth portion of
     +	 *   pqConnectOptions2(), below.
     +	 */
    -+	StaticAssertDecl(lengthof(conn->allowed_sasl_mechs) == 2,
    ++	StaticAssertDecl(lengthof(conn->allowed_sasl_mechs) == 1,
     +					 "fill_allowed_sasl_mechs() must be updated when resizing conn->allowed_sasl_mechs[]");
     +
     +	conn->allowed_sasl_mechs[0] = &pg_scram_mech;
    -+	conn->allowed_sasl_mechs[1] = &pg_oauth_mech;
     +}
     +
     +/*
    @@ src/interfaces/libpq/fe-connect.c: pqConnectOptions2(PGconn *conn)
     -				bits |= (1 << AUTH_REQ_SASL_FIN);
     +				mech = &pg_scram_mech;
      			}
    -+			else if (strcmp(method, "oauth") == 0)
    -+			{
    -+				mech = &pg_oauth_mech;
    -+			}
     +
     +			/*
     +			 * Final group: meta-options.
    @@ src/interfaces/libpq/fe-connect.c: pqConnectOptions2(PGconn *conn)
     +				 * updated for SASL further down.
     +				 */
     +				Assert(!bits);
    - 
    --				conn->allowed_auth_methods &= ~bits;
    ++
     +				if (negated)
     +				{
     +					/* Remove the existing mechanism from the list. */
     +					i = index_of_allowed_sasl_mech(conn, mech);
     +					if (i < 0)
     +						goto duplicate;
    -+
    + 
    +-				conn->allowed_auth_methods &= ~bits;
     +					conn->allowed_sasl_mechs[i] = NULL;
     +				}
     +				else
    @@ src/interfaces/libpq/fe-connect.c: pqConnectOptions2(PGconn *conn)
     -					goto duplicate;
     +				/* Update the method bitmask. */
     +				Assert(bits);
    - 
    --				conn->allowed_auth_methods |= bits;
    ++
     +				if (negated)
     +				{
     +					if ((conn->allowed_auth_methods & bits) == 0)
    @@ src/interfaces/libpq/fe-connect.c: pqConnectOptions2(PGconn *conn)
     +				{
     +					if ((conn->allowed_auth_methods & bits) == bits)
     +						goto duplicate;
    -+
    + 
    +-				conn->allowed_auth_methods |= bits;
     +					conn->allowed_auth_methods |= bits;
     +				}
      			}
    @@ src/interfaces/libpq/libpq-int.h: struct pg_conn
      								 * the server? */
      	uint32		allowed_auth_methods;	/* bitmask of acceptable AuthRequest
      										 * codes */
    -+	const pg_fe_sasl_mech *allowed_sasl_mechs[2];	/* and acceptable SASL
    ++	const pg_fe_sasl_mech *allowed_sasl_mechs[1];	/* and acceptable SASL
     +													 * mechanisms */
      	bool		client_finished_auth;	/* have we finished our half of the
      										 * authentication exchange? */
    @@ src/test/authentication/t/001_password.pl: $node->connect_fails(
      
      # Unknown value defined in require_auth.
      $node->connect_fails(
    -@@ src/test/authentication/t/001_password.pl: $node->connect_fails(
    - $node->connect_fails(
    - 	"user=scram_role require_auth=!scram-sha-256",
    - 	"SCRAM authentication forbidden, fails with SCRAM auth",
    --	expected_stderr => qr/server requested SASL authentication/);
    -+	expected_stderr => qr/server requested SCRAM-SHA-256 authentication/);
    - $node->connect_fails(
    - 	"user=scram_role require_auth=!password,!md5,!scram-sha-256",
    - 	"multiple authentication types forbidden, fails with SCRAM auth",
    --	expected_stderr => qr/server requested SASL authentication/);
    -+	expected_stderr => qr/server requested SCRAM-SHA-256 authentication/);
    - 
    - # Test that bad passwords are rejected.
    - $ENV{"PGPASSWORD"} = 'badpass';
    -@@ src/test/authentication/t/001_password.pl: $node->connect_fails(
    - 	"user=scram_role require_auth=!scram-sha-256",
    - 	"password authentication forbidden, fails with SCRAM auth",
    - 	expected_stderr =>
    --	  qr/authentication method requirement "!scram-sha-256" failed: server requested SASL authentication/
    -+	  qr/authentication method requirement "!scram-sha-256" failed: server requested SCRAM-SHA-256 authentication/
    - );
    - $node->connect_fails(
    - 	"user=scram_role require_auth=!password,!md5,!scram-sha-256",
    - 	"multiple authentication types forbidden, fails with SCRAM auth",
    - 	expected_stderr =>
    --	  qr/authentication method requirement "!password,!md5,!scram-sha-256" failed: server requested SASL authentication/
    -+	  qr/authentication method requirement "!password,!md5,!scram-sha-256" failed: server requested SCRAM-SHA-256 authentication/
    - );
    - 
    - # Test SYSTEM_USER <> NULL with parallel workers.
    -
    - ## src/test/modules/oauth_validator/t/001_server.pl ##
    -@@ src/test/modules/oauth_validator/t/001_server.pl: $node->connect_fails(
    - 	  qr@server's discovery document at \Q$issuer/.well-known/oauth-authorization-server/alternate\E \(issuer "\Q$issuer/alternate\E"\) is incompatible with oauth_issuer \(\Q$issuer\E\)@
    - );
    - 
    -+# Test require_auth settings against OAUTHBEARER.
    -+my @cases = (
    -+	{ require_auth => "oauth" },
    -+	{ require_auth => "oauth,scram-sha-256" },
    -+	{ require_auth => "password,oauth" },
    -+	{ require_auth => "none,oauth" },
    -+	{ require_auth => "!scram-sha-256" },
    -+	{ require_auth => "!none" },
    -+
    -+	{
    -+		require_auth => "!oauth",
    -+		failure => qr/server requested OAUTHBEARER authentication/
    -+	},
    -+	{
    -+		require_auth => "scram-sha-256",
    -+		failure => qr/server requested OAUTHBEARER authentication/
    -+	},
    -+	{
    -+		require_auth => "!password,!oauth",
    -+		failure => qr/server requested OAUTHBEARER authentication/
    -+	},
    -+	{
    -+		require_auth => "none",
    -+		failure => qr/server requested SASL authentication/
    -+	},
    -+	{
    -+		require_auth => "!oauth,!scram-sha-256",
    -+		failure => qr/server requested SASL authentication/
    -+	});
    -+
    -+$user = "test";
    -+foreach my $c (@cases)
    -+{
    -+	my $connstr =
    -+	  "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635 require_auth=$c->{'require_auth'}";
    -+
    -+	if (defined $c->{'failure'})
    -+	{
    -+		$node->connect_fails(
    -+			$connstr,
    -+			"require_auth=$c->{'require_auth'} fails",
    -+			expected_stderr => $c->{'failure'});
    -+	}
    -+	else
    -+	{
    -+		$node->connect_ok(
    -+			$connstr,
    -+			"require_auth=$c->{'require_auth'} succeeds",
    -+			expected_stderr =>
    -+			  qr@Visit https://example\.com/ and enter the code: postgresuser@
    -+		);
    -+	}
    -+}
    -+
    - # Make sure the client_id and secret are correctly encoded. $vschars contains
    - # every allowed character for a client_id/_secret (the "VSCHAR" class).
    - # $vschars_esc is additionally backslash-escaped for inclusion in a
    -@@ src/test/modules/oauth_validator/t/001_server.pl: my $vschars_esc =
    -   " !\"#\$%&\\'()*+,-./0123456789:;<=>?\@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
    - 
    - $node->connect_ok(
    --	"user=$user dbname=postgres oauth_issuer=$issuer/alternate oauth_client_id='$vschars_esc'",
    -+	"user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id='$vschars_esc'",
    - 	"escapable characters: client_id",
    - 	expected_stderr =>
    --	  qr@Visit https://example\.org/ and enter the code: postgresuser@);
    -+	  qr@Visit https://example\.com/ and enter the code: postgresuser@);
    - $node->connect_ok(
    --	"user=$user dbname=postgres oauth_issuer=$issuer/alternate oauth_client_id='$vschars_esc' oauth_client_secret='$vschars_esc'",
    -+	"user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id='$vschars_esc' oauth_client_secret='$vschars_esc'",
    - 	"escapable characters: client_id and secret",
    - 	expected_stderr =>
    --	  qr@Visit https://example\.org/ and enter the code: postgresuser@);
    -+	  qr@Visit https://example\.com/ and enter the code: postgresuser@);
    - 
    - #
    - # Further tests rely on support for specific behaviors in oauth_server.py. To
-:  ----------- > 3:  f88f98df97d libpq: handle asynchronous actions during SASL
1:  7ee8628abac ! 4:  d96712cda1d Add OAUTHBEARER SASL mechanism
    @@ .cirrus.tasks.yml: task:
      
        # NB: Intentionally build without -Dllvm. The freebsd image size is already
        # large enough to make VM startup slow, and even without llvm freebsd
    -@@ .cirrus.tasks.yml: task:
    -         --buildtype=debug \
    -         -Dcassert=true -Dinjection_points=true \
    -         -Duuid=bsd -Dtcl_version=tcl86 -Ddtrace=auto \
    -+        -Dlibcurl=enabled \
    -         -Dextra_lib_dirs=/usr/local/lib -Dextra_include_dirs=/usr/local/include/ \
    -         build
    -     EOF
     @@ .cirrus.tasks.yml: LINUX_CONFIGURE_FEATURES: &LINUX_CONFIGURE_FEATURES >-
        --with-gssapi
        --with-icu
    @@ .cirrus.tasks.yml: LINUX_CONFIGURE_FEATURES: &LINUX_CONFIGURE_FEATURES >-
        --with-libxml
        --with-libxslt
        --with-llvm
    -@@ .cirrus.tasks.yml: LINUX_CONFIGURE_FEATURES: &LINUX_CONFIGURE_FEATURES >-
    -   --with-zstd
    - 
    - LINUX_MESON_FEATURES: &LINUX_MESON_FEATURES >-
    -+  -Dlibcurl=enabled
    -   -Dllvm=enabled
    -   -Duuid=e2fs
    - 
     @@ .cirrus.tasks.yml: task:
          EOF
      
    @@ .cirrus.tasks.yml: task:
        ###
        # Test that code can be built with gcc/clang without warnings
     
    - ## config/programs.m4 ##
    -@@ config/programs.m4: if test "$pgac_cv_ldap_safe" != yes; then
    - *** also uses LDAP will crash on exit.])
    - fi])
    - 
    --
    --
    - # PGAC_CHECK_READLINE
    - # -------------------
    - # Check for the readline library and dependent libraries, either
    -
      ## configure ##
     @@ configure: XML2_LIBS
      XML2_CFLAGS
    @@ configure: Optional Packages:
                                prefer BSD Libedit over GNU Readline
        --with-uuid=LIB         build contrib/uuid-ossp using LIB (bsd,e2fs,ossp)
        --with-ossp-uuid        obsolete spelling of --with-uuid=ossp
    -+  --with-libcurl          build with libcurl support for OAuth client flows
    ++  --with-libcurl          build with libcurl support
        --with-libxml           build with XML support
        --with-libxslt          use XSLT support when building contrib/xml2
        --with-system-tzdata=DIR
    @@ configure: fi
     +#
     +# libcurl
     +#
    -+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with libcurl support for OAuth client flows" >&5
    -+$as_echo_n "checking whether to build with libcurl support for OAuth client flows... " >&6; }
    ++{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with libcurl support" >&5
    ++$as_echo_n "checking whether to build with libcurl support... " >&6; }
     +
     +
     +
    @@ configure.ac: fi
     +#
     +# libcurl
     +#
    -+AC_MSG_CHECKING([whether to build with libcurl support for OAuth client flows])
    -+PGAC_ARG_BOOL(with, libcurl, no, [build with libcurl support for OAuth client flows],
    -+              [AC_DEFINE([USE_LIBCURL], 1, [Define to 1 to build with libcurl support for OAuth client flows. (--with-libcurl)])])
    ++AC_MSG_CHECKING([whether to build with libcurl support])
    ++PGAC_ARG_BOOL(with, libcurl, no, [build with libcurl support],
    ++              [AC_DEFINE([USE_LIBCURL], 1, [Define to 1 to build with libcurl support. (--with-libcurl)])])
     +AC_MSG_RESULT([$with_libcurl])
     +AC_SUBST(with_libcurl)
     +
    @@ doc/src/sgml/client-auth.sgml: host ... radius radiusservers="server1,server2" r
     +     </varlistentry>
     +
     +     <varlistentry>
    -+      <term>Issuer</term>
    ++      <term id="auth-oauth-issuer">Issuer</term>
     +      <listitem>
     +       <para>
     +        An identifier for an authorization server, printed as an
    @@ doc/src/sgml/client-auth.sgml: host ... radius radiusservers="server1,server2" r
     +      <term><literal>issuer</literal></term>
     +      <listitem>
     +       <para>
    -+        The issuer identifier of the authorization server, as defined by its
    -+        discovery document, or a well-known URI pointing to that discovery
    -+        document. This parameter is required.
    ++        An HTTPS URL which is either the exact
    ++        <link linkend="auth-oauth-issuer">issuer identifier</link> of the
    ++        authorization server, as defined by its discovery document, or a
    ++        well-known URI that points directly to that discovery document. This
    ++        parameter is required.
     +       </para>
     +       <para>
    -+        When an OAuth client connects to the server, a discovery document URI
    -+        will be constructed using the issuer identifier. By default, the URI
    -+        uses the conventions of OpenID Connect Discovery: the path
    ++        When an OAuth client connects to the server, a URL for the discovery
    ++        document will be constructed using the issuer identifier. By default,
    ++        this URL uses the conventions of OpenID Connect Discovery: the path
     +        <literal>/.well-known/openid-configuration</literal> will be appended
    -+        to the issuer identifier. Alternatively, if the
    ++        to the end of the issuer identifier. Alternatively, if the
     +        <literal>issuer</literal> contains a <literal>/.well-known/</literal>
    -+        path segment, the URI will be provided to the client as-is.
    ++        path segment, that URL will be provided to the client as-is.
     +       </para>
     +       <warning>
     +        <para>
    @@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
     +      <term><literal>oauth_issuer</literal></term>
     +      <listitem>
     +       <para>
    -+        The HTTPS URL of an issuer to contact if the server requests an OAuth
    -+        token for the connection. This parameter is required for all OAuth
    ++        The HTTPS URL of a trusted issuer to contact if the server requests an
    ++        OAuth token for the connection. This parameter is required for all OAuth
     +        connections; it should exactly match the <literal>issuer</literal>
     +        setting in <link linkend="auth-oauth">the server's HBA configuration.</link>
     +       </para>
     +       <para>
     +        As part of the standard authentication handshake, <application>libpq</application>
    -+        will ask the server for a <emphasis>discovery document:</emphasis> a URI
    ++        will ask the server for a <emphasis>discovery document:</emphasis> a URL
     +        providing a set of OAuth configuration parameters. The server must
    -+        provide a URI that is directly constructed from the components of the
    ++        provide a URL 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
     +       <para>
     +        This standard handshake requires two separate network connections to the
     +        server per authentication attempt. To skip asking the server for a
    -+        discovery document URI, you may set <literal>oauth_issuer</literal> to a
    ++        discovery document URL, you may set <literal>oauth_issuer</literal> to a
     +        <literal>/.well-known/</literal> URI used for OAuth discovery. (In this
     +        case, it is recommended that
     +        <xref linkend="libpq-connect-oauth-scope"/> be set as well, since the
    @@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
     +         <listitem><para><literal>/.well-known/oauth-authorization-server</literal></para></listitem>
     +        </itemizedlist>
     +       </para>
    ++       <warning>
    ++        <para>
    ++         Issuers are highly privileged during the OAuth connection handshake. As
    ++         a rule of thumb, if you would not trust the operator of a URL to handle
    ++         access to your servers, or to impersonate you directly, that URL should
    ++         not be trusted as an <literal>oauth_issuer</literal>.
    ++        </para>
    ++       </warning>
     +      </listitem>
     +     </varlistentry>
     +
    @@ doc/src/sgml/libpq.sgml: void PQinitSSL(int do_ssl);
     +typedef struct _PGoauthBearerRequest
     +{
     +    /* Hook inputs (constant across all calls) */
    -+    const char *const openid_configuration; /* OIDC discovery URI */
    ++    const char *const openid_configuration; /* OIDC discovery URL */
     +    const char *const scope;                /* required scope(s), or NULL */
     +
     +    /* Hook outputs */
    @@ doc/src/sgml/oauth-validators.sgml (new)
     +  <sect2 id="oauth-validator-design-guidelines">
     +   <title>General Coding Guidelines</title>
     +   <para>
    -+    TODO
    ++    Developers should keep the following in mind when implementing token
    ++    validation:
     +   </para>
     +   <variablelist>
     +    <varlistentry>
    @@ doc/src/sgml/oauth-validators.sgml (new)
     +       <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.
    ++       this guidance may result in unresponsive backend sessions.
     +      </para>
     +     </listitem>
     +    </varlistentry>
    @@ doc/src/sgml/oauth-validators.sgml (new)
     +     <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.
    ++       documentation, but at minimum, negative testing should be considered
    ++       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>
    ++    <varlistentry>
    ++     <term>Documentation</term>
    ++     <listitem>
    ++      <para>
    ++       Validator implementations should document the contents and format of the
    ++       authenticated ID that is reported to the server for each end user, since
    ++       DBAs may need to use this information to construct pg_ident maps. (For
    ++       instance, is it an email address? an organizational ID number? a UUID?)
    ++       They should also document whether or not it is safe to use the module in
    ++       <symbol>delegate_ident_mapping=1</symbol> mode, and what additional
    ++       configuration is required in order to do so.
     +      </para>
     +     </listitem>
     +    </varlistentry>
    @@ meson_options.txt: option('icu', type: 'feature', value: 'auto',
        description: 'LDAP support')
      
     +option('libcurl', type : 'feature', value: 'auto',
    -+  description: 'libcurl support for OAuth client flows')
    ++  description: 'libcurl support')
     +
      option('libedit_preferred', type: 'boolean', value: false,
        description: 'Prefer BSD Libedit over GNU Readline')
    @@ src/backend/libpq/auth-oauth.c (new)
     +};
     +
     +/* Valid states for the oauth_exchange() machine. */
    -+typedef enum
    ++enum oauth_state
     +{
     +	OAUTH_STATE_INIT = 0,
     +	OAUTH_STATE_ERROR,
     +	OAUTH_STATE_FINISHED,
    -+} oauth_state;
    ++};
     +
     +/* Mechanism callback state. */
     +struct oauth_ctx
     +{
    -+	oauth_state state;
    ++	enum oauth_state state;
     +	Port	   *port;
     +	const char *issuer;
     +	const char *scope;
    @@ src/backend/libpq/auth-oauth.c (new)
     +										  token, port->user_name);
     +	if (ret == NULL)
     +	{
    -+		ereport(LOG, errmsg("Internal error in OAuth validator module"));
    ++		ereport(LOG, errmsg("internal error in OAuth validator module"));
     +		return false;
     +	}
     +
    @@ src/backend/libpq/auth.c
      
      
      /*----------------------------------------------------------------
    -@@ src/backend/libpq/auth.c: static int	CheckRADIUSAuth(Port *port);
    - static int	PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd);
    - 
    - 
    --/*
    -- * Maximum accepted size of GSS and SSPI authentication tokens.
    -- * We also use this as a limit on ordinary password packet lengths.
    -- *
    -- * Kerberos tickets are usually quite small, but the TGTs issued by Windows
    -- * domain controllers include an authorization field known as the Privilege
    -- * Attribute Certificate (PAC), which contains the user's Windows permissions
    -- * (group memberships etc.). The PAC is copied into all tickets obtained on
    -- * the basis of this TGT (even those issued by Unix realms which the Windows
    -- * realm trusts), and can be several kB in size. The maximum token size
    -- * accepted by Windows systems is determined by the MaxAuthToken Windows
    -- * registry setting. Microsoft recommends that it is not set higher than
    -- * 65535 bytes, so that seems like a reasonable limit for us as well.
    -- */
    --#define PG_MAX_AUTH_TOKEN_LENGTH	65535
    --
    - /*----------------------------------------------------------------
    -  * Global authentication functions
    -  *----------------------------------------------------------------
     @@ src/backend/libpq/auth.c: auth_failed(Port *port, int status, const char *logdetail)
      		case uaRADIUS:
      			errstr = gettext_noop("RADIUS authentication failed for user \"%s\"");
    @@ src/backend/libpq/hba.c: parse_hba_auth_opt(char *name, char *val, HbaLine *hbal
     
      ## src/backend/libpq/meson.build ##
     @@
    - # Copyright (c) 2022-2024, PostgreSQL Global Development Group
    + # Copyright (c) 2022-2025, PostgreSQL Global Development Group
      
      backend_sources += files(
     +  'auth-oauth.c',
    @@ src/backend/libpq/pg_hba.conf.sample
      #
      # OPTIONS are a set of options for the authentication in the format
     
    + ## src/backend/utils/adt/hbafuncs.c ##
    +@@ src/backend/utils/adt/hbafuncs.c: get_hba_options(HbaLine *hba)
    + 				CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s));
    + 	}
    + 
    ++	if (hba->auth_method == uaOAuth)
    ++	{
    ++		if (hba->oauth_issuer)
    ++			options[noptions++] =
    ++				CStringGetTextDatum(psprintf("issuer=%s", hba->oauth_issuer));
    ++
    ++		if (hba->oauth_scope)
    ++			options[noptions++] =
    ++				CStringGetTextDatum(psprintf("scope=%s", hba->oauth_scope));
    ++
    ++		if (hba->oauth_validator)
    ++			options[noptions++] =
    ++				CStringGetTextDatum(psprintf("validator=%s", hba->oauth_validator));
    ++
    ++		if (hba->oauth_skip_usermap)
    ++			options[noptions++] =
    ++				CStringGetTextDatum(psprintf("delegate_ident_mapping=true"));
    ++	}
    ++
    + 	/* If you add more options, consider increasing MAX_HBA_OPTIONS. */
    + 	Assert(noptions <= MAX_HBA_OPTIONS);
    + 
    +
      ## src/backend/utils/misc/guc_tables.c ##
     @@
      #include "jit/jit.h"
    @@ src/include/common/oauth-common.h (new)
     +#endif							/* OAUTH_COMMON_H */
     
      ## src/include/libpq/auth.h ##
    -@@
    - 
    - #include "libpq/libpq-be.h"
    - 
    -+/*
    -+ * Maximum accepted size of GSS and SSPI authentication tokens.
    -+ * We also use this as a limit on ordinary password packet lengths.
    -+ *
    -+ * Kerberos tickets are usually quite small, but the TGTs issued by Windows
    -+ * domain controllers include an authorization field known as the Privilege
    -+ * Attribute Certificate (PAC), which contains the user's Windows permissions
    -+ * (group memberships etc.). The PAC is copied into all tickets obtained on
    -+ * the basis of this TGT (even those issued by Unix realms which the Windows
    -+ * realm trusts), and can be several kB in size. The maximum token size
    -+ * accepted by Windows systems is determined by the MaxAuthToken Windows
    -+ * registry setting. Microsoft recommends that it is not set higher than
    -+ * 65535 bytes, so that seems like a reasonable limit for us as well.
    -+ */
    -+#define PG_MAX_AUTH_TOKEN_LENGTH	65535
    -+
    - extern PGDLLIMPORT char *pg_krb_server_keyfile;
    - extern PGDLLIMPORT bool pg_krb_caseins_users;
    - extern PGDLLIMPORT bool pg_gss_accept_delegation;
     @@ src/include/libpq/auth.h: extern PGDLLIMPORT bool pg_gss_accept_delegation;
      extern void ClientAuthentication(Port *port);
      extern void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
    @@ src/include/pg_config.h.in
      /* Define to 1 to build with LDAP support. (--with-ldap) */
      #undef USE_LDAP
      
    -+/* Define to 1 to build with libcurl support for OAuth client flows.
    -+   (--with-libcurl) */
    ++/* Define to 1 to build with libcurl support. (--with-libcurl) */
     +#undef USE_LIBCURL
     +
      /* Define to 1 to build with XML support. (--with-libxml) */
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +};
     +
     +/*
    -+ * Frees the async_ctx, which is stored directly on the PGconn. This is called
    -+ * during pqDropConnection() so that we don't leak resources even if
    -+ * PQconnectPoll() never calls us back.
    -+ *
    -+ * TODO: we should probably call this at the end of a successful authentication,
    -+ * too, to proactively free up resources.
    ++ * Tears down the Curl handles and frees the async_ctx.
     + */
     +static void
    -+free_curl_async_ctx(PGconn *conn, void *ctx)
    ++free_async_ctx(PGconn *conn, struct async_ctx *actx)
     +{
    -+	struct async_ctx *actx = ctx;
    -+
    -+	Assert(actx);				/* oauth_free() shouldn't call us otherwise */
    -+
     +	/*
     +	 * TODO: in general, none of the error cases below should ever happen if
     +	 * we have no bugs above. But if we do hit them, surfacing those errors
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +}
     +
     +/*
    ++ * Release resources used for the asynchronous exchange and disconnect the
    ++ * altsock.
    ++ *
    ++ * This is called either at the end of a successful authentication, or during
    ++ * pqDropConnection(), so we won't leak resources even if PQconnectPoll() never
    ++ * calls us back.
    ++ */
    ++void
    ++pg_fe_cleanup_oauth_flow(PGconn *conn)
    ++{
    ++	fe_oauth_state *state = conn->sasl_state;
    ++
    ++	if (state->async_ctx)
    ++	{
    ++		free_async_ctx(conn, state->async_ctx);
    ++		state->async_ctx = NULL;
    ++	}
    ++
    ++	conn->altsock = PGINVALID_SOCKET;
    ++}
    ++
    ++/*
     + * Macros for manipulating actx->errbuf. actx_error() translates and formats a
     + * string for you; actx_error_str() appends a string directly without
     + * translation.
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +	return true;
     +#endif
     +
    -+	actx_error(actx, "here's a nickel kid, get yourself a better computer");
    ++	actx_error(actx, "libpq does not support the Device Authorization flow on this platform");
     +	return false;
     +}
     +
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     + * provider.
     + */
     +static PostgresPollingStatusType
    -+pg_fe_run_oauth_flow_impl(PGconn *conn, pgsocket *altsock)
    ++pg_fe_run_oauth_flow_impl(PGconn *conn)
     +{
     +	fe_oauth_state *state = conn->sasl_state;
     +	struct async_ctx *actx;
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +		actx->debugging = oauth_unsafe_debugging_enabled();
     +
     +		state->async_ctx = actx;
    -+		state->free_async_ctx = free_curl_async_ctx;
     +
     +		initPQExpBuffer(&actx->work_data);
     +		initPQExpBuffer(&actx->errbuf);
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +	do
     +	{
     +		/* By default, the multiplexer is the altsock. Reassign as desired. */
    -+		*altsock = actx->mux;
    ++		conn->altsock = actx->mux;
     +
     +		switch (actx->step)
     +		{
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +				 * the client wait directly on the timerfd rather than the
     +				 * multiplexer. (This isn't possible for kqueue.)
     +				 */
    -+				*altsock = actx->timerfd;
    ++				conn->altsock = actx->timerfd;
     +#endif
     +
     +				actx->step = OAUTH_STEP_WAIT_INTERVAL;
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     + * wrapper logic before handing off to the true implementation, above.
     + */
     +PostgresPollingStatusType
    -+pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock)
    ++pg_fe_run_oauth_flow(PGconn *conn)
     +{
     +	PostgresPollingStatusType result;
     +#ifndef WIN32
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +	masked = (pq_block_sigpipe(&osigset, &sigpipe_pending) == 0);
     +#endif
     +
    -+	result = pg_fe_run_oauth_flow_impl(conn, altsock);
    ++	result = pg_fe_run_oauth_flow_impl(conn);
     +
     +#ifndef WIN32
     +	if (masked)
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +
     +/*
     + * Frees the state allocated by oauth_init().
    ++ *
    ++ * This handles only mechanism state tied to the connection lifetime; state
    ++ * stored in state->async_ctx is freed up either immediately after the
    ++ * authentication handshake succeeds, or before the mechanism is cleaned up on
    ++ * failure. See pg_fe_cleanup_oauth_flow() and cleanup_user_oauth_flow().
     + */
     +static void
     +oauth_free(void *opaq)
     +{
     +	fe_oauth_state *state = opaq;
     +
    ++	/* Any async authentication state should have been cleaned up already. */
    ++	Assert(!state->async_ctx);
    ++
     +	free(state->token);
    -+	if (state->async_ctx)
    -+		state->free_async_ctx(state->conn, state->async_ctx);
    -+
     +	free(state);
     +}
     +
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	}
     +
     +	/*
    -+	 * Find the start of the .well-known prefix. IETF rules state this must be
    -+	 * at the beginning of the path component, but OIDC defined it at the end
    -+	 * instead, so we have to search for it anywhere.
    ++	 * Find the start of the .well-known prefix. IETF rules (RFC 8615) state
    ++	 * this must be at the beginning of the path component, but OIDC defined
    ++	 * it at the end instead (OIDC Discovery 1.0, Sec. 4), so we have to
    ++	 * search for it anywhere.
     +	 */
     +	wk_start = strstr(authority_start, WK_PREFIX);
     +	if (!wk_start)
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	{
     +		char	   *discovery_issuer;
     +
    -+		/* The URI must correspond to our existing issuer, to avoid mix-ups. */
    ++		/*
    ++		 * The URI MUST correspond to our existing issuer, to avoid mix-ups.
    ++		 *
    ++		 * Issuer comparison is done byte-wise, rather than performing any URL
    ++		 * normalization; this follows the suggestions for issuer comparison
    ++		 * in RFC 9207 Sec. 2.4 (which requires simple string comparison) and
    ++		 * vastly simplifies things. Since this is the key protection against
    ++		 * a rogue server sending the client to an untrustworthy location,
    ++		 * simpler is better.
    ++		 */
     +		discovery_issuer = issuer_from_well_known_uri(conn, ctx.discovery_uri);
     +		if (!discovery_issuer)
     +			goto cleanup;		/* error message already set */
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     + * statuses for use by PQconnectPoll().
     + */
     +static PostgresPollingStatusType
    -+run_user_oauth_flow(PGconn *conn, pgsocket *altsock)
    ++run_user_oauth_flow(PGconn *conn)
     +{
     +	fe_oauth_state *state = conn->sasl_state;
     +	PGoauthBearerRequest *request = state->async_ctx;
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +		return PGRES_POLLING_FAILED;
     +	}
     +
    -+	status = request->async(conn, request, altsock);
    ++	status = request->async(conn, request, &conn->altsock);
     +	if (status == PGRES_POLLING_FAILED)
     +	{
     +		libpq_append_conn_error(conn, "user-defined OAuth flow failed");
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +}
     +
     +/*
    -+ * Cleanup callback for the user flow. Delegates most of its job to the
    -+ * user-provided cleanup implementation.
    ++ * Cleanup callback for the async user flow. Delegates most of its job to the
    ++ * user-provided cleanup implementation, then disconnects the altsock.
     + */
     +static void
    -+free_request(PGconn *conn, void *vreq)
    ++cleanup_user_oauth_flow(PGconn *conn)
     +{
    -+	PGoauthBearerRequest *request = vreq;
    ++	fe_oauth_state *state = conn->sasl_state;
    ++	PGoauthBearerRequest *request = state->async_ctx;
    ++
    ++	Assert(request);
     +
     +	if (request->cleanup)
     +		request->cleanup(conn, request);
    ++	conn->altsock = PGINVALID_SOCKET;
     +
     +	free(request);
    ++	state->async_ctx = NULL;
     +}
     +
     +/*
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +		memcpy(request_copy, &request, sizeof(request));
     +
     +		conn->async_auth = run_user_oauth_flow;
    ++		conn->cleanup_async_auth = cleanup_user_oauth_flow;
     +		state->async_ctx = request_copy;
    -+		state->free_async_ctx = free_request;
     +	}
     +	else if (res < 0)
     +	{
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +		 * caching at the moment. (Custom flows might be more sophisticated.)
     +		 */
     +		conn->async_auth = pg_fe_run_oauth_flow;
    ++		conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow;
     +		conn->oauth_want_retry = PG_BOOL_NO;
     +
     +#else
    -+		libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built using --with-libcurl");
    ++		libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built with libcurl support");
     +		goto fail;
     +
     +#endif
    @@ src/interfaces/libpq/fe-auth-oauth.h (new)
     +	char	   *token;
     +
     +	void	   *async_ctx;
    -+	void		(*free_async_ctx) (PGconn *conn, void *ctx);
     +} fe_oauth_state;
     +
    -+extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock);
    ++extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn);
    ++extern void pg_fe_cleanup_oauth_flow(PGconn *conn);
     +extern bool oauth_unsafe_debugging_enabled(void);
     +
     +/* Mechanisms in fe-auth-oauth.c */
    @@ src/interfaces/libpq/fe-auth-oauth.h (new)
     +
     +#endif							/* FE_AUTH_OAUTH_H */
     
    - ## src/interfaces/libpq/fe-auth-sasl.h ##
    -@@ src/interfaces/libpq/fe-auth-sasl.h: typedef enum
    - 	SASL_COMPLETE = 0,
    - 	SASL_FAILED,
    - 	SASL_CONTINUE,
    -+	SASL_ASYNC,
    - } SASLStatus;
    - 
    - /*
    -@@ src/interfaces/libpq/fe-auth-sasl.h: typedef struct pg_fe_sasl_mech
    - 	 *
    - 	 *	state:	   The opaque mechanism state returned by init()
    - 	 *
    -+	 *	final:	   true if the server has sent a final exchange outcome
    -+	 *
    - 	 *	input:	   The challenge data sent by the server, or NULL when
    - 	 *			   generating a client-first initial response (that is, when
    - 	 *			   the server expects the client to send a message to start
    -@@ src/interfaces/libpq/fe-auth-sasl.h: typedef struct pg_fe_sasl_mech
    - 	 *
    - 	 *	SASL_CONTINUE:	The output buffer is filled with a client response.
    - 	 *					Additional server challenge is expected
    -+	 *	SASL_ASYNC:		Some asynchronous processing external to the
    -+	 *					connection needs to be done before a response can be
    -+	 *					generated. The mechanism is responsible for setting up
    -+	 *					conn->async_auth appropriately before returning.
    - 	 *	SASL_COMPLETE:	The SASL exchange has completed successfully.
    - 	 *	SASL_FAILED:	The exchange has failed and the connection should be
    - 	 *					dropped.
    - 	 *--------
    - 	 */
    --	SASLStatus	(*exchange) (void *state, char *input, int inputlen,
    -+	SASLStatus	(*exchange) (void *state, bool final,
    -+							 char *input, int inputlen,
    - 							 char **output, int *outputlen);
    - 
    - 	/*--------
    -
    - ## src/interfaces/libpq/fe-auth-scram.c ##
    -@@
    - /* The exported SCRAM callback mechanism. */
    - static void *scram_init(PGconn *conn, const char *password,
    - 						const char *sasl_mechanism);
    --static SASLStatus scram_exchange(void *opaq, char *input, int inputlen,
    -+static SASLStatus scram_exchange(void *opaq, bool final,
    -+								 char *input, int inputlen,
    - 								 char **output, int *outputlen);
    - static bool scram_channel_bound(void *opaq);
    - static void scram_free(void *opaq);
    -@@ src/interfaces/libpq/fe-auth-scram.c: scram_free(void *opaq)
    -  * Exchange a SCRAM message with backend.
    -  */
    - static SASLStatus
    --scram_exchange(void *opaq, char *input, int inputlen,
    -+scram_exchange(void *opaq, bool final,
    -+			   char *input, int inputlen,
    - 			   char **output, int *outputlen)
    - {
    - 	fe_scram_state *state = (fe_scram_state *) opaq;
    -
      ## src/interfaces/libpq/fe-auth.c ##
     @@
      #endif
    @@ src/interfaces/libpq/fe-auth.c
      #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.
    -  */
    - static int
    --pg_SASL_init(PGconn *conn, int payloadlen)
    -+pg_SASL_init(PGconn *conn, int payloadlen, bool *async)
    - {
    - 	char	   *initialresponse = NULL;
    - 	int			initialresponselen;
    -@@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
    - 		goto error;
    - 	}
    - 
    --	if (conn->sasl_state)
    -+	if (conn->sasl_state && !conn->async_auth)
    - 	{
    - 		libpq_append_conn_error(conn, "duplicate SASL authentication request");
    - 		goto error;
    -@@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
    +@@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen, bool *async)
      			conn->sasl = &pg_scram_mech;
      			conn->password_needed = true;
      		}
    @@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
      	}
      
      	if (!selected_mechanism)
    -@@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
    - 
    - 	Assert(conn->sasl);
    - 
    --	/*
    --	 * Initialize the SASL state information with all the information gathered
    --	 * during the initial exchange.
    --	 *
    --	 * Note: Only tls-unique is supported for the moment.
    --	 */
    --	conn->sasl_state = conn->sasl->init(conn,
    --										password,
    --										selected_mechanism);
    - 	if (!conn->sasl_state)
    --		goto oom_error;
    -+	{
    -+		/*
    -+		 * Initialize the SASL state information with all the information
    -+		 * gathered during the initial exchange.
    -+		 *
    -+		 * Note: Only tls-unique is supported for the moment.
    -+		 */
    -+		conn->sasl_state = conn->sasl->init(conn,
    -+											password,
    -+											selected_mechanism);
    -+		if (!conn->sasl_state)
    -+			goto oom_error;
    -+	}
    -+	else
    -+	{
    -+		/*
    -+		 * This is only possible if we're returning from an async loop.
    -+		 * Disconnect it now.
    -+		 */
    -+		Assert(conn->async_auth);
    -+		conn->async_auth = NULL;
    -+	}
    - 
    - 	/* Get the mechanism-specific Initial Client Response, if any */
    --	status = conn->sasl->exchange(conn->sasl_state,
    -+	status = conn->sasl->exchange(conn->sasl_state, false,
    - 								  NULL, -1,
    - 								  &initialresponse, &initialresponselen);
    - 
    - 	if (status == SASL_FAILED)
    - 		goto error;
    - 
    -+	if (status == SASL_ASYNC)
    -+	{
    -+		/*
    -+		 * The mechanism should have set up the necessary callbacks; all we
    -+		 * need to do is signal the caller.
    -+		 */
    -+		*async = true;
    -+		return STATUS_OK;
    -+	}
    -+
    - 	/*
    - 	 * Build a SASLInitialResponse message, and send it.
    - 	 */
    -@@ src/interfaces/libpq/fe-auth.c: oom_error:
    -  * the protocol.
    -  */
    - static int
    --pg_SASL_continue(PGconn *conn, int payloadlen, bool final)
    -+pg_SASL_continue(PGconn *conn, int payloadlen, bool final, bool *async)
    - {
    - 	char	   *output;
    - 	int			outputlen;
    -@@ src/interfaces/libpq/fe-auth.c: pg_SASL_continue(PGconn *conn, int payloadlen, bool final)
    - 	/* For safety and convenience, ensure the buffer is NULL-terminated. */
    - 	challenge[payloadlen] = '\0';
    - 
    --	status = conn->sasl->exchange(conn->sasl_state,
    -+	status = conn->sasl->exchange(conn->sasl_state, final,
    - 								  challenge, payloadlen,
    - 								  &output, &outputlen);
    - 	free(challenge);			/* don't need the input anymore */
    - 
    -+	if (status == SASL_ASYNC)
    -+	{
    -+		/*
    -+		 * The mechanism should have set up the necessary callbacks; all we
    -+		 * need to do is signal the caller.
    -+		 */
    -+		*async = true;
    -+		return STATUS_OK;
    -+	}
    -+
    - 	if (final && status == SASL_CONTINUE)
    - 	{
    - 		if (outputlen != 0)
    -@@ src/interfaces/libpq/fe-auth.c: check_expected_areq(AuthRequest areq, PGconn *conn)
    -  * it. We are responsible for reading any remaining extra data, specific
    -  * to the authentication method. 'payloadlen' is the remaining length in
    -  * the message.
    -+ *
    -+ * If *async is set to true on return, the client doesn't yet have enough
    -+ * information to respond, and the caller must temporarily switch to
    -+ * conn->async_auth() to continue driving the exchange.
    -  */
    - int
    --pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
    -+pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn, bool *async)
    - {
    - 	int			oldmsglen;
    - 
    -+	*async = false;
    -+
    - 	if (!check_expected_areq(areq, conn))
    - 		return STATUS_ERROR;
    - 
    -@@ src/interfaces/libpq/fe-auth.c: pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
    - 			 * The request contains the name (as assigned by IANA) of the
    - 			 * authentication mechanism.
    - 			 */
    --			if (pg_SASL_init(conn, payloadlen) != STATUS_OK)
    -+			if (pg_SASL_init(conn, payloadlen, async) != STATUS_OK)
    - 			{
    - 				/* pg_SASL_init already set the error message */
    - 				return STATUS_ERROR;
    -@@ src/interfaces/libpq/fe-auth.c: pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
    - 			}
    - 			oldmsglen = conn->errorMessage.len;
    - 			if (pg_SASL_continue(conn, payloadlen,
    --								 (areq == AUTH_REQ_SASL_FIN)) != STATUS_OK)
    -+								 (areq == AUTH_REQ_SASL_FIN),
    -+								 async) != STATUS_OK)
    - 			{
    - 				/* Use this message if pg_SASL_continue didn't supply one */
    - 				if (conn->errorMessage.len == oldmsglen)
     @@ src/interfaces/libpq/fe-auth.c: PQchangePassword(PGconn *conn, const char *user, const char *passwd)
      		}
      	}
    @@ src/interfaces/libpq/fe-auth.h
     +
     +
      /* Prototypes for functions in fe-auth.c */
    --extern int	pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
    -+extern int	pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn,
    -+						   bool *async);
    - extern char *pg_fe_getusername(uid_t user_id, PQExpBuffer errorMessage);
    - extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
    - 
    + extern int	pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn,
    + 						   bool *async);
     
      ## src/interfaces/libpq/fe-connect.c ##
     @@
    @@ src/interfaces/libpq/fe-connect.c: pqDropServerData(PGconn *conn)
      
      	/*
      	 * Cancel connections need to retain their be_pid and be_key across
    -@@ src/interfaces/libpq/fe-connect.c: PQconnectPoll(PGconn *conn)
    - 		case CONNECTION_NEEDED:
    - 		case CONNECTION_GSS_STARTUP:
    - 		case CONNECTION_CHECK_TARGET:
    -+		case CONNECTION_AUTHENTICATING:
    - 			break;
    - 
    - 		default:
    -@@ src/interfaces/libpq/fe-connect.c: keep_going:						/* We will come back to here until there is
    - 				int			avail;
    - 				AuthRequest areq;
    - 				int			res;
    -+				bool		async;
    - 
    - 				/*
    - 				 * Scan the message from current point (note that if we find
     @@ src/interfaces/libpq/fe-connect.c: keep_going:						/* We will come back to here until there is
      					/* Check to see if we should mention pgpassfile */
      					pgpassfileWarning(conn);
    @@ src/interfaces/libpq/fe-connect.c: keep_going:						/* We will come back to here
      					CONNECTION_FAILED();
      				}
      				else if (beresp == PqMsg_NegotiateProtocolVersion)
    -@@ src/interfaces/libpq/fe-connect.c: keep_going:						/* We will come back to here until there is
    - 				 * Note that conn->pghost must be non-NULL if we are going to
    - 				 * avoid the Kerberos code doing a hostname look-up.
    - 				 */
    --				res = pg_fe_sendauth(areq, msgLength, conn);
    -+				res = pg_fe_sendauth(areq, msgLength, conn, &async);
    -+
    -+				if (async && (res == STATUS_OK))
    -+				{
    -+					/*
    -+					 * We'll come back later once we're ready to respond.
    -+					 * Don't consume the request yet.
    -+					 */
    -+					conn->status = CONNECTION_AUTHENTICATING;
    -+					goto keep_going;
    -+				}
    - 
    - 				/*
    - 				 * OK, we have processed the message; mark data consumed.  We
    -@@ src/interfaces/libpq/fe-connect.c: keep_going:						/* We will come back to here until there is
    - 				goto keep_going;
    - 			}
    - 
    -+		case CONNECTION_AUTHENTICATING:
    -+			{
    -+				PostgresPollingStatusType status;
    -+				pgsocket	altsock = PGINVALID_SOCKET;
    -+
    -+				if (!conn->async_auth)
    -+				{
    -+					/* programmer error; should not happen */
    -+					libpq_append_conn_error(conn, "async authentication has no handler");
    -+					goto error_return;
    -+				}
    -+
    -+				status = conn->async_auth(conn, &altsock);
    -+
    -+				if (status == PGRES_POLLING_FAILED)
    -+					goto error_return;
    -+
    -+				if (status == PGRES_POLLING_OK)
    -+				{
    -+					/*
    -+					 * Reenter the authentication exchange with the server. We
    -+					 * didn't consume the message that started external
    -+					 * authentication, so it'll be reprocessed as if we just
    -+					 * received it.
    -+					 */
    -+					conn->status = CONNECTION_AWAITING_RESPONSE;
    -+					conn->altsock = PGINVALID_SOCKET;	/* TODO: what frees
    -+														 * this? */
    -+					goto keep_going;
    -+				}
    -+
    -+				conn->altsock = altsock;
    -+				return status;
    -+			}
    -+
    - 		case CONNECTION_AUTH_OK:
    - 			{
    - 				/*
    -@@ src/interfaces/libpq/fe-connect.c: pqMakeEmptyPGconn(void)
    - 	conn->verbosity = PQERRORS_DEFAULT;
    - 	conn->show_context = PQSHOW_CONTEXT_ERRORS;
    - 	conn->sock = PGINVALID_SOCKET;
    -+	conn->altsock = PGINVALID_SOCKET;
    - 	conn->Pfdebug = NULL;
    - 
    - 	/*
     @@ src/interfaces/libpq/fe-connect.c: freePGconn(PGconn *conn)
      	free(conn->rowBuf);
      	free(conn->target_session_attrs);
    @@ src/interfaces/libpq/fe-connect.c: freePGconn(PGconn *conn)
      	termPQExpBuffer(&conn->errorMessage);
      	termPQExpBuffer(&conn->workBuffer);
      
    -@@ src/interfaces/libpq/fe-connect.c: PQsocket(const PGconn *conn)
    - {
    - 	if (!conn)
    - 		return -1;
    -+	if (conn->altsock != PGINVALID_SOCKET)
    -+		return conn->altsock;
    - 	return (conn->sock != PGINVALID_SOCKET) ? conn->sock : -1;
    - }
    - 
    -
    - ## src/interfaces/libpq/fe-misc.c ##
    -@@ src/interfaces/libpq/fe-misc.c: static int
    - pqSocketCheck(PGconn *conn, int forRead, int forWrite, pg_usec_time_t end_time)
    - {
    - 	int			result;
    -+	pgsocket	sock;
    - 
    - 	if (!conn)
    - 		return -1;
    --	if (conn->sock == PGINVALID_SOCKET)
    -+
    -+	sock = (conn->altsock != PGINVALID_SOCKET) ? conn->altsock : conn->sock;
    -+	if (sock == PGINVALID_SOCKET)
    - 	{
    - 		libpq_append_conn_error(conn, "invalid socket");
    - 		return -1;
    -@@ src/interfaces/libpq/fe-misc.c: pqSocketCheck(PGconn *conn, int forRead, int forWrite, pg_usec_time_t end_time)
    - 
    - 	/* We will retry as long as we get EINTR */
    - 	do
    --		result = PQsocketPoll(conn->sock, forRead, forWrite, end_time);
    -+		result = PQsocketPoll(sock, forRead, forWrite, end_time);
    - 	while (result < 0 && SOCK_ERRNO == EINTR);
    - 
    - 	if (result < 0)
     
      ## src/interfaces/libpq/libpq-fe.h ##
    -@@ src/interfaces/libpq/libpq-fe.h: extern "C"
    -  */
    - #include "postgres_ext.h"
    - 
    -+#ifdef WIN32
    -+#include <winsock2.h>			/* for SOCKET */
    -+#endif
    -+
    - /*
    -  * These symbols may be used in compile-time #ifdef tests for the availability
    -  * of v14-and-newer libpq features.
     @@ src/interfaces/libpq/libpq-fe.h: extern "C"
      /* Features added in PostgreSQL v18: */
      /* Indicates presence of PQfullProtocolVersion */
    @@ src/interfaces/libpq/libpq-fe.h: extern "C"
      
      /*
       * Option flags for PQcopyResult
    -@@ src/interfaces/libpq/libpq-fe.h: typedef enum
    - 	CONNECTION_CHECK_STANDBY,	/* Checking if server is in standby mode. */
    - 	CONNECTION_ALLOCATED,		/* Waiting for connection attempt to be
    - 								 * started.  */
    -+	CONNECTION_AUTHENTICATING,	/* Authentication is in progress with some
    -+								 * external system. */
    - } ConnStatusType;
    - 
    - typedef enum
     @@ src/interfaces/libpq/libpq-fe.h: typedef enum
      	PQ_PIPELINE_ABORTED
      } PGpipelineStatus;
    @@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
     +} PGpromptOAuthDevice;
     +
     +/* for PGoauthBearerRequest.async() */
    -+#ifdef WIN32
    -+#define SOCKTYPE SOCKET
    ++#ifdef _WIN32
    ++#define SOCKTYPE uintptr_t		/* avoids depending on winsock2.h for SOCKET */
     +#else
     +#define SOCKTYPE int
     +#endif
    @@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
     +	 * Callback implementing a custom asynchronous OAuth flow.
     +	 *
     +	 * The callback may return
    -+	 * - PGRES_POLLING_READING/WRITING, to indicate that a file descriptor
    ++	 * - PGRES_POLLING_READING/WRITING, to indicate that a socket descriptor
     +	 *   has been stored in *altsock and libpq should wait until it is
     +	 *   readable or writable before calling back;
     +	 * - PGRES_POLLING_OK, to indicate that the flow is complete and
    @@ src/interfaces/libpq/libpq-int.h: struct pg_conn
      	/* Optional file to write trace info to */
      	FILE	   *Pfdebug;
      	int			traceFlags;
    -@@ src/interfaces/libpq/libpq-int.h: struct pg_conn
    - 										 * know which auth response we're
    - 										 * sending */
    - 
    -+	/* Callback for external async authentication */
    -+	PostgresPollingStatusType (*async_auth) (PGconn *conn, pgsocket *altsock);
    -+	pgsocket	altsock;		/* alternative socket for client to poll */
    -+
    -+
    - 	/* Transient state needed while establishing connection */
    - 	PGTargetServerType target_server_type;	/* desired session properties */
    - 	PGLoadBalanceType load_balance_type;	/* desired load balancing
     
      ## src/interfaces/libpq/meson.build ##
     @@
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
     +
     +#include "postgres_fe.h"
     +
    -+#include <stdio.h>
    -+#include <stdlib.h>
    -+
    -+#ifdef WIN32
    -+#include <winsock2.h>
    -+#else
     +#include <sys/socket.h>
    -+#endif
     +
     +#include "getopt_long.h"
     +#include "libpq-fe.h"
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
     +static void
     +usage(char *argv[])
     +{
    -+	fprintf(stderr, "usage: %s [flags] CONNINFO\n\n", argv[0]);
    ++	printf("usage: %s [flags] CONNINFO\n\n", argv[0]);
     +
    -+	fprintf(stderr, "recognized flags:\n");
    -+	fprintf(stderr, " -h, --help				show this message\n");
    -+	fprintf(stderr, " --expected-scope SCOPE	fail if received scopes do not match SCOPE\n");
    -+	fprintf(stderr, " --expected-uri URI		fail if received configuration link does not match URI\n");
    -+	fprintf(stderr, " --no-hook					don't install OAuth hooks (connection will fail)\n");
    -+	fprintf(stderr, " --hang-forever			don't ever return a token (combine with connect_timeout)\n");
    -+	fprintf(stderr, " --token TOKEN				use the provided TOKEN value\n");
    ++	printf("recognized flags:\n");
    ++	printf(" -h, --help				show this message\n");
    ++	printf(" --expected-scope SCOPE	fail if received scopes do not match SCOPE\n");
    ++	printf(" --expected-uri URI		fail if received configuration link does not match URI\n");
    ++	printf(" --no-hook				don't install OAuth hooks (connection will fail)\n");
    ++	printf(" --hang-forever			don't ever return a token (combine with connect_timeout)\n");
    ++	printf(" --token TOKEN			use the provided TOKEN value\n");
     +}
     +
     +/* --options */
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +
     +my $log_start = $node->wait_for_log(qr/reloading configuration files/);
     +
    ++# Check pg_hba_file_rules() support.
    ++my $contents = $bgconn->query_safe(
    ++	qq(SELECT rule_number, auth_method, options
    ++		 FROM pg_hba_file_rules
    ++		 ORDER BY rule_number;));
    ++is( $contents,
    ++	qq{1|oauth|\{issuer=$issuer,"scope=openid postgres",validator=validator\}
    ++2|oauth|\{issuer=$issuer/.well-known/oauth-authorization-server/alternate,"scope=openid postgres alt",validator=validator\}
    ++3|oauth|\{issuer=$issuer/param,"scope=openid postgres",validator=validator\}},
    ++	"pg_hba_file_rules recreates OAuth HBA settings");
     +
     +# To test against HTTP rather than HTTPS, we need to enable PGOAUTHDEBUG. But
     +# first, check to make sure the client refuses such connections by default.
    @@ src/test/modules/oauth_validator/t/002_client.pl (new)
     +		"fails without custom hook installed",
     +		flags => ["--no-hook"],
     +		expected_stderr =>
    -+		  qr/no custom OAuth flows are available, and libpq was not built using --with-libcurl/
    ++		  qr/no custom OAuth flows are available, and libpq was not built with libcurl support/
     +	);
     +}
     +
    @@ src/tools/pgindent/typedefs.list: explain_get_index_name_hook_type
      fe_scram_state
      fe_scram_state_enum
      fetch_range_request
    -@@ src/tools/pgindent/typedefs.list: nsphash_hash
    - ntile_context
    - nullingrel_info
    - numeric
    -+oauth_state
    - object_access_hook_type
    - object_access_hook_type_str
    - off_t
-:  ----------- > 5:  18507c6978b squash! Add OAUTHBEARER SASL mechanism
-:  ----------- > 6:  8e82059700b XXX fix libcurl link error
3:  661de01c4ed = 7:  5339b3f2617 DO NOT MERGE: Add pytest suite for OAuth