-: ----------- > 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 ## 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 - - - -+ -+ oauth -+ -+ -+ The server must request an OAuth bearer token from the client. -+ -+ -+ -+ - - none - + 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 + + + -+ Issuer ++ Issuer + + + An identifier for an authorization server, printed as an @@ doc/src/sgml/client-auth.sgml: host ... radius radiusservers="server1,server2" r + issuer + + -+ 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 ++ issuer identifier 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. + + -+ 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 + /.well-known/openid-configuration will be appended -+ to the issuer identifier. Alternatively, if the ++ to the end of the issuer identifier. Alternatively, if the + issuer contains a /.well-known/ -+ path segment, the URI will be provided to the client as-is. ++ path segment, that URL will be provided to the client as-is. + + + @@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + oauth_issuer + + -+ 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 issuer + setting in the server's HBA configuration. + + + As part of the standard authentication handshake, libpq -+ will ask the server for a discovery document: a URI ++ will ask the server for a discovery document: 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 + oauth_issuer, 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 + + 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 oauth_issuer to a ++ discovery document URL, you may set oauth_issuer to a + /.well-known/ URI used for OAuth discovery. (In this + case, it is recommended that + be set as well, since the @@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + /.well-known/oauth-authorization-server + + ++ ++ ++ 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 oauth_issuer. ++ ++ + + + @@ 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) + + General Coding Guidelines + -+ TODO ++ Developers should keep the following in mind when implementing token ++ validation: + + + @@ doc/src/sgml/oauth-validators.sgml (new) + EINTR/EAGAIN from a blocking call + should call CHECK_FOR_INTERRUPTS() 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. + + + @@ doc/src/sgml/oauth-validators.sgml (new) + + + 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. ++ ++ ++ ++ ++ Documentation ++ ++ ++ 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 ++ delegate_ident_mapping=1 mode, and what additional ++ configuration is required in order to do so. + + + @@ 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 /* 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 -+#include -+ -+#ifdef WIN32 -+#include -+#else +#include -+#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