1: 5d474397364 = 1: 258b8dbb770 Move PG_MAX_AUTH_TOKEN_LENGTH to libpq/auth.h 2: 20452d21e0b ! 2: ec960cf363d require_auth: prepare for multiple SASL mechanisms @@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen) { ## src/interfaces/libpq/fe-connect.c ## +@@ src/interfaces/libpq/fe-connect.c: static const PQEnvironmentOption EnvironmentOptions[] = + } + }; + ++static const pg_fe_sasl_mech *supported_sasl_mechs[] = ++{ ++ &pg_scram_mech, ++}; ++#define SASL_MECHANISM_COUNT lengthof(supported_sasl_mechs) ++ + /* The connection URI must start with either of the following designators: */ + static const char uri_designator[] = "postgresql://"; + static const char short_uri_designator[] = "postgres://"; @@ src/interfaces/libpq/fe-connect.c: libpq_prng_init(PGconn *conn) pg_prng_seed(&conn->prng_state, rseed); } @@ src/interfaces/libpq/fe-connect.c: libpq_prng_init(PGconn *conn) + * rely on the compile-time assertion here to keep us honest. + * + * To add a new mechanism to require_auth, ++ * - add it to supported_sasl_mechs, + * - update the length of conn->allowed_sasl_mechs, -+ * - add the new pg_fe_sasl_mech pointer to this function, and + * - handle the new mechanism name in the require_auth portion of + * pqConnectOptions2(), below. + */ -+ StaticAssertDecl(lengthof(conn->allowed_sasl_mechs) == 1, -+ "fill_allowed_sasl_mechs() must be updated when resizing conn->allowed_sasl_mechs[]"); ++ StaticAssertDecl(lengthof(conn->allowed_sasl_mechs) == SASL_MECHANISM_COUNT, ++ "conn->allowed_sasl_mechs[] is not sufficiently large for holding all supported SASL mechanisms"); + -+ conn->allowed_sasl_mechs[0] = &pg_scram_mech; ++ for (int i = 0; i < SASL_MECHANISM_COUNT; i++) ++ conn->allowed_sasl_mechs[i] = supported_sasl_mechs[i]; +} + +/* @@ src/interfaces/libpq/fe-connect.c: pqConnectOptions2(PGconn *conn) + * Next group: SASL mechanisms. All of these use the same request + * codes, so the list of allowed mechanisms is tracked separately. + * -+ * fill_allowed_sasl_mechs() must be updated when adding a new -+ * mechanism here! ++ * supported_sasl_mechs must contain all mechanisms handled here. + */ else if (strcmp(method, "scram-sha-256") == 0) { @@ src/interfaces/libpq/fe-connect.c: pqConnectOptions2(PGconn *conn) + 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) + i = index_of_allowed_sasl_mech(conn, mech); + if (i >= 0) + goto duplicate; -+ + +- conn->allowed_auth_methods &= ~bits; + i = index_of_allowed_sasl_mech(conn, NULL); + if (i < 0) + { 3: f0afefb80d6 ! 3: 9725788086c libpq: handle asynchronous actions during SASL @@ src/interfaces/libpq/fe-connect.c: pqDropConnection(PGconn *conn, bool flushInpu + conn->cleanup_async_auth = NULL; + } + conn->async_auth = NULL; -+ conn->altsock = PGINVALID_SOCKET; /* cleanup_async_auth() should have -+ * done this, but make sure. */ ++ /* cleanup_async_auth() should have done this, but make sure */ ++ conn->altsock = PGINVALID_SOCKET; #ifdef ENABLE_GSS { OM_uint32 min_s; 4: 711ca3f1efc ! 4: a260d9436f0 Add OAUTHBEARER SASL mechanism @@ .cirrus.tasks.yml: task: ### # Test that code can be built with gcc/clang without warnings + ## config/programs.m4 ## +@@ config/programs.m4: AC_DEFUN([PGAC_CHECK_STRIP], + AC_SUBST(STRIP_STATIC_LIB) + AC_SUBST(STRIP_SHARED_LIB) + ])# PGAC_CHECK_STRIP ++ ++ ++ ++# PGAC_CHECK_LIBCURL ++# ------------------ ++# Check for required libraries and headers, and test to see whether the current ++# installation of libcurl is threadsafe. ++ ++AC_DEFUN([PGAC_CHECK_LIBCURL], ++[ ++ AC_CHECK_HEADER(curl/curl.h, [], ++ [AC_MSG_ERROR([header file is required for --with-libcurl])]) ++ AC_CHECK_LIB(curl, curl_multi_init, [], ++ [AC_MSG_ERROR([library 'curl' does not provide curl_multi_init])]) ++ ++ # Check to see whether the current platform supports threadsafe Curl ++ # initialization. ++ AC_CACHE_CHECK([for curl_global_init thread safety], [pgac_cv__libcurl_threadsafe_init], ++ [AC_RUN_IFELSE([AC_LANG_PROGRAM([ ++#include ++],[ ++ curl_version_info_data *info; ++ ++ if (curl_global_init(CURL_GLOBAL_ALL)) ++ return -1; ++ ++ info = curl_version_info(CURLVERSION_NOW); ++#ifdef CURL_VERSION_THREADSAFE ++ if (info->features & CURL_VERSION_THREADSAFE) ++ return 0; ++#endif ++ ++ return 1; ++])], ++ [pgac_cv__libcurl_threadsafe_init=yes], ++ [pgac_cv__libcurl_threadsafe_init=no], ++ [pgac_cv__libcurl_threadsafe_init=unknown])]) ++ if test x"$pgac_cv__libcurl_threadsafe_init" = xyes ; then ++ AC_DEFINE(HAVE_THREADSAFE_CURL_GLOBAL_INIT, 1, ++ [Define to 1 if curl_global_init() is guaranteed to be threadsafe.]) ++ fi ++])# PGAC_CHECK_LIBCURL + ## configure ## @@ configure: XML2_LIBS XML2_CFLAGS @@ configure: fi + +fi + ++ # We only care about -I, -D, and -L switches; ++ # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. ++ for pgac_option in $LIBCURL_CFLAGS; do ++ case $pgac_option in ++ -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; ++ esac ++ done ++ for pgac_option in $LIBCURL_LIBS; do ++ case $pgac_option in ++ -L*) LDFLAGS="$LDFLAGS $pgac_option";; ++ esac ++ done ++ + # OAuth requires python for testing + if test "$with_python" != yes; then + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests require --with-python to run" >&5 @@ configure: fi +# during gss_acquire_cred(). This is possibly related to Curl's Heimdal +# dependency on that platform? +if test "$with_libcurl" = yes ; then ++ ++ ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default" ++if test "x$ac_cv_header_curl_curl_h" = xyes; then : ++ ++else ++ as_fn_error $? "header file is required for --with-libcurl" "$LINENO" 5 ++fi ++ ++ + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_multi_init in -lcurl" >&5 +$as_echo_n "checking for curl_multi_init in -lcurl... " >&6; } +if ${ac_cv_lib_curl_curl_multi_init+:} false; then : @@ configure: fi + LIBS="-lcurl $LIBS" + +else -+ as_fn_error $? "library 'curl' is required for --with-libcurl" "$LINENO" 5 ++ as_fn_error $? "library 'curl' does not provide curl_multi_init" "$LINENO" 5 +fi + ++ ++ # Check to see whether the current platform supports threadsafe Curl ++ # initialization. ++ { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_global_init thread safety" >&5 ++$as_echo_n "checking for curl_global_init thread safety... " >&6; } ++if ${pgac_cv__libcurl_threadsafe_init+:} false; then : ++ $as_echo_n "(cached) " >&6 ++else ++ if test "$cross_compiling" = yes; then : ++ pgac_cv__libcurl_threadsafe_init=unknown ++else ++ cat confdefs.h - <<_ACEOF >conftest.$ac_ext ++/* end confdefs.h. */ ++ ++#include ++ ++int ++main () ++{ ++ ++ curl_version_info_data *info; ++ ++ if (curl_global_init(CURL_GLOBAL_ALL)) ++ return -1; ++ ++ info = curl_version_info(CURLVERSION_NOW); ++#ifdef CURL_VERSION_THREADSAFE ++ if (info->features & CURL_VERSION_THREADSAFE) ++ return 0; ++#endif ++ ++ return 1; ++ ++ ; ++ return 0; ++} ++_ACEOF ++if ac_fn_c_try_run "$LINENO"; then : ++ pgac_cv__libcurl_threadsafe_init=yes ++else ++ pgac_cv__libcurl_threadsafe_init=no ++fi ++rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ ++ conftest.$ac_objext conftest.beam conftest.$ac_ext ++fi ++ ++fi ++{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv__libcurl_threadsafe_init" >&5 ++$as_echo "$pgac_cv__libcurl_threadsafe_init" >&6; } ++ if test x"$pgac_cv__libcurl_threadsafe_init" = xyes ; then ++ ++$as_echo "#define HAVE_THREADSAFE_CURL_GLOBAL_INIT 1" >>confdefs.h ++ ++ fi ++ +fi + if test "$with_gssapi" = yes ; then if test "$PORTNAME" != "win32"; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking for library containing gss_store_cred_into" >&5 -@@ configure: fi - - done - -+fi -+ -+if test "$with_libcurl" = yes; then -+ ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default" -+if test "x$ac_cv_header_curl_curl_h" = xyes; then : -+ -+else -+ as_fn_error $? "header file is required for --with-libcurl" "$LINENO" 5 -+fi -+ -+ - fi - - if test "$PORTNAME" = "win32" ; then ## configure.ac ## @@ configure.ac: fi @@ configure.ac: fi + # to explicitly set TLS 1.3 ciphersuites). + PKG_CHECK_MODULES(LIBCURL, [libcurl >= 7.61.0]) + ++ # We only care about -I, -D, and -L switches; ++ # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. ++ for pgac_option in $LIBCURL_CFLAGS; do ++ case $pgac_option in ++ -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; ++ esac ++ done ++ for pgac_option in $LIBCURL_LIBS; do ++ case $pgac_option in ++ -L*) LDFLAGS="$LDFLAGS $pgac_option";; ++ esac ++ done ++ + # OAuth requires python for testing + if test "$with_python" != yes; then + AC_MSG_WARN([*** OAuth support tests require --with-python to run]) @@ configure.ac: failure. It is possible the compiler isn't looking in the proper +# during gss_acquire_cred(). This is possibly related to Curl's Heimdal +# dependency on that platform? +if test "$with_libcurl" = yes ; then -+ AC_CHECK_LIB(curl, curl_multi_init, [], [AC_MSG_ERROR([library 'curl' is required for --with-libcurl])]) ++ PGAC_CHECK_LIBCURL +fi + if test "$with_gssapi" = yes ; then if test "$PORTNAME" != "win32"; then AC_SEARCH_LIBS(gss_store_cred_into, [gssapi_krb5 gss 'gssapi -lkrb5 -lcrypto'], [], -@@ configure.ac: elif test "$with_uuid" = ossp ; then - [AC_MSG_ERROR([header file or is required for OSSP UUID])])]) - fi - -+if test "$with_libcurl" = yes; then -+ AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file is required for --with-libcurl])]) -+fi -+ - if test "$PORTNAME" = "win32" ; then - AC_CHECK_HEADERS(crtdefs.h) - fi ## doc/src/sgml/client-auth.sgml ## @@ doc/src/sgml/client-auth.sgml: include_dir directory @@ doc/src/sgml/libpq.sgml: void PQinitSSL(int do_ssl); Behavior in Threaded Programs +@@ doc/src/sgml/libpq.sgml: int PQisthreadsafe(); + libpq source code for a way to do cooperative + locking between libpq and your application. + ++ ++ ++ Similarly, if you are using Curl inside your application, ++ and you do not already ++ initialize ++ libcurl globally before starting new threads, you will need to ++ cooperatively lock (again via PQregisterThreadLock) ++ around any code that may initialize libcurl. This restriction is lifted for ++ more recent versions of Curl that are built to support threadsafe ++ initialization; those builds can be identified by the advertisement of a ++ threadsafe feature in their version metadata. ++ + + + ## doc/src/sgml/oauth-validators.sgml (new) ## @@ @@ doc/src/sgml/postgres.sgml: break is not needed in a wider output rendering. + ## doc/src/sgml/protocol.sgml ## +@@ doc/src/sgml/protocol.sgml: SELCT 1/0; + + + SASL is a framework for authentication in connection-oriented +- protocols. At the moment, PostgreSQL implements two SASL +- authentication mechanisms, SCRAM-SHA-256 and SCRAM-SHA-256-PLUS. More +- might be added in the future. The below steps illustrate how SASL +- authentication is performed in general, while the next subsection gives +- more details on SCRAM-SHA-256 and SCRAM-SHA-256-PLUS. ++ protocols. At the moment, PostgreSQL implements three ++ SASL authentication mechanisms: SCRAM-SHA-256, SCRAM-SHA-256-PLUS, and ++ OAUTHBEARER. More might be added in the future. The below steps illustrate how SASL ++ authentication is performed in general, while the next subsections give ++ more details on particular mechanisms. + + + +@@ doc/src/sgml/protocol.sgml: SELCT 1/0; + + + Finally, when the authentication exchange is completed successfully, the +- server sends an AuthenticationSASLFinal message, followed ++ server sends an optional AuthenticationSASLFinal message, followed + immediately by an AuthenticationOk message. The AuthenticationSASLFinal + contains additional server-to-client data, whose content is particular to the + selected authentication mechanism. If the authentication mechanism doesn't +@@ doc/src/sgml/protocol.sgml: SELCT 1/0; + SCRAM-SHA-256 Authentication + + +- The implemented SASL mechanisms at the moment +- are SCRAM-SHA-256 and its variant with channel +- binding SCRAM-SHA-256-PLUS. They are described in ++ SCRAM-SHA-256, and its variant with channel ++ binding SCRAM-SHA-256-PLUS, are password-based ++ authentication mechanisms. They are described in + detail in RFC 7677 + and RFC 5802. + +@@ doc/src/sgml/protocol.sgml: SELCT 1/0; + + + ++ ++ ++ OAUTHBEARER Authentication ++ ++ ++ OAUTHBEARER is a token-based mechanism for federated ++ authentication. It is described in detail in ++ RFC 7628. ++ ++ ++ ++ A typical exchange differs depending on whether or not the client already ++ has a bearer token cached for the current user. If it does not, the exchange ++ will take place over two connections: the first "discovery" connection to ++ obtain OAuth metadata from the server, and the second connection to send ++ the token after the client has obtained it. (libpq does not currently ++ implement a caching method as part of its builtin flow, so it uses the ++ two-connection exchange.) ++ ++ ++ ++ This mechanism is client-initiated, like SCRAM. The client initial response ++ consists of the standard "GS2" header used by SCRAM, followed by a list of ++ key=value pairs. The only key currently supported by ++ the server is auth, which contains the bearer token. ++ OAUTHBEARER additionally specifies three optional ++ components of the client initial response (the authzid of ++ the GS2 header, and the host and ++ port keys) which are currently ignored by the ++ server. ++ ++ ++ ++ OAUTHBEARER does not support channel binding, and there ++ is no "OAUTHBEARER-PLUS" mechanism. This mechanism does not make use of ++ server data during a successful authentication, so the ++ AuthenticationSASLFinal message is not used in the exchange. ++ ++ ++ ++ Example ++ ++ ++ During the first exchange, the server sends an AuthenticationSASL message ++ with the OAUTHBEARER mechanism advertised. ++ ++ ++ ++ ++ ++ The client responds by sending a SASLInitialResponse message which ++ indicates the OAUTHBEARER mechanism. Assuming the ++ client does not already have a valid bearer token for the current user, ++ the auth field is empty, indicating a discovery ++ connection. ++ ++ ++ ++ ++ ++ Server sends an AuthenticationSASLContinue message containing an error ++ status alongside a well-known URI and scopes that the ++ client should use to conduct an OAuth flow. ++ ++ ++ ++ ++ ++ Client sends a SASLResponse message containing the empty set (a single ++ 0x01 byte) to finish its half of the discovery ++ exchange. ++ ++ ++ ++ ++ ++ Server sends an ErrorMessage to fail the first exchange. ++ ++ ++ At this point, the client conducts one of many possible OAuth flows to ++ obtain a bearer token, using any metadata that it has been configured with ++ in addition to that provided by the server. (This description is left ++ deliberately vague; OAUTHBEARER does not specify or ++ mandate any particular method for obtaining a token.) ++ ++ ++ Once it has a token, the client reconnects to the server for the final ++ exchange: ++ ++ ++ ++ ++ ++ The server once again sends an AuthenticationSASL message with the ++ OAUTHBEARER mechanism advertised. ++ ++ ++ ++ ++ ++ The client responds by sending a SASLInitialResponse message, but this ++ time the auth field in the message contains the ++ bearer token that was obtained during the client flow. ++ ++ ++ ++ ++ ++ The server validates the token according to the instructions of the ++ token provider. If the client is authorized to connect, it sends an ++ AuthenticationOk message to end the SASL exchange. ++ ++ ++ ++ + + + + ## doc/src/sgml/regress.sgml ## @@ doc/src/sgml/regress.sgml: make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption' @@ meson.build: endif + libcurl = dependency('libcurl', version: '>= 7.61.0', required: libcurlopt) + if libcurl.found() + cdata.set('USE_LIBCURL', 1) ++ ++ # Check to see whether the current platform supports threadsafe Curl ++ # initialization. ++ libcurl_threadsafe_init = false ++ ++ if not meson.is_cross_build() ++ r = cc.run(''' ++ #include ++ ++ int main(void) ++ { ++ curl_version_info_data *info; ++ ++ if (curl_global_init(CURL_GLOBAL_ALL)) ++ return -1; ++ ++ info = curl_version_info(CURLVERSION_NOW); ++ #ifdef CURL_VERSION_THREADSAFE ++ if (info->features & CURL_VERSION_THREADSAFE) ++ return 0; ++ #endif ++ ++ return 1; ++ }''', ++ name: 'test for curl_global_init thread safety', ++ dependencies: libcurl, ++ ) ++ ++ assert(r.compiled()) ++ if r.returncode() == 0 ++ libcurl_threadsafe_init = true ++ message('curl_global_init is threadsafe') ++ elif r.returncode() == 1 ++ message('curl_global_init is not threadsafe') ++ else ++ message('curl_global_init failed; assuming not threadsafe') ++ endif ++ endif ++ ++ if libcurl_threadsafe_init ++ cdata.set('HAVE_THREADSAFE_CURL_GLOBAL_INIT', 1) ++ endif + endif ++ +else + libcurl = not_found_dep +endif @@ src/backend/utils/misc/postgresql.conf.sample #ssl_passphrase_command_supports_reload = off +# OAuth -+#oauth_validator_libraries = '' ++#oauth_validator_libraries = '' # comma-separated list of trusted validator modules + #------------------------------------------------------------------------------ @@ src/include/pg_config.h.in /* Define to 1 if you have the `ldap' library (-lldap). */ #undef HAVE_LIBLDAP +@@ + /* Define to 1 if you have the header file. */ + #undef HAVE_TERMIOS_H + ++/* Define to 1 if curl_global_init() is guaranteed to be threadsafe. */ ++#undef HAVE_THREADSAFE_CURL_GLOBAL_INIT ++ + /* Define to 1 if your compiler understands `typeof' or something similar. */ + #undef HAVE_TYPEOF + @@ /* Define to 1 to build with LDAP support. (--with-ldap) */ #undef USE_LDAP @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + return true; +} + ++#define HTTPS_SCHEME "https://" +#define OAUTH_GRANT_TYPE_DEVICE_CODE "urn:ietf:params:oauth:grant-type:device_code" + +/* + * Ensure that the provider supports the Device Authorization flow (i.e. it -+ * accepts the device_code grant type and provides an authorization endpoint). ++ * provides an authorization endpoint, and both the token and authorization ++ * endpoint URLs seem reasonable). + */ +static bool +check_for_device_flow(struct async_ctx *actx) +{ + const struct provider *provider = &actx->provider; -+ const struct curl_slist *grant; -+ bool device_grant_found = false; + + Assert(provider->issuer); /* ensured by parse_provider() */ -+ -+ /*------ -+ * First, sanity checks for discovery contents that are OPTIONAL in the -+ * spec but required for our flow: -+ * - the issuer must support the device_code grant -+ * - the issuer must have actually given us a -+ * device_authorization_endpoint -+ */ -+ -+ grant = provider->grant_types_supported; -+ while (grant) -+ { -+ if (strcmp(grant->data, OAUTH_GRANT_TYPE_DEVICE_CODE) == 0) -+ { -+ device_grant_found = true; -+ break; -+ } -+ -+ grant = grant->next; -+ } -+ -+ if (!device_grant_found) -+ { -+ actx_error(actx, "issuer \"%s\" does not support device code grants", -+ provider->issuer); -+ return false; -+ } ++ Assert(provider->token_endpoint); /* ensured by parse_provider() */ + + if (!provider->device_authorization_endpoint) + { @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + return false; + } + -+ /* TODO: check that the endpoint uses HTTPS */ ++ /* ++ * The original implementation checked that OAUTH_GRANT_TYPE_DEVICE_CODE ++ * was present in the discovery document's grant_types_supported list. MS ++ * Entra does not advertise this grant type, though, and since it doesn't ++ * make sense to stand up a device_authorization_endpoint without also ++ * accepting device codes at the token_endpoint, that's the only thing we ++ * currently require. ++ */ ++ ++ /* ++ * Although libcurl will fail later if the URL contains an unsupported ++ * scheme, that error message is going to be a bit opaque. This is a ++ * decent time to bail out if we're not using HTTPS for the endpoints ++ * we'll use for the flow. ++ */ ++ if (!actx->debugging) ++ { ++ if (pg_strncasecmp(provider->device_authorization_endpoint, ++ HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0) ++ { ++ actx_error(actx, ++ "device authorization endpoint \"%s\" must use HTTPS", ++ provider->device_authorization_endpoint); ++ return false; ++ } ++ ++ if (pg_strncasecmp(provider->token_endpoint, ++ HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0) ++ { ++ actx_error(actx, ++ "token endpoint \"%s\" must use HTTPS", ++ provider->token_endpoint); ++ return false; ++ } ++ } + + return true; +} @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + return true; +} + ++/* ++ * Calls curl_global_init() in a thread-safe way. ++ * ++ * libcurl has stringent requirements for the thread context in which you call ++ * curl_global_init(), because it's going to try initializing a bunch of other ++ * libraries (OpenSSL, Winsock, etc). Recent versions of libcurl have improved ++ * the thread-safety situation, but there's a chicken-and-egg problem at ++ * runtime: you can't check the thread safety until you've initialized libcurl, ++ * which you can't do from within a thread unless you know it's thread-safe... ++ * ++ * Returns true if initialization was successful. Successful or not, this ++ * function will not try to reinitialize Curl on successive calls. ++ */ ++static bool ++initialize_curl(PGconn *conn) ++{ ++ /* ++ * Don't let the compiler play tricks with this variable. In the ++ * HAVE_THREADSAFE_CURL_GLOBAL_INIT case, we don't care if two threads ++ * enter simultaneously, but we do care if this gets set transiently to ++ * PG_BOOL_YES/NO in cases where that's not the final answer. ++ */ ++ static volatile PGTernaryBool init_successful = PG_BOOL_UNKNOWN; ++#if HAVE_THREADSAFE_CURL_GLOBAL_INIT ++ curl_version_info_data *info; ++#endif ++ ++#if !HAVE_THREADSAFE_CURL_GLOBAL_INIT ++ ++ /* ++ * Lock around the whole function. If a libpq client performs its own work ++ * with libcurl, it must either ensure that Curl is initialized safely ++ * before calling us (in which case our call will be a no-op), or else it ++ * must guard its own calls to curl_global_init() with a registered ++ * threadlock handler. See PQregisterThreadLock(). ++ */ ++ pglock_thread(); ++#endif ++ ++ /* ++ * Skip initialization if we've already done it. (Curl tracks the number ++ * of calls; there's no point in incrementing the counter every time we ++ * connect.) ++ */ ++ if (init_successful == PG_BOOL_YES) ++ goto done; ++ else if (init_successful == PG_BOOL_NO) ++ { ++ libpq_append_conn_error(conn, ++ "curl_global_init previously failed during OAuth setup"); ++ goto done; ++ } ++ ++ /* ++ * We know we've already initialized Winsock by this point (see ++ * pqMakeEmptyPGconn()), so we should be able to safely skip that bit. But ++ * we have to tell libcurl to initialize everything else, because other ++ * pieces of our client executable may already be using libcurl for their ++ * own purposes. If we initialize libcurl with only a subset of its ++ * features, we could break those other clients nondeterministically, and ++ * that would probably be a nightmare to debug. ++ * ++ * If some other part of the program has already called this, it's a ++ * no-op. ++ */ ++ if (curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32) != CURLE_OK) ++ { ++ libpq_append_conn_error(conn, ++ "curl_global_init failed during OAuth setup"); ++ init_successful = PG_BOOL_NO; ++ goto done; ++ } ++ ++#if HAVE_THREADSAFE_CURL_GLOBAL_INIT ++ ++ /* ++ * If we determined at configure time that the Curl installation is ++ * threadsafe, our job here is much easier. We simply initialize above ++ * without any locking (concurrent or duplicated calls are fine in that ++ * situation), then double-check to make sure the runtime setting agrees, ++ * to try to catch silent downgrades. ++ */ ++ info = curl_version_info(CURLVERSION_NOW); ++ if (!(info->features & CURL_VERSION_THREADSAFE)) ++ { ++ /* ++ * In a downgrade situation, the damage is already done. Curl global ++ * state may be corrupted. Be noisy. ++ */ ++ libpq_append_conn_error(conn, "libcurl is no longer threadsafe\n" ++ "\tCurl initialization was reported threadsafe when libpq\n" ++ "\twas compiled, but the currently installed version of\n" ++ "\tlibcurl reports that it is not. Recompile libpq against\n" ++ "\tthe installed version of libcurl."); ++ init_successful = PG_BOOL_NO; ++ goto done; ++ } ++#endif ++ ++ init_successful = PG_BOOL_YES; ++ ++done: ++#if !HAVE_THREADSAFE_CURL_GLOBAL_INIT ++ pgunlock_thread(); ++#endif ++ return (init_successful == PG_BOOL_YES); ++} + +/* + * The core nonblocking libcurl implementation. This will be called several @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + fe_oauth_state *state = conn->sasl_state; + struct async_ctx *actx; + -+ /* -+ * XXX This is not safe. libcurl has stringent requirements for the thread -+ * context in which you call curl_global_init(), because it's going to try -+ * initializing a bunch of other libraries (OpenSSL, Winsock...). And we -+ * probably need to consider both the TLS backend libcurl is compiled -+ * against and what the user has asked us to do via PQinit[Open]SSL. -+ * -+ * Recent versions of libcurl have improved the thread-safety situation, -+ * but you apparently can't check at compile time whether the -+ * implementation is thread-safe, and there's a chicken-and-egg problem -+ * where you can't check the thread safety until you've initialized -+ * libcurl, which you can't do before you've made sure it's thread-safe... -+ * -+ * We know we've already initialized Winsock by this point, so we should -+ * be able to safely skip that bit. But we have to tell libcurl to -+ * initialize everything else, because other pieces of our client -+ * executable may already be using libcurl for their own purposes. If we -+ * initialize libcurl first, with only a subset of its features, we could -+ * break those other clients nondeterministically, and that would probably -+ * be a nightmare to debug. -+ */ -+ curl_global_init(CURL_GLOBAL_ALL -+ & ~CURL_GLOBAL_WIN32); /* we already initialized Winsock */ ++ if (!initialize_curl(conn)) ++ return PGRES_POLLING_FAILED; + + if (!state->async_ctx) + { @@ src/interfaces/libpq/fe-connect.c: static const internalPQconninfoOption PQconni /* Terminating entry --- MUST BE LAST */ {NULL, NULL, NULL, NULL, NULL, NULL, 0} +@@ src/interfaces/libpq/fe-connect.c: static const PQEnvironmentOption EnvironmentOptions[] = + static const pg_fe_sasl_mech *supported_sasl_mechs[] = + { + &pg_scram_mech, ++ &pg_oauth_mech, + }; + #define SASL_MECHANISM_COUNT lengthof(supported_sasl_mechs) + @@ src/interfaces/libpq/fe-connect.c: pqDropServerData(PGconn *conn) conn->write_failed = false; free(conn->write_err_msg); @@ src/interfaces/libpq/fe-connect.c: static inline void * 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: fill_allowed_sasl_mechs(PGconn *conn) - * - handle the new mechanism name in the require_auth portion of - * pqConnectOptions2(), below. - */ -- StaticAssertDecl(lengthof(conn->allowed_sasl_mechs) == 1, -+ StaticAssertDecl(lengthof(conn->allowed_sasl_mechs) == 2, - "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) { mech = &pg_scram_mech; 5: 66ef3b4b687 = 5: 035a3832b40 XXX fix libcurl link error 6: 4df1bc59638 ! 6: 5e360725bf9 DO NOT MERGE: Add pytest suite for OAuth @@ src/test/python/client/test_oauth.py (new) + { + "issuer": "{issuer}", + "token_endpoint": "https://256.256.256.256/token", -+ "device_authorization_endpoint": "https://256.256.256.256/dev", -+ }, -+ ), -+ r'cannot run OAuth device authorization: issuer "https://.*" does not support device code grants', -+ id="missing device code grants", -+ ), -+ pytest.param( -+ ( -+ 200, -+ { -+ "issuer": "{issuer}", -+ "token_endpoint": "https://256.256.256.256/token", + "grant_types_supported": [ + "urn:ietf:params:oauth:grant-type:device_code" + ],