since-v40.diff.txt
text/plain
Filename: since-v40.diff.txt
Type: text/plain
Part: 0
-: ----------- > 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