since-v35.diff.txt

text/plain

Filename: since-v35.diff.txt
Type: text/plain
Part: 0
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER
1:  dc4f869365 ! 1:  5730b875b8 Add OAUTHBEARER SASL mechanism
    @@ Commit message
         Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
     
      ## .cirrus.tasks.yml ##
    +@@ .cirrus.tasks.yml: env:
    +   MTEST_ARGS: --print-errorlogs --no-rebuild -C build
    +   PGCTLTIMEOUT: 120 # avoids spurious failures during parallel tests
    +   TEMP_CONFIG: ${CIRRUS_WORKING_DIR}/src/tools/ci/pg_ci_base.conf
    +-  PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance
    ++  PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance oauth
    + 
    + 
    + # What files to preserve in case tests fail
     @@ .cirrus.tasks.yml: task:
          chown root:postgres /tmp/cores
          sysctl kern.corefile='/tmp/cores/%N.%P.core'
    @@ .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 \
    -         -DPG_TEST_EXTRA="$PG_TEST_EXTRA" \
    -+        -Doauth=curl \
    ++        -Dbuiltin_oauth=curl \
              -Dextra_lib_dirs=/usr/local/lib -Dextra_include_dirs=/usr/local/include/ \
              build
          EOF
    @@ .cirrus.tasks.yml: LINUX_CONFIGURE_FEATURES: &LINUX_CONFIGURE_FEATURES >-
        --with-libxslt
        --with-llvm
        --with-lz4
    -+  --with-oauth=curl
    ++  --with-builtin-oauth=curl
        --with-pam
        --with-perl
        --with-python
    @@ .cirrus.tasks.yml: LINUX_CONFIGURE_FEATURES: &LINUX_CONFIGURE_FEATURES >-
      
      LINUX_MESON_FEATURES: &LINUX_MESON_FEATURES >-
        -Dllvm=enabled
    -+  -Doauth=curl
    ++  -Dbuiltin_oauth=curl
        -Duuid=e2fs
      
      
    @@ configure: with_uuid
      with_readline
      with_systemd
      with_selinux
    -+with_oauth
    ++with_builtin_oauth
      with_ldap
      with_krb_srvnam
      krb_srvtab
    @@ configure: with_krb_srvnam
      with_pam
      with_bsd_auth
      with_ldap
    -+with_oauth
    ++with_builtin_oauth
      with_bonjour
      with_selinux
      with_systemd
    @@ configure: Optional Packages:
        --with-pam              build with PAM support
        --with-bsd-auth         build with BSD Authentication support
        --with-ldap             build with LDAP support
    -+  --with-oauth=LIB        use LIB for OAuth 2.0 support (curl)
    ++  --with-builtin-oauth=LIB
    ++                          use LIB for built-in OAuth 2.0 client flows (curl)
        --with-bonjour          build with Bonjour support
        --with-selinux          build with SELinux support
        --with-systemd          build with systemd support
    @@ configure: $as_echo "$with_ldap" >&6; }
     +#
     +# OAuth 2.0
     +#
    -+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with OAuth support" >&5
    -+$as_echo_n "checking whether to build with OAuth support... " >&6; }
    ++{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with built-in OAuth client support" >&5
    ++$as_echo_n "checking whether to build with built-in OAuth client support... " >&6; }
     +
     +
     +
    -+# Check whether --with-oauth was given.
    -+if test "${with_oauth+set}" = set; then :
    -+  withval=$with_oauth;
    ++# Check whether --with-builtin-oauth was given.
    ++if test "${with_builtin_oauth+set}" = set; then :
    ++  withval=$with_builtin_oauth;
     +  case $withval in
     +    yes)
    -+      as_fn_error $? "argument required for --with-oauth option" "$LINENO" 5
    ++      as_fn_error $? "argument required for --with-builtin-oauth option" "$LINENO" 5
     +      ;;
     +    no)
    -+      as_fn_error $? "argument required for --with-oauth option" "$LINENO" 5
    ++      as_fn_error $? "argument required for --with-builtin-oauth option" "$LINENO" 5
     +      ;;
     +    *)
     +
    @@ configure: $as_echo "$with_ldap" >&6; }
     +fi
     +
     +
    -+if test x"$with_oauth" = x"" ; then
    -+  with_oauth=no
    ++if test x"$with_builtin_oauth" = x"" ; then
    ++  with_builtin_oauth=no
     +fi
     +
    -+if test x"$with_oauth" = x"curl"; then
    ++if test x"$with_builtin_oauth" = x"curl"; then
     +
    -+$as_echo "#define USE_OAUTH 1" >>confdefs.h
    ++$as_echo "#define USE_BUILTIN_OAUTH 1" >>confdefs.h
     +
     +
     +$as_echo "#define USE_OAUTH_CURL 1" >>confdefs.h
    @@ configure: $as_echo "$with_ldap" >&6; }
     +    { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests requires --with-python to run" >&5
     +$as_echo "$as_me: WARNING: *** OAuth support tests requires --with-python to run" >&2;}
     +  fi
    -+elif test x"$with_oauth" != x"no"; then
    -+  as_fn_error $? "--with-oauth must specify curl" "$LINENO" 5
    ++elif test x"$with_builtin_oauth" != x"no"; then
    ++  as_fn_error $? "--with-builtin-oauth must specify curl" "$LINENO" 5
     +fi
     +
    -+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_oauth" >&5
    -+$as_echo "$with_oauth" >&6; }
    ++{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_builtin_oauth" >&5
    ++$as_echo "$with_builtin_oauth" >&6; }
     +
     +
     +
    @@ configure: $as_echo "$with_ldap" >&6; }
      #
     @@ configure: fi
      
    + fi
      
    - 
    -+if test "$with_oauth" = curl ; then
    ++# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
    ++# during gss_acquire_cred(). This is possibly related to Curl's Heimdal
    ++# dependency on that platform?
    ++if test "$with_builtin_oauth" = curl ; then
     +  { $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-oauth=curl" "$LINENO" 5
    ++  as_fn_error $? "library 'curl' is required for --with-builtin-oauth=curl" "$LINENO" 5
     +fi
     +
     +  { $as_echo "$as_me:${as_lineno-$LINENO}: checking for compatible libcurl" >&5
    @@ configure: fi
     +fi
     +fi
     +
    - # for contrib/sepgsql
    - if test "$with_selinux" = yes; then
    -   { $as_echo "$as_me:${as_lineno-$LINENO}: checking for security_compute_create_name in -lselinux" >&5
    + 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_oauth" = curl; then
    ++if test "$with_builtin_oauth" = curl; 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 OAuth" "$LINENO" 5
    ++  as_fn_error $? "header file <curl/curl.h> is required for --with-builtin-oauth=curl" "$LINENO" 5
     +fi
     +
     +
    @@ configure.ac: AC_MSG_RESULT([$with_ldap])
     +#
     +# OAuth 2.0
     +#
    -+AC_MSG_CHECKING([whether to build with OAuth support])
    -+PGAC_ARG_REQ(with, oauth, [LIB], [use LIB for OAuth 2.0 support (curl)])
    -+if test x"$with_oauth" = x"" ; then
    -+  with_oauth=no
    ++AC_MSG_CHECKING([whether to build with built-in OAuth client support])
    ++PGAC_ARG_REQ(with, builtin-oauth, [LIB], [use LIB for built-in OAuth 2.0 client flows (curl)])
    ++if test x"$with_builtin_oauth" = x"" ; then
    ++  with_builtin_oauth=no
     +fi
     +
    -+if test x"$with_oauth" = x"curl"; then
    -+  AC_DEFINE([USE_OAUTH], 1, [Define to 1 to build with OAuth 2.0 support. (--with-oauth)])
    -+  AC_DEFINE([USE_OAUTH_CURL], 1, [Define to 1 to use libcurl for OAuth support.])
    ++if test x"$with_builtin_oauth" = x"curl"; then
    ++  AC_DEFINE([USE_BUILTIN_OAUTH], 1, [Define to 1 to build with OAuth 2.0 client flows. (--with-builtin-oauth)])
    ++  AC_DEFINE([USE_OAUTH_CURL], 1, [Define to 1 to use libcurl for OAuth client flows.])
     +  # OAuth requires python for testing
     +  if test "$with_python" != yes; then
     +    AC_MSG_WARN([*** OAuth support tests requires --with-python to run])
     +  fi
    -+elif test x"$with_oauth" != x"no"; then
    -+  AC_MSG_ERROR([--with-oauth must specify curl])
    ++elif test x"$with_builtin_oauth" != x"no"; then
    ++  AC_MSG_ERROR([--with-builtin-oauth must specify curl])
     +fi
     +
    -+AC_MSG_RESULT([$with_oauth])
    -+AC_SUBST(with_oauth)
    ++AC_MSG_RESULT([$with_builtin_oauth])
    ++AC_SUBST(with_builtin_oauth)
     +
     +
      #
      # Bonjour
      #
    -@@ configure.ac: fi
    - AC_SUBST(LDAP_LIBS_FE)
    - AC_SUBST(LDAP_LIBS_BE)
    +@@ configure.ac: failure.  It is possible the compiler isn't looking in the proper directory.
    + Use --without-zlib to disable zlib support.])])
    + fi
      
    -+if test "$with_oauth" = curl ; then
    -+  AC_CHECK_LIB(curl, curl_multi_init, [], [AC_MSG_ERROR([library 'curl' is required for --with-oauth=curl])])
    ++# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
    ++# during gss_acquire_cred(). This is possibly related to Curl's Heimdal
    ++# dependency on that platform?
    ++if test "$with_builtin_oauth" = curl ; then
    ++  AC_CHECK_LIB(curl, curl_multi_init, [], [AC_MSG_ERROR([library 'curl' is required for --with-builtin-oauth=curl])])
     +  PGAC_CHECK_LIBCURL
     +fi
     +
    - # for contrib/sepgsql
    - if test "$with_selinux" = yes; then
    -   AC_CHECK_LIB(selinux, security_compute_create_name, [],
    + 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_oauth" = curl; then
    -+  AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file <curl/curl.h> is required for OAuth])])
    ++if test "$with_builtin_oauth" = curl; then
    ++  AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file <curl/curl.h> is required for --with-builtin-oauth=curl])])
     +fi
     +
      if test "$PORTNAME" = "win32" ; then
    @@ doc/src/sgml/installation.sgml: build-postgresql:
             </listitem>
            </varlistentry>
      
    -+      <varlistentry id="configure-option-with-oauth">
    -+       <term><option>--with-oauth=<replaceable>LIBRARY</replaceable></option></term>
    ++      <varlistentry id="configure-option-with-builtin-oauth">
    ++       <term><option>--with-builtin-oauth=<replaceable>LIBRARY</replaceable></option></term>
     +       <listitem>
     +        <para>
    -+         Build with OAuth authentication and authorization support.  The only
    ++         Build with support for OAuth 2.0 client flows.  The only
     +         <replaceable>LIBRARY</replaceable> supported is <option>curl</option>.
     +         This requires the <productname>curl</productname> package to be
     +         installed.  Building with this will check for the required header files
    @@ doc/src/sgml/installation.sgml: ninja install
            </listitem>
           </varlistentry>
      
    -+     <varlistentry id="configure-with-oauth">
    -+      <term><option>-Doauth={ auto | <replaceable>LIBRARY</replaceable> | disabled }</option></term>
    ++     <varlistentry id="configure-with-builtin-oauth">
    ++      <term><option>-Dbuiltin_oauth={ auto | <replaceable>LIBRARY</replaceable> | disabled }</option></term>
     +      <listitem>
     +       <para>
    -+        Build with OAuth authentication and authorization support.  The only
    ++        Build with support for OAuth 2.0 client flows.  The only
     +        <replaceable>LIBRARY</replaceable> supported is <option>curl</option>.
     +        This requires the <productname>curl</productname> package to be
     +        installed.  Building with this will check for the required header files
    @@ meson.build: endif
      
      
     +###############################################################
    -+# Library: oauth
    ++# Library: OAuth (libcurl)
     +###############################################################
     +
    -+oauth = not_found_dep
    ++libcurl = not_found_dep
     +oauth_library = 'none'
    -+oauthopt = get_option('oauth')
    ++oauthopt = get_option('builtin_oauth')
     +
     +if oauthopt == 'auto' and auto_features.disabled()
     +  oauthopt = 'none'
    @@ meson.build: endif
     +if oauthopt in ['auto', 'curl']
     +  # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability
     +  # to explicitly set TLS 1.3 ciphersuites).
    -+  oauth = dependency('libcurl', version: '>= 7.61.0', required: (oauthopt == 'curl'))
    -+
    -+  if oauth.found()
    ++  libcurl = dependency('libcurl', version: '>= 7.61.0',
    ++                       required: (oauthopt == 'curl'))
    ++  if libcurl.found()
     +    oauth_library = 'curl'
    -+    cdata.set('USE_OAUTH', 1)
    ++    cdata.set('USE_BUILTIN_OAUTH', 1)
     +    cdata.set('USE_OAUTH_CURL', 1)
     +  endif
     +endif
     +
    -+if oauthopt == 'auto' and auto_features.enabled() and not oauth.found()
    ++if oauthopt == 'auto' and auto_features.enabled() and not libcurl.found()
     +  error('no OAuth implementation library found')
     +endif
     +
    @@ meson.build: endif
      # Library: Tcl (for pltcl)
      #
     @@ meson.build: libpq_deps += [
    + 
        gssapi,
        ldap_r,
    ++  # XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
    ++  # during gss_acquire_cred(). This is possibly related to Curl's Heimdal
    ++  # dependency on that platform?
    ++  libcurl,
        libintl,
    -+  oauth,
        ssl,
      ]
    - 
     @@ meson.build: if meson.version().version_compare('>=0.57')
    +       'gss': gssapi,
    +       'icu': icu,
    +       'ldap': ldap,
    ++      'libcurl': libcurl,
    +       'libxml': libxml,
    +       'libxslt': libxslt,
            'llvm': llvm,
    -       'lz4': lz4,
    -       'nls': libintl,
    -+      'oauth': oauth,
    -       'openssl': ssl,
    -       'pam': pam,
    -       'plperl': perl_dep,
     
      ## meson_options.txt ##
    -@@ meson_options.txt: option('lz4', type: 'feature', value: 'auto',
    - option('nls', type: 'feature', value: 'auto',
    -   description: 'Native language support')
    +@@ meson_options.txt: option('bonjour', type: 'feature', value: 'auto',
    + option('bsd_auth', type: 'feature', value: 'auto',
    +   description: 'BSD Authentication support')
      
    -+option('oauth', type : 'combo', choices : ['auto', 'none', 'curl'],
    ++option('builtin_oauth', type : 'combo', choices : ['auto', 'none', 'curl'],
     +  value: 'auto',
    -+  description: 'use LIB for OAuth 2.0 support (curl)')
    ++  description: 'use LIB for built-in OAuth 2.0 client flows (curl)')
     +
    - option('pam', type: 'feature', value: 'auto',
    -   description: 'PAM support')
    + option('docs', type: 'feature', value: 'auto',
    +   description: 'Documentation in HTML and man page format')
      
     
      ## src/Makefile.global.in ##
    @@ src/Makefile.global.in: with_ldap	= @with_ldap@
      with_libxml	= @with_libxml@
      with_libxslt	= @with_libxslt@
      with_llvm	= @with_llvm@
    -+with_oauth	= @with_oauth@
    ++with_builtin_oauth = @with_builtin_oauth@
      with_system_tzdata = @with_system_tzdata@
      with_uuid	= @with_uuid@
      with_zlib	= @with_zlib@
    @@ src/include/pg_config.h.in
      /* Define to 1 if you have the `ldap' library (-lldap). */
      #undef HAVE_LIBLDAP
      
    +@@
    + /* Define to 1 to build with BSD Authentication support. (--with-bsd-auth) */
    + #undef USE_BSD_AUTH
    + 
    ++/* Define to 1 to build with OAuth 2.0 client flows. (--with-builtin-oauth) */
    ++#undef USE_BUILTIN_OAUTH
    ++
    + /* Define to build with ICU support. (--with-icu) */
    + #undef USE_ICU
    + 
     @@
      /* Define to select named POSIX semaphores. */
      #undef USE_NAMED_POSIX_SEMAPHORES
      
    -+/* Define to 1 to build with OAuth 2.0 support. (--with-oauth) */
    -+#undef USE_OAUTH
    -+
    -+/* Define to 1 to use libcurl for OAuth support. */
    ++/* Define to 1 to use libcurl for OAuth client flows. */
     +#undef USE_OAUTH_CURL
     +
      /* Define to 1 to build with OpenSSL support. (--with-ssl=openssl) */
    @@ src/include/pg_config.h.in
      
     
      ## src/interfaces/libpq/Makefile ##
    +@@ src/interfaces/libpq/Makefile: endif
    + 
    + OBJS = \
    + 	$(WIN32RES) \
    ++	fe-auth-oauth.o \
    + 	fe-auth-scram.o \
    + 	fe-cancel.o \
    + 	fe-connect.o \
     @@ src/interfaces/libpq/Makefile: OBJS += \
      	fe-secure-gssapi.o
      endif
      
    -+ifneq ($(with_oauth),no)
    -+OBJS += fe-auth-oauth.o
    -+
    -+ifeq ($(with_oauth),curl)
    ++ifeq ($(with_builtin_oauth),curl)
     +OBJS += fe-auth-oauth-curl.o
     +endif
    -+endif
     +
      ifeq ($(PORTNAME), cygwin)
      override shlib = cyg$(NAME)$(DLSUFFIX)
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +
     +	if (tok.access_token)
     +	{
    -+		/* Construct our Bearer token. */
    -+		resetPQExpBuffer(&actx->work_data);
    -+		appendPQExpBuffer(&actx->work_data, "Bearer %s", tok.access_token);
    -+
    -+		if (PQExpBufferDataBroken(actx->work_data))
    -+		{
    -+			actx_error(actx, "out of memory");
    -+			goto token_cleanup;
    -+		}
    -+
    -+		*token = strdup(actx->work_data.data);
    -+		if (!*token)
    -+		{
    -+			actx_error(actx, "out of memory");
    -+			goto token_cleanup;
    -+		}
    ++		*token = tok.access_token;
    ++		tok.access_token = NULL;
     +
     +		success = true;
     +		goto token_cleanup;
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +
     +#define kvsep "\x01"
     +
    ++/*
    ++ * Constructs an OAUTHBEARER client initial response (RFC 7628, Sec. 3.1).
    ++ *
    ++ * If discover is true, the token pointer will be ignored and the initial
    ++ * response will instead contain a request for the server's required OAuth
    ++ * parameters (Sec. 4.3). Otherwise, a bearer token must be provided.
    ++ *
    ++ * Returns the response as a null-terminated string, or NULL on error.
    ++ */
     +static char *
    -+client_initial_response(PGconn *conn, const char *token)
    ++client_initial_response(PGconn *conn, bool discover, const char *token)
     +{
    -+	static const char *const resp_format = "n,," kvsep "auth=%s" kvsep kvsep;
    ++	static const char *const resp_format = "n,," kvsep "auth=%s%s" kvsep kvsep;
     +
     +	PQExpBufferData buf;
    ++	const char *authn_scheme;
     +	char	   *response = NULL;
     +
    -+	if (!token)
    ++	if (discover)
    ++	{
    ++		/* Parameter discovery uses a completely empty auth value. */
    ++		authn_scheme = token = "";
    ++	}
    ++	else
     +	{
     +		/*
    -+		 * Either programmer error, or something went badly wrong during the
    -+		 * asynchronous fetch.
    -+		 *
    -+		 * TODO: users shouldn't see this; what action should they take if
    -+		 * they do?
    ++		 * Use a Bearer authentication scheme (RFC 6750, Sec. 2.1). A trailing
    ++		 * space is used as a separator.
     +		 */
    -+		libpq_append_conn_error(conn, "no OAuth token was set for the connection");
    -+		return NULL;
    ++		authn_scheme = "Bearer ";
    ++
    ++		/* We must have a token. */
    ++		if (!token)
    ++		{
    ++			/*
    ++			 * Either programmer error, or something went badly wrong during
    ++			 * the asynchronous fetch.
    ++			 *
    ++			 * TODO: users shouldn't see this; what action should they take if
    ++			 * they do?
    ++			 */
    ++			libpq_append_conn_error(conn, "no OAuth token was set for the connection");
    ++			return NULL;
    ++		}
     +	}
     +
     +	initPQExpBuffer(&buf);
    -+	appendPQExpBuffer(&buf, resp_format, token);
    ++	appendPQExpBuffer(&buf, resp_format, authn_scheme, token);
     +
     +	if (!PQExpBufferDataBroken(buf))
     +		response = strdup(buf.data);
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +		 * onto the original string, since it may not be safe for us to free()
     +		 * it.)
     +		 */
    -+		PQExpBufferData token;
    -+
     +		if (!request->token)
     +		{
     +			libpq_append_conn_error(conn, "user-defined OAuth flow did not provide a token");
     +			return PGRES_POLLING_FAILED;
     +		}
     +
    -+		initPQExpBuffer(&token);
    -+		appendPQExpBuffer(&token, "Bearer %s", request->token);
    -+
    -+		if (PQExpBufferDataBroken(token))
    ++		state->token = strdup(request->token);
    ++		if (!state->token)
     +		{
     +			libpq_append_conn_error(conn, "out of memory");
     +			return PGRES_POLLING_FAILED;
     +		}
     +
    -+		state->token = token.data;
     +		return PGRES_POLLING_OK;
     +	}
     +
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +			 * hold onto the original string, since it may not be safe for us
     +			 * to free() it.)
     +			 */
    -+			PQExpBufferData token;
    -+
    -+			initPQExpBuffer(&token);
    -+			appendPQExpBuffer(&token, "Bearer %s", request.token);
    -+
    -+			if (PQExpBufferDataBroken(token))
    ++			state->token = strdup(request.token);
    ++			if (!state->token)
     +			{
     +				libpq_append_conn_error(conn, "out of memory");
     +				goto fail;
     +			}
     +
    -+			state->token = token.data;
    -+
     +			/* short-circuit */
     +			if (request.cleanup)
     +				request.cleanup(conn, &request);
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	}
     +	else
     +	{
    ++#if USE_BUILTIN_OAUTH
     +		/*
    -+		 * Use our built-in OAuth flow.
    ++		 * Hand off to our built-in OAuth flow.
     +		 *
     +		 * Only allow one try per connection, since we're not performing any
     +		 * caching at the moment. (Custom flows might be more sophisticated.)
     +		 */
     +		conn->async_auth = pg_fe_run_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-builtin-oauth");
    ++		goto fail;
    ++
    ++#endif
     +	}
     +
     +	return true;
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +{
     +	fe_oauth_state *state = opaq;
     +	PGconn	   *conn = state->conn;
    ++	bool		discover = false;
     +
     +	*output = NULL;
     +	*outputlen = 0;
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	switch (state->state)
     +	{
     +		case FE_OAUTH_INIT:
    ++			/* We begin in the initial response phase. */
     +			Assert(inputlen == -1);
     +
     +			if (!derive_discovery_uri(conn))
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +			{
     +				/*
     +				 * If we don't have a discovery URI to be able to request a
    -+				 * token, we ask the server for one explicitly with an empty
    -+				 * token. This doesn't require any asynchronous work.
    ++				 * token, we ask the server for one explicitly. This doesn't
    ++				 * require any asynchronous work.
     +				 */
    -+				state->token = strdup("");
    -+				if (!state->token)
    -+				{
    -+					libpq_append_conn_error(conn, "out of memory");
    -+					return SASL_FAILED;
    -+				}
    ++				discover = true;
     +			}
     +
     +			/* fall through */
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +			/* We should still be in the initial response phase. */
     +			Assert(inputlen == -1);
     +
    -+			*output = client_initial_response(conn, state->token);
    ++			*output = client_initial_response(conn, discover, state->token);
     +			if (!*output)
     +				return SASL_FAILED;
     +
    @@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
      			conn->sasl = &pg_scram_mech;
      			conn->password_needed = true;
      		}
    -+#ifdef USE_OAUTH
     +		else if (strcmp(mechanism_buf.data, OAUTHBEARER_NAME) == 0 &&
     +				 !selected_mechanism)
     +		{
    @@ src/interfaces/libpq/fe-auth.c: pg_SASL_init(PGconn *conn, int payloadlen)
     +			conn->sasl = &pg_oauth_mech;
     +			conn->password_needed = false;
     +		}
    -+#endif
      	}
      
      	if (!selected_mechanism)
    @@ src/interfaces/libpq/fe-connect.c: keep_going:						/* We will come back to here
      					/* Check to see if we should mention pgpassfile */
      					pgpassfileWarning(conn);
      
    -+#ifdef USE_OAUTH
    ++					/*
    ++					 * OAuth connections may perform two-step discovery, where
    ++					 * the first connection is a dummy.
    ++					 */
     +					if (conn->sasl == &pg_oauth_mech
     +						&& conn->oauth_want_retry == PG_BOOL_YES)
     +					{
    @@ src/interfaces/libpq/fe-connect.c: keep_going:						/* We will come back to here
     +						need_new_connection = true;
     +						goto keep_going;
     +					}
    -+#endif
     +
      					CONNECTION_FAILED();
      				}
    @@ src/interfaces/libpq/fe-misc.c: pqSocketCheck(PGconn *conn, int forRead, int for
      	if (result < 0)
     
      ## src/interfaces/libpq/libpq-fe.h ##
    +@@ src/interfaces/libpq/libpq-fe.h: extern "C"
    +  */
    + #include "postgres_ext.h"
    + 
    ++#ifdef WIN32
    ++#include <winsock2.h>			/* for SOCKET */
    ++#endif
    ++
    + /*
    +  * These symbols may be used in compile-time #ifdef tests for the availability
    +  * of v14-and-newer libpq features.
     @@ src/interfaces/libpq/libpq-fe.h: extern "C"
      /* Features added in PostgreSQL v18: */
      /* Indicates presence of PQfullProtocolVersion */
    @@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
     +	const char *user_code;		/* user code to enter */
     +} PQpromptOAuthDevice;
     +
    ++/* for _PQoauthBearerRequest.async() */
    ++#ifdef WIN32
    ++#define SOCKTYPE SOCKET
    ++#else
    ++#define SOCKTYPE int
    ++#endif
    ++
     +typedef struct _PQoauthBearerRequest
     +{
     +	/* Hook inputs (constant across all calls) */
    @@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
     +	 */
     +	PostgresPollingStatusType (*async) (PGconn *conn,
     +										struct _PQoauthBearerRequest *request,
    -+										int *altsock);
    ++										SOCKTYPE *altsock);
     +
     +	/*
     +	 * Callback to clean up custom allocations. A hook implementation may use
    @@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
     +	 */
     +	void	   *user;
     +} PQoauthBearerRequest;
    ++
    ++#undef SOCKTYPE
     +
      extern char *PQencryptPassword(const char *passwd, const char *user);
      extern char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm);
    @@ src/interfaces/libpq/libpq-int.h: struct pg_conn
      	PGLoadBalanceType load_balance_type;	/* desired load balancing
     
      ## src/interfaces/libpq/meson.build ##
    +@@
    + # args for executables (which depend on libpq).
    + 
    + libpq_sources = files(
    ++  'fe-auth-oauth.c',
    +   'fe-auth-scram.c',
    +   'fe-auth.c',
    +   'fe-cancel.c',
     @@ src/interfaces/libpq/meson.build: if gssapi.found()
        )
      endif
      
    -+if oauth.found()
    -+  libpq_sources += files('fe-auth-oauth.c')
    -+  if oauth_library == 'curl'
    -+    libpq_sources += files('fe-auth-oauth-curl.c')
    -+  endif
    ++if oauth_library == 'curl'
    ++  libpq_sources += files('fe-auth-oauth-curl.c')
     +endif
     +
      export_file = custom_target('libpq.exports',
    @@ src/interfaces/libpq/pqexpbuffer.h: extern void initPQExpBuffer(PQExpBuffer str)
       *		Reset a PQExpBuffer to empty
     
      ## src/makefiles/meson.build ##
    +@@ src/makefiles/meson.build: pgxs_kv = {
    +   'SUN_STUDIO_CC': 'no', # not supported so far
    + 
    +   # want the chosen option, rather than the library
    ++  'with_builtin_oauth' : oauth_library,
    +   'with_ssl' : ssl_library,
    +   'with_uuid': uuidopt,
    + 
     @@ src/makefiles/meson.build: pgxs_deps = {
    +   'gssapi': gssapi,
    +   'icu': icu,
    +   'ldap': ldap,
    ++  'libcurl': libcurl,
    +   'libxml': libxml,
    +   'libxslt': libxslt,
        'llvm': llvm,
    -   'lz4': lz4,
    -   'nls': libintl,
    -+  'oauth': oauth,
    -   'pam': pam,
    -   'perl': perl_dep,
    -   'python': python3_dep,
     
      ## src/test/modules/Makefile ##
     @@ src/test/modules/Makefile: SUBDIRS = \
    @@ src/test/modules/oauth_validator/Makefile (new)
     +MODULES = validator
     +PGFILEDESC = "validator - test OAuth validator module"
     +
    ++PROGRAM = oauth_hook_client
    ++PGAPPICON = win32
    ++OBJS = $(WIN32RES) oauth_hook_client.o
    ++
    ++PG_CPPFLAGS = -I$(libpq_srcdir)
    ++PG_LIBS_INTERNAL += $(libpq_pgport)
    ++
     +NO_INSTALLCHECK = 1
     +
     +TAP_TESTS = 1
    @@ src/test/modules/oauth_validator/Makefile (new)
     +include $(top_srcdir)/contrib/contrib-global.mk
     +
     +export PYTHON
    -+export with_oauth
    ++export with_builtin_oauth
     +export with_python
     +
     +endif
    @@ src/test/modules/oauth_validator/meson.build (new)
     +)
     +test_install_libs += validator
     +
    ++oauth_hook_client_sources = files(
    ++  'oauth_hook_client.c',
    ++)
    ++
    ++if host_system == 'windows'
    ++  oauth_hook_client_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
    ++    '--NAME', 'oauth_hook_client',
    ++    '--FILEDESC', 'oauth_hook_client - test program for libpq OAuth hooks',])
    ++endif
    ++
    ++oauth_hook_client = executable('oauth_hook_client',
    ++  oauth_hook_client_sources,
    ++  dependencies: [frontend_code, libpq],
    ++  kwargs: default_bin_args + {
    ++    'install': false,
    ++  },
    ++)
    ++testprep_targets += oauth_hook_client
    ++
     +tests += {
     +  'name': 'oauth_validator',
     +  'sd': meson.current_source_dir(),
    @@ src/test/modules/oauth_validator/meson.build (new)
     +  'tap': {
     +    'tests': [
     +      't/001_server.pl',
    ++      't/002_client.pl',
     +    ],
     +    'env': {
     +      'PYTHON': python.path(),
    -+      'with_oauth': oauth_library,
    ++      'with_builtin_oauth': oauth_library,
     +      'with_python': 'yes',
     +    },
     +  },
    ++}
    +
    + ## src/test/modules/oauth_validator/oauth_hook_client.c (new) ##
    +@@
    ++/*-------------------------------------------------------------------------
    ++ *
    ++ * oauth_hook_client.c
    ++ *		Verify OAuth hook functionality in libpq
    ++ *
    ++ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
    ++ * Portions Copyright (c) 1994, Regents of the University of California
    ++ *
    ++ *
    ++ * IDENTIFICATION
    ++ *		src/test/modules/oauth_validator/oauth_hook_client.c
    ++ *
    ++ *-------------------------------------------------------------------------
    ++ */
    ++
    ++#include "postgres_fe.h"
    ++
    ++#include <stdio.h>
    ++#include <stdlib.h>
    ++
    ++#include "getopt_long.h"
    ++#include "libpq-fe.h"
    ++
    ++static int	handle_auth_data(PGAuthData type, PGconn *conn, void *data);
    ++
    ++static void
    ++usage(char *argv[])
    ++{
    ++	fprintf(stderr, "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, " --token TOKEN				use the provided TOKEN value\n");
    ++}
    ++
    ++static bool no_hook = false;
    ++static const char *expected_uri = NULL;
    ++static const char *expected_scope = NULL;
    ++static char *token = NULL;
    ++
    ++int
    ++main(int argc, char *argv[])
    ++{
    ++	static const struct option long_options[] = {
    ++		{"help", no_argument, NULL, 'h'},
    ++
    ++		{"expected-scope", required_argument, NULL, 1000},
    ++		{"expected-uri", required_argument, NULL, 1001},
    ++		{"no-hook", no_argument, NULL, 1002},
    ++		{"token", required_argument, NULL, 1003},
    ++		{0}
    ++	};
    ++
    ++	const char *conninfo;
    ++	PGconn	   *conn;
    ++	int			c;
    ++
    ++	while ((c = getopt_long(argc, argv, "h", long_options, NULL)) != -1)
    ++	{
    ++		switch (c)
    ++		{
    ++			case 'h':
    ++				usage(argv);
    ++				return 0;
    ++
    ++			case 1000:			/* --expected-scope */
    ++				expected_scope = optarg;
    ++				break;
    ++
    ++			case 1001:			/* --expected-uri */
    ++				expected_uri = optarg;
    ++				break;
    ++
    ++			case 1002:			/* --no-hook */
    ++				no_hook = true;
    ++				break;
    ++
    ++			case 1003:			/* --token */
    ++				token = optarg;
    ++				break;
    ++
    ++			default:
    ++				usage(argv);
    ++				return 1;
    ++		}
    ++	}
    ++
    ++	if (argc != optind + 1)
    ++	{
    ++		usage(argv);
    ++		return 1;
    ++	}
    ++
    ++	conninfo = argv[optind];
    ++
    ++	/* Set up our OAuth hooks. */
    ++	PQsetAuthDataHook(handle_auth_data);
    ++
    ++	/* Connect. (All the actual work is in the hook.) */
    ++	conn = PQconnectdb(conninfo);
    ++	if (PQstatus(conn) != CONNECTION_OK)
    ++	{
    ++		fprintf(stderr, "Connection to database failed: %s\n",
    ++				PQerrorMessage(conn));
    ++		PQfinish(conn);
    ++		return 1;
    ++	}
    ++
    ++	printf("connection succeeded\n");
    ++	PQfinish(conn);
    ++	return 0;
    ++}
    ++
    ++static int
    ++handle_auth_data(PGAuthData type, PGconn *conn, void *data)
    ++{
    ++	PQoauthBearerRequest *req = data;
    ++
    ++	if (no_hook || (type != PQAUTHDATA_OAUTH_BEARER_TOKEN))
    ++		return 0;
    ++
    ++	if (expected_uri)
    ++	{
    ++		if (!req->openid_configuration)
    ++		{
    ++			fprintf(stderr, "expected URI \"%s\", got NULL\n", expected_uri);
    ++			return -1;
    ++		}
    ++
    ++		if (strcmp(expected_uri, req->openid_configuration) != 0)
    ++		{
    ++			fprintf(stderr, "expected URI \"%s\", got \"%s\"\n", expected_uri, req->openid_configuration);
    ++			return -1;
    ++		}
    ++	}
    ++
    ++	if (expected_scope)
    ++	{
    ++		if (!req->scope)
    ++		{
    ++			fprintf(stderr, "expected scope \"%s\", got NULL\n", expected_scope);
    ++			return -1;
    ++		}
    ++
    ++		if (strcmp(expected_scope, req->scope) != 0)
    ++		{
    ++			fprintf(stderr, "expected scope \"%s\", got \"%s\"\n", expected_scope, req->scope);
    ++			return -1;
    ++		}
    ++	}
    ++
    ++	req->token = token;
    ++	return 1;
     +}
     
      ## src/test/modules/oauth_validator/t/001_server.pl (new) ##
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +	  'Potentially unsafe test oauth not enabled in PG_TEST_EXTRA';
     +}
     +
    -+if ($ENV{with_oauth} ne 'curl')
    ++if ($ENV{with_builtin_oauth} ne 'curl')
     +{
     +	plan skip_all => 'client-side OAuth not supported by this build';
     +}
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +
     +$node->stop;
     +
    ++done_testing();
    +
    + ## src/test/modules/oauth_validator/t/002_client.pl (new) ##
    +@@
    ++
    ++# Copyright (c) 2021-2024, PostgreSQL Global Development Group
    ++
    ++use strict;
    ++use warnings FATAL => 'all';
    ++
    ++use JSON::PP     qw(encode_json);
    ++use MIME::Base64 qw(encode_base64);
    ++use PostgreSQL::Test::Cluster;
    ++use PostgreSQL::Test::Utils;
    ++use PostgreSQL::Test::OAuthServer;
    ++use Test::More;
    ++
    ++if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\boauth\b/)
    ++{
    ++	plan skip_all =>
    ++	  'Potentially unsafe test oauth not enabled in PG_TEST_EXTRA';
    ++}
    ++
    ++#
    ++# Cluster Setup
    ++#
    ++
    ++my $node = PostgreSQL::Test::Cluster->new('primary');
    ++$node->init;
    ++$node->append_conf('postgresql.conf', "log_connections = on\n");
    ++$node->append_conf('postgresql.conf',
    ++	"oauth_validator_library = 'validator'\n");
    ++$node->start;
    ++
    ++$node->safe_psql('postgres', 'CREATE USER test;');
    ++
    ++my $issuer = "https://127.0.0.1:54321";
    ++my $scope = "openid postgres";
    ++
    ++unlink($node->data_dir . '/pg_hba.conf');
    ++$node->append_conf(
    ++	'pg_hba.conf', qq{
    ++local all test oauth issuer="$issuer" scope="$scope"
    ++});
    ++$node->reload;
    ++
    ++my ($log_start, $log_end);
    ++$log_start = $node->wait_for_log(qr/reloading configuration files/);
    ++
    ++$ENV{PGOAUTHDEBUG} = "UNSAFE";
    ++
    ++#
    ++# Tests
    ++#
    ++
    ++my $user = "test";
    ++my $base_connstr = $node->connstr() . " user=$user";
    ++my $common_connstr = "$base_connstr oauth_client_id=myID";
    ++
    ++sub test
    ++{
    ++	my ($test_name, %params) = @_;
    ++
    ++	my $flags = [];
    ++	if (defined($params{flags}))
    ++	{
    ++		$flags = $params{flags};
    ++	}
    ++
    ++	my @cmd = ("oauth_hook_client", @{$flags}, $common_connstr);
    ++	diag "running '" . join("' '", @cmd) . "'";
    ++
    ++	my ($stdout, $stderr) = run_command(\@cmd);
    ++
    ++	if (defined($params{expected_stdout}))
    ++	{
    ++		like($stdout, $params{expected_stdout}, "$test_name: stdout matches");
    ++	}
    ++
    ++	if (defined($params{expected_stderr}))
    ++	{
    ++		like($stderr, $params{expected_stderr}, "$test_name: stderr matches");
    ++	}
    ++	else
    ++	{
    ++		is($stderr, "", "$test_name: no stderr");
    ++	}
    ++}
    ++
    ++test(
    ++	"basic synchronous hook can provide a token",
    ++	flags => [
    ++		"--token", "my-token",
    ++		"--expected-uri", "$issuer/.well-known/openid-configuration",
    ++		"--expected-scope", $scope,
    ++	],
    ++	expected_stdout => qr/connection succeeded/);
    ++
    ++$node->log_check("validator receives correct token",
    ++	$log_start,
    ++	log_like => [ qr/oauth_validator: token="my-token", role="$user"/, ]);
    ++
    ++if ($ENV{with_builtin_oauth} ne 'curl')
    ++{
    ++	# libpq should help users out if no OAuth support is built in.
    ++	test(
    ++		"fails without custom hook installed",
    ++		flags => ["--no-hook"],
    ++		expected_stderr =>
    ++		  qr/no custom OAuth flows are available, and libpq was not built using --with-builtin-oauth/
    ++	);
    ++}
    ++
     +done_testing();
     
      ## src/test/modules/oauth_validator/t/oauth_server.py (new) ##
2:  cca5de6726 ! 2:  01df79980b DO NOT MERGE: Add pytest suite for OAuth
    @@ .cirrus.tasks.yml: env:
        MTEST_ARGS: --print-errorlogs --no-rebuild -C build
        PGCTLTIMEOUT: 120 # avoids spurious failures during parallel tests
        TEMP_CONFIG: ${CIRRUS_WORKING_DIR}/src/tools/ci/pg_ci_base.conf
    --  PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance
    -+  PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance python
    +-  PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance oauth
    ++  PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance oauth python
      
      
      # What files to preserve in case tests fail
    @@ .cirrus.tasks.yml: task:
            configure_32_script: |
              su postgres <<-EOF
                export CC='ccache gcc -m32'
    -@@ .cirrus.tasks.yml: task:
    -             -Dllvm=disabled \
    -             --pkg-config-path /usr/lib/i386-linux-gnu/pkgconfig/ \
    -             -DPERL=perl5.36-i386-linux-gnu \
    --            -DPG_TEST_EXTRA="$PG_TEST_EXTRA" \
    -+            -DPG_TEST_EXTRA="${PG_TEST_EXTRA//"python"}" \
    -             build-32
    -         EOF
    - 
    ++          export PG_TEST_EXTRA="${PG_TEST_EXTRA//python}"
    +           meson setup \
    +             --buildtype=debug \
    +             -Dcassert=true -Dinjection_points=true \
     
      ## meson.build ##
     @@ meson.build: else
    @@ src/test/python/client/test_oauth.py (new)
     +# The client tests need libpq to have been compiled with OAuth support; skip
     +# them otherwise.
     +pytestmark = pytest.mark.skipif(
    -+    os.getenv("with_oauth") == "none",
    -+    reason="OAuth client tests require --with-oauth support",
    ++    os.getenv("with_builtin_oauth") == "none",
    ++    reason="OAuth client tests require --with-builtin-oauth support",
     +)
     +
     +if platform.system() == "Darwin":
    @@ src/test/python/meson.build (new)
     +subdir('server')
     +
     +pytest_env = {
    -+  'with_oauth': oauth_library,
    ++  'with_builtin_oauth': oauth_library,
     +
     +  # Point to the default database; the tests will create their own databases as
     +  # needed.