since-v43.diff.txt

text/plain

Filename: since-v43.diff.txt
Type: text/plain
Part: 0
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER
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 <curl/curl.h> 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/curl.h>
    ++],[
    ++    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 <curl/curl.h> 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 <curl/curl.h>
    ++
    ++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 <curl/curl.h> 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 <ossp/uuid.h> or <uuid.h> is required for OSSP UUID])])])
    - fi
    - 
    -+if test "$with_libcurl" = yes; then
    -+  AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file <curl/curl.h> 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         <replaceable>directory</replaceable>
    @@ doc/src/sgml/libpq.sgml: void PQinitSSL(int do_ssl);
      
       <sect1 id="libpq-threading">
        <title>Behavior in Threaded Programs</title>
    +@@ doc/src/sgml/libpq.sgml: int PQisthreadsafe();
    +    <application>libpq</application> source code for a way to do cooperative
    +    locking between <application>libpq</application> and your application.
    +   </para>
    ++
    ++  <para>
    ++   Similarly, if you are using Curl inside your application,
    ++   <emphasis>and</emphasis> you do not already
    ++   <ulink url="https://curl.se/libcurl/c/curl_global_init.html">initialize
    ++   libcurl globally</ulink> before starting new threads, you will need to
    ++   cooperatively lock (again via <function>PQregisterThreadLock</function>)
    ++   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
    ++   <literal>threadsafe</literal> feature in their version metadata.
    ++  </para>
    +  </sect1>
    + 
    + 
     
      ## doc/src/sgml/oauth-validators.sgml (new) ##
     @@
    @@ doc/src/sgml/postgres.sgml: break is not needed in a wider output rendering.
       </part>
      
     
    + ## doc/src/sgml/protocol.sgml ##
    +@@ doc/src/sgml/protocol.sgml: SELCT 1/0;<!-- this typo is intentional -->
    + 
    +   <para>
    +    <firstterm>SASL</firstterm> is a framework for authentication in connection-oriented
    +-   protocols. At the moment, <productname>PostgreSQL</productname> 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, <productname>PostgreSQL</productname> 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.
    +   </para>
    + 
    +   <procedure>
    +@@ doc/src/sgml/protocol.sgml: SELCT 1/0;<!-- this typo is intentional -->
    +    <step id="sasl-auth-end">
    +     <para>
    +      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;<!-- this typo is intentional -->
    +    <title>SCRAM-SHA-256 Authentication</title>
    + 
    +    <para>
    +-    The implemented SASL mechanisms at the moment
    +-    are <literal>SCRAM-SHA-256</literal> and its variant with channel
    +-    binding <literal>SCRAM-SHA-256-PLUS</literal>. They are described in
    ++    <literal>SCRAM-SHA-256</literal>, and its variant with channel
    ++    binding <literal>SCRAM-SHA-256-PLUS</literal>, are password-based
    ++    authentication mechanisms. They are described in
    +     detail in <ulink url="https://datatracker.ietf.org/doc/html/rfc7677">RFC 7677</ulink>
    +     and <ulink url="https://datatracker.ietf.org/doc/html/rfc5802">RFC 5802</ulink>.
    +    </para>
    +@@ doc/src/sgml/protocol.sgml: SELCT 1/0;<!-- this typo is intentional -->
    +     </step>
    +    </procedure>
    +   </sect2>
    ++
    ++  <sect2 id="sasl-oauthbearer">
    ++   <title>OAUTHBEARER Authentication</title>
    ++
    ++   <para>
    ++    <literal>OAUTHBEARER</literal> is a token-based mechanism for federated
    ++    authentication. It is described in detail in
    ++    <ulink url="https://datatracker.ietf.org/doc/html/rfc7628">RFC 7628</ulink>.
    ++   </para>
    ++
    ++   <para>
    ++    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.)
    ++   </para>
    ++
    ++   <para>
    ++    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
    ++    <literal>key=value</literal> pairs. The only key currently supported by
    ++    the server is <literal>auth</literal>, which contains the bearer token.
    ++    <literal>OAUTHBEARER</literal> additionally specifies three optional
    ++    components of the client initial response (the <literal>authzid</literal> of
    ++    the GS2 header, and the <structfield>host</structfield> and
    ++    <structfield>port</structfield> keys) which are currently ignored by the
    ++    server.
    ++   </para>
    ++
    ++   <para>
    ++    <literal>OAUTHBEARER</literal> 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.
    ++   </para>
    ++
    ++   <procedure>
    ++    <title>Example</title>
    ++    <step>
    ++     <para>
    ++      During the first exchange, the server sends an AuthenticationSASL message
    ++      with the <literal>OAUTHBEARER</literal> mechanism advertised.
    ++     </para>
    ++    </step>
    ++
    ++    <step>
    ++     <para>
    ++      The client responds by sending a SASLInitialResponse message which
    ++      indicates the <literal>OAUTHBEARER</literal> mechanism. Assuming the
    ++      client does not already have a valid bearer token for the current user,
    ++      the <structfield>auth</structfield> field is empty, indicating a discovery
    ++      connection.
    ++     </para>
    ++    </step>
    ++
    ++    <step>
    ++     <para>
    ++      Server sends an AuthenticationSASLContinue message containing an error
    ++      <literal>status</literal> alongside a well-known URI and scopes that the
    ++      client should use to conduct an OAuth flow.
    ++     </para>
    ++    </step>
    ++
    ++    <step>
    ++     <para>
    ++      Client sends a SASLResponse message containing the empty set (a single
    ++      <literal>0x01</literal> byte) to finish its half of the discovery
    ++      exchange.
    ++     </para>
    ++    </step>
    ++
    ++    <step>
    ++     <para>
    ++      Server sends an ErrorMessage to fail the first exchange.
    ++     </para>
    ++     <para>
    ++      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; <literal>OAUTHBEARER</literal> does not specify or
    ++      mandate any particular method for obtaining a token.)
    ++     </para>
    ++     <para>
    ++      Once it has a token, the client reconnects to the server for the final
    ++      exchange:
    ++     </para>
    ++    </step>
    ++
    ++    <step>
    ++     <para>
    ++      The server once again sends an AuthenticationSASL message with the
    ++      <literal>OAUTHBEARER</literal> mechanism advertised.
    ++     </para>
    ++    </step>
    ++
    ++    <step>
    ++     <para>
    ++      The client responds by sending a SASLInitialResponse message, but this
    ++      time the <structfield>auth</structfield> field in the message contains the
    ++      bearer token that was obtained during the client flow.
    ++     </para>
    ++    </step>
    ++
    ++    <step>
    ++     <para>
    ++      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.
    ++     </para>
    ++    </step>
    ++   </procedure>
    ++  </sect2>
    +  </sect1>
    + 
    +  <sect1 id="protocol-replication">
    +
      ## doc/src/sgml/regress.sgml ##
     @@ doc/src/sgml/regress.sgml: make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
            </para>
    @@ 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 <curl/curl.h>
    ++
    ++        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 <termios.h> 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"
     +                    ],