since-v18.diff.txt

text/plain

Filename: since-v18.diff.txt
Type: text/plain
Part: 0
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER
 1:  e2a0b48561 =  1:  ce06c03e2b common/jsonapi: support FRONTEND clients
 2:  db625e1d01 =  2:  6989b75153 Refactor SASL exchange to return tri-state status
 3:  e4ad0260d5 =  3:  783bfe0b95 Explicitly require password for SCRAM exchange
 4:  229f602d5c !  4:  77550a47db libpq: add OAUTHBEARER SASL mechanism
    @@ Commit message
         are currently implemented (but clients may provide their own flows; see
         below).
     
    -    The client implementation requires either libcurl or libiddawc and their
    -    development headers. Pass `curl` or `iddawc` to --with-oauth/-Doauth
    -    during configuration.
    +    The client implementation requires libcurl and its development headers.
    +    Pass `curl` to --with-oauth/-Doauth during configuration.
     
         Thomas Munro wrote the kqueue() implementation for oauth-curl; thanks!
     
    @@ 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, iddawc)
    ++  --with-oauth=LIB        use LIB for OAuth 2.0 support (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; }
     +
     +$as_echo "#define USE_OAUTH_CURL 1" >>confdefs.h
     +
    -+elif test x"$with_oauth" = x"iddawc"; then
    -+
    -+$as_echo "#define USE_OAUTH 1" >>confdefs.h
    -+
    -+
    -+$as_echo "#define USE_OAUTH_IDDAWC 1" >>confdefs.h
    -+
     +elif test x"$with_oauth" != x"no"; then
    -+  as_fn_error $? "--with-oauth must specify curl or iddawc" "$LINENO" 5
    ++  as_fn_error $? "--with-oauth must specify curl" "$LINENO" 5
     +fi
     +
     +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_oauth" >&5
    @@ configure: fi
     +  as_fn_error $? "library 'curl' is required for --with-oauth=curl" "$LINENO" 5
     +fi
     +
    -+elif test "$with_oauth" = iddawc ; then
    -+  { $as_echo "$as_me:${as_lineno-$LINENO}: checking for i_init_session in -liddawc" >&5
    -+$as_echo_n "checking for i_init_session in -liddawc... " >&6; }
    -+if ${ac_cv_lib_iddawc_i_init_session+:} false; then :
    -+  $as_echo_n "(cached) " >&6
    -+else
    -+  ac_check_lib_save_LIBS=$LIBS
    -+LIBS="-liddawc  $LIBS"
    -+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
    -+/* end confdefs.h.  */
    -+
    -+/* Override any GCC internal prototype to avoid an error.
    -+   Use char because int might match the return type of a GCC
    -+   builtin and then its argument prototype would still apply.  */
    -+#ifdef __cplusplus
    -+extern "C"
    -+#endif
    -+char i_init_session ();
    -+int
    -+main ()
    -+{
    -+return i_init_session ();
    -+  ;
    -+  return 0;
    -+}
    -+_ACEOF
    -+if ac_fn_c_try_link "$LINENO"; then :
    -+  ac_cv_lib_iddawc_i_init_session=yes
    -+else
    -+  ac_cv_lib_iddawc_i_init_session=no
    -+fi
    -+rm -f core conftest.err conftest.$ac_objext \
    -+    conftest$ac_exeext conftest.$ac_ext
    -+LIBS=$ac_check_lib_save_LIBS
    -+fi
    -+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_iddawc_i_init_session" >&5
    -+$as_echo "$ac_cv_lib_iddawc_i_init_session" >&6; }
    -+if test "x$ac_cv_lib_iddawc_i_init_session" = xyes; then :
    -+  cat >>confdefs.h <<_ACEOF
    -+#define HAVE_LIBIDDAWC 1
    -+_ACEOF
    -+
    -+  LIBS="-liddawc $LIBS"
    -+
    -+else
    -+  as_fn_error $? "library 'iddawc' is required for --with-oauth=iddawc" "$LINENO" 5
    -+fi
    -+
    -+  # Check for an older spelling of i_get_openid_config
    -+  for ac_func in i_load_openid_config
    -+do :
    -+  ac_fn_c_check_func "$LINENO" "i_load_openid_config" "ac_cv_func_i_load_openid_config"
    -+if test "x$ac_cv_func_i_load_openid_config" = xyes; then :
    -+  cat >>confdefs.h <<_ACEOF
    -+#define HAVE_I_LOAD_OPENID_CONFIG 1
    -+_ACEOF
    -+
    -+fi
    -+done
    -+
     +fi
     +
      # for contrib/sepgsql
    @@ configure: fi
     +  as_fn_error $? "header file <curl/curl.h> is required for OAuth" "$LINENO" 5
     +fi
     +
    -+
    -+elif test "$with_oauth" = iddawc; then
    -+  ac_fn_c_check_header_mongrel "$LINENO" "iddawc.h" "ac_cv_header_iddawc_h" "$ac_includes_default"
    -+if test "x$ac_cv_header_iddawc_h" = xyes; then :
    -+
    -+else
    -+  as_fn_error $? "header file <iddawc.h> is required for OAuth" "$LINENO" 5
    -+fi
    -+
     +
      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, iddawc)])
    ++PGAC_ARG_REQ(with, oauth, [LIB], [use LIB for OAuth 2.0 support (curl)])
     +if test x"$with_oauth" = x"" ; then
     +  with_oauth=no
     +fi
    @@ configure.ac: AC_MSG_RESULT([$with_ldap])
     +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.])
    -+elif test x"$with_oauth" = x"iddawc"; then
    -+  AC_DEFINE([USE_OAUTH], 1, [Define to 1 to build with OAuth 2.0 support. (--with-oauth)])
    -+  AC_DEFINE([USE_OAUTH_IDDAWC], 1, [Define to 1 to use libiddawc for OAuth support.])
     +elif test x"$with_oauth" != x"no"; then
    -+  AC_MSG_ERROR([--with-oauth must specify curl or iddawc])
    ++  AC_MSG_ERROR([--with-oauth must specify curl])
     +fi
     +
     +AC_MSG_RESULT([$with_oauth])
    @@ configure.ac: 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])])
    -+elif test "$with_oauth" = iddawc ; then
    -+  AC_CHECK_LIB(iddawc, i_init_session, [], [AC_MSG_ERROR([library 'iddawc' is required for --with-oauth=iddawc])])
    -+  # Check for an older spelling of i_get_openid_config
    -+  AC_CHECK_FUNCS([i_load_openid_config])
     +fi
     +
      # for contrib/sepgsql
    @@ configure.ac: elif test "$with_uuid" = ossp ; then
      
     +if test "$with_oauth" = curl; then
     +  AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file <curl/curl.h> is required for OAuth])])
    -+elif test "$with_oauth" = iddawc; then
    -+  AC_CHECK_HEADER(iddawc.h, [], [AC_MSG_ERROR([header file <iddawc.h> is required for OAuth])])
     +fi
     +
      if test "$PORTNAME" = "win32" ; then
    @@ meson.build: endif
     +  endif
     +endif
     +
    -+if not oauth.found() and oauthopt in ['auto', 'iddawc']
    -+  oauth = dependency('libiddawc', required: (oauthopt == 'iddawc'))
    -+
    -+  if oauth.found()
    -+    oauth_library = 'iddawc'
    -+    cdata.set('USE_OAUTH', 1)
    -+    cdata.set('USE_OAUTH_IDDAWC', 1)
    -+
    -+    # Check for an older spelling of i_get_openid_config
    -+    if cc.has_function('i_load_openid_config',
    -+                       dependencies: oauth, args: test_c_args)
    -+      cdata.set('HAVE_I_LOAD_OPENID_CONFIG', 1)
    -+    endif
    -+  endif
    -+endif
    -+
     +if oauthopt == 'auto' and auto_features.enabled() and not oauth.found()
     +  error('no OAuth implementation library found')
     +endif
    @@ meson_options.txt: option('lz4', type: 'feature', value: 'auto',
      option('nls', type: 'feature', value: 'auto',
        description: 'Native language support')
      
    -+option('oauth', type : 'combo', choices : ['auto', 'none', 'curl', 'iddawc'],
    ++option('oauth', type : 'combo', choices : ['auto', 'none', 'curl'],
     +  value: 'auto',
    -+  description: 'use LIB for OAuth 2.0 support (curl, iddawc)')
    ++  description: 'use LIB for OAuth 2.0 support (curl)')
     +
      option('pam', type: 'feature', value: 'auto',
        description: 'PAM support')
    @@ src/include/common/oauth-common.h (new)
     +#endif							/* OAUTH_COMMON_H */
     
      ## src/include/pg_config.h.in ##
    -@@
    - /* Define to 1 if __builtin_constant_p(x) implies "i"(x) acceptance. */
    - #undef HAVE_I_CONSTRAINT__BUILTIN_CONSTANT_P
    - 
    -+/* Define to 1 if you have the `i_load_openid_config' function. */
    -+#undef HAVE_I_LOAD_OPENID_CONFIG
    -+
    - /* Define to 1 if you have the `kqueue' function. */
    - #undef HAVE_KQUEUE
    - 
     @@
      /* Define to 1 if you have the `crypto' library (-lcrypto). */
      #undef HAVE_LIBCRYPTO
      
     +/* Define to 1 if you have the `curl' library (-lcurl). */
     +#undef HAVE_LIBCURL
    -+
    -+/* Define to 1 if you have the `iddawc' library (-liddawc). */
    -+#undef HAVE_LIBIDDAWC
     +
      /* Define to 1 if you have the `ldap' library (-lldap). */
      #undef HAVE_LIBLDAP
    @@ src/include/pg_config.h.in
     +
     +/* Define to 1 to use libcurl for OAuth support. */
     +#undef USE_OAUTH_CURL
    -+
    -+/* Define to 1 to use libiddawc for OAuth support. */
    -+#undef USE_OAUTH_IDDAWC
     +
      /* Define to 1 to build with OpenSSL support. (--with-ssl=openssl) */
      #undef USE_OPENSSL
    @@ src/interfaces/libpq/Makefile: OBJS += \
     +ifneq ($(with_oauth),no)
     +OBJS += fe-auth-oauth.o
     +
    -+ifeq ($(with_oauth),iddawc)
    -+OBJS += fe-auth-oauth-iddawc.o
    -+else
    ++ifeq ($(with_oauth),curl)
     +OBJS += fe-auth-oauth-curl.o
     +endif
     +endif
    @@ src/interfaces/libpq/Makefile: endif
      SHLIB_LINK_INTERNAL = -lpgcommon_shlib -lpgport_shlib
      ifneq ($(PORTNAME), win32)
     -SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS)
    -+SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lcurl -liddawc -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS)
    ++SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lcurl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS)
      else
      SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE)
      endif
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +
     +	free_token(&tok);
     +	return PGRES_POLLING_FAILED;
    -+}
    -
    - ## src/interfaces/libpq/fe-auth-oauth-iddawc.c (new) ##
    -@@
    -+/*-------------------------------------------------------------------------
    -+ *
    -+ * fe-auth-oauth-iddawc.c
    -+ *	   The libiddawc implementation of OAuth/OIDC authentication.
    -+ *
    -+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
    -+ * Portions Copyright (c) 1994, Regents of the University of California
    -+ *
    -+ * IDENTIFICATION
    -+ *	  src/interfaces/libpq/fe-auth-oauth-iddawc.c
    -+ *
    -+ *-------------------------------------------------------------------------
    -+ */
    -+
    -+#include "postgres_fe.h"
    -+
    -+#include <iddawc.h>
    -+
    -+#include "fe-auth.h"
    -+#include "fe-auth-oauth.h"
    -+#include "libpq-int.h"
    -+
    -+#ifdef HAVE_I_LOAD_OPENID_CONFIG
    -+/* Older versions of iddawc used 'load' instead of 'get' for some APIs. */
    -+#define i_get_openid_config i_load_openid_config
    -+#endif
    -+
    -+static const char *
    -+iddawc_error_string(int errcode)
    -+{
    -+	switch (errcode)
    -+	{
    -+		case I_OK:
    -+			return "I_OK";
    -+
    -+		case I_ERROR:
    -+			return "I_ERROR";
    -+
    -+		case I_ERROR_PARAM:
    -+			return "I_ERROR_PARAM";
    -+
    -+		case I_ERROR_MEMORY:
    -+			return "I_ERROR_MEMORY";
    -+
    -+		case I_ERROR_UNAUTHORIZED:
    -+			return "I_ERROR_UNAUTHORIZED";
    -+
    -+		case I_ERROR_SERVER:
    -+			return "I_ERROR_SERVER";
    -+	}
    -+
    -+	return "<unknown>";
    -+}
    -+
    -+static void
    -+iddawc_error(PGconn *conn, int errcode, const char *msg)
    -+{
    -+	appendPQExpBufferStr(&conn->errorMessage, libpq_gettext(msg));
    -+	appendPQExpBuffer(&conn->errorMessage,
    -+					  libpq_gettext(" (iddawc error %s)\n"),
    -+					  iddawc_error_string(errcode));
    -+}
    -+
    -+static void
    -+iddawc_request_error(PGconn *conn, struct _i_session *i, int err, const char *msg)
    -+{
    -+	const char *error_code;
    -+	const char *desc;
    -+
    -+	appendPQExpBuffer(&conn->errorMessage, "%s: ", libpq_gettext(msg));
    -+
    -+	error_code = i_get_str_parameter(i, I_OPT_ERROR);
    -+	if (!error_code)
    -+	{
    -+		/*
    -+		 * The server didn't give us any useful information, so just print the
    -+		 * error code.
    -+		 */
    -+		appendPQExpBuffer(&conn->errorMessage,
    -+						  libpq_gettext("(iddawc error %s)\n"),
    -+						  iddawc_error_string(err));
    -+		return;
    -+	}
    -+
    -+	/* If the server gave a string description, print that too. */
    -+	desc = i_get_str_parameter(i, I_OPT_ERROR_DESCRIPTION);
    -+	if (desc)
    -+		appendPQExpBuffer(&conn->errorMessage, "%s ", desc);
    -+
    -+	appendPQExpBuffer(&conn->errorMessage, "(%s)\n", error_code);
    -+}
    -+
    -+/*
    -+ * Runs the device authorization flow using libiddawc. If successful, a malloc'd
    -+ * token string in "Bearer xxxx..." format, suitable for sending to an
    -+ * OAUTHBEARER server, is returned. NULL is returned on error.
    -+ */
    -+static char *
    -+run_iddawc_auth_flow(PGconn *conn, const char *discovery_uri)
    -+{
    -+	struct _i_session session;
    -+	PQExpBuffer token_buf = NULL;
    -+	int			err;
    -+	int			auth_method;
    -+	bool		user_prompted = false;
    -+	const char *verification_uri;
    -+	const char *user_code;
    -+	const char *access_token;
    -+	const char *token_type;
    -+	char	   *token = NULL;
    -+
    -+	i_init_session(&session);
    -+
    -+	token_buf = createPQExpBuffer();
    -+	if (!token_buf)
    -+		goto cleanup;
    -+
    -+	err = i_set_str_parameter(&session, I_OPT_OPENID_CONFIG_ENDPOINT, discovery_uri);
    -+	if (err)
    -+	{
    -+		iddawc_error(conn, err, "failed to set OpenID config endpoint");
    -+		goto cleanup;
    -+	}
    -+
    -+	err = i_get_openid_config(&session);
    -+	if (err)
    -+	{
    -+		iddawc_error(conn, err, "failed to fetch OpenID discovery document");
    -+		goto cleanup;
    -+	}
    -+
    -+	if (!i_get_str_parameter(&session, I_OPT_TOKEN_ENDPOINT))
    -+	{
    -+		appendPQExpBufferStr(&conn->errorMessage,
    -+							 libpq_gettext("issuer has no token endpoint"));
    -+		goto cleanup;
    -+	}
    -+
    -+	if (!i_get_str_parameter(&session, I_OPT_DEVICE_AUTHORIZATION_ENDPOINT))
    -+	{
    -+		appendPQExpBufferStr(&conn->errorMessage,
    -+							 libpq_gettext("issuer does not support device authorization"));
    -+		goto cleanup;
    -+	}
    -+
    -+	err = i_set_response_type(&session, I_RESPONSE_TYPE_DEVICE_CODE);
    -+	if (err)
    -+	{
    -+		iddawc_error(conn, err, "failed to set device code response type");
    -+		goto cleanup;
    -+	}
    -+
    -+	auth_method = I_TOKEN_AUTH_METHOD_NONE;
    -+	if (conn->oauth_client_secret && *conn->oauth_client_secret)
    -+		auth_method = I_TOKEN_AUTH_METHOD_SECRET_BASIC;
    -+
    -+	err = i_set_parameter_list(&session,
    -+							   I_OPT_CLIENT_ID, conn->oauth_client_id,
    -+							   I_OPT_CLIENT_SECRET, conn->oauth_client_secret,
    -+							   I_OPT_TOKEN_METHOD, auth_method,
    -+							   I_OPT_SCOPE, conn->oauth_scope,
    -+							   I_OPT_NONE
    -+		);
    -+	if (err)
    -+	{
    -+		iddawc_error(conn, err, "failed to set client identifier");
    -+		goto cleanup;
    -+	}
    -+
    -+	err = i_run_device_auth_request(&session);
    -+	if (err)
    -+	{
    -+		iddawc_request_error(conn, &session, err,
    -+							 "failed to obtain device authorization");
    -+		goto cleanup;
    -+	}
    -+
    -+	verification_uri = i_get_str_parameter(&session, I_OPT_DEVICE_AUTH_VERIFICATION_URI);
    -+	if (!verification_uri)
    -+	{
    -+		appendPQExpBufferStr(&conn->errorMessage,
    -+							 libpq_gettext("issuer did not provide a verification URI"));
    -+		goto cleanup;
    -+	}
    -+
    -+	user_code = i_get_str_parameter(&session, I_OPT_DEVICE_AUTH_USER_CODE);
    -+	if (!user_code)
    -+	{
    -+		appendPQExpBufferStr(&conn->errorMessage,
    -+							 libpq_gettext("issuer did not provide a user code"));
    -+		goto cleanup;
    -+	}
    -+
    -+	/*
    -+	 * Poll the token endpoint until either the user logs in and authorizes
    -+	 * the use of a token, or a hard failure occurs. We perform one ping
    -+	 * _before_ prompting the user, so that we don't make them do the work of
    -+	 * logging in only to find that the token endpoint is completely
    -+	 * unreachable.
    -+	 */
    -+	err = i_run_token_request(&session);
    -+	while (err)
    -+	{
    -+		const char *error_code;
    -+		uint		interval;
    -+
    -+		error_code = i_get_str_parameter(&session, I_OPT_ERROR);
    -+
    -+		/*
    -+		 * authorization_pending and slow_down are the only acceptable errors;
    -+		 * anything else and we bail.
    -+		 */
    -+		if (!error_code || (strcmp(error_code, "authorization_pending")
    -+							&& strcmp(error_code, "slow_down")))
    -+		{
    -+			iddawc_request_error(conn, &session, err,
    -+								 "failed to obtain access token");
    -+			goto cleanup;
    -+		}
    -+
    -+		if (!user_prompted)
    -+		{
    -+			int			res;
    -+			PQpromptOAuthDevice prompt = {
    -+				.verification_uri = verification_uri,
    -+				.user_code = user_code,
    -+				/* TODO: optional fields */
    -+			};
    -+
    -+			/*
    -+			 * Now that we know the token endpoint isn't broken, give the user
    -+			 * the login instructions.
    -+			 */
    -+			res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn,
    -+								 &prompt);
    -+
    -+			if (!res)
    -+			{
    -+				fprintf(stderr, "Visit %s and enter the code: %s",
    -+						prompt.verification_uri, prompt.user_code);
    -+			}
    -+			else if (res < 0)
    -+			{
    -+				appendPQExpBufferStr(&conn->errorMessage,
    -+									 libpq_gettext("device prompt failed\n"));
    -+				goto cleanup;
    -+			}
    -+
    -+			user_prompted = true;
    -+		}
    -+
    -+		/*---
    -+		 * We are required to wait between polls; the server tells us how
    -+		 * long.
    -+		 * TODO: if interval's not set, we need to default to five seconds
    -+		 * TODO: sanity check the interval
    -+		 */
    -+		interval = i_get_int_parameter(&session, I_OPT_DEVICE_AUTH_INTERVAL);
    -+
    -+		/*
    -+		 * A slow_down error requires us to permanently increase our retry
    -+		 * interval by five seconds. RFC 8628, Sec. 3.5.
    -+		 */
    -+		if (!strcmp(error_code, "slow_down"))
    -+		{
    -+			interval += 5;
    -+			i_set_int_parameter(&session, I_OPT_DEVICE_AUTH_INTERVAL, interval);
    -+		}
    -+
    -+		sleep(interval);
    -+
    -+		/*
    -+		 * XXX Reset the error code before every call, because iddawc won't do
    -+		 * that for us. This matters if the server first sends a "pending"
    -+		 * error code, then later hard-fails without sending an error code to
    -+		 * overwrite the first one.
    -+		 *
    -+		 * That we have to do this at all seems like a bug in iddawc.
    -+		 */
    -+		i_set_str_parameter(&session, I_OPT_ERROR, NULL);
    -+
    -+		err = i_run_token_request(&session);
    -+	}
    -+
    -+	access_token = i_get_str_parameter(&session, I_OPT_ACCESS_TOKEN);
    -+	token_type = i_get_str_parameter(&session, I_OPT_TOKEN_TYPE);
    -+
    -+	if (!access_token || !token_type || strcasecmp(token_type, "Bearer"))
    -+	{
    -+		appendPQExpBufferStr(&conn->errorMessage,
    -+							 libpq_gettext("issuer did not provide a bearer token"));
    -+		goto cleanup;
    -+	}
    -+
    -+	appendPQExpBufferStr(token_buf, "Bearer ");
    -+	appendPQExpBufferStr(token_buf, access_token);
    -+
    -+	if (PQExpBufferBroken(token_buf))
    -+		goto cleanup;
    -+
    -+	token = strdup(token_buf->data);
    -+
    -+cleanup:
    -+	if (token_buf)
    -+		destroyPQExpBuffer(token_buf);
    -+	i_clean_session(&session);
    -+
    -+	return token;
    -+}
    -+
    -+PostgresPollingStatusType
    -+pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock)
    -+{
    -+	fe_oauth_state *state = conn->sasl_state;
    -+
    -+	/* TODO: actually make this asynchronous */
    -+	state->token = run_iddawc_auth_flow(conn, conn->oauth_discovery_uri);
    -+	return state->token ? PGRES_POLLING_OK : PGRES_POLLING_FAILED;
     +}
     
      ## src/interfaces/libpq/fe-auth-oauth.c (new) ##
    @@ src/interfaces/libpq/meson.build: if gssapi.found()
      
     +if oauth.found()
     +  libpq_sources += files('fe-auth-oauth.c')
    -+  if oauth_library == 'iddawc'
    -+    libpq_sources += files('fe-auth-oauth-iddawc.c')
    -+  else
    ++  if oauth_library == 'curl'
     +    libpq_sources += files('fe-auth-oauth-curl.c')
     +  endif
     +endif
 5:  5c5a83e44e =  5:  12ae7c4355 backend: add OAUTHBEARER SASL mechanism
 6:  7a42365d62 =  6:  707edf9314 Introduce OAuth validator libraries
 7:  9c46ea6cf9 !  7:  47236c5644 Add pytest suite for OAuth
    @@ Commit message
         option of the same name, which allows an ephemeral server to be spun up
         during a test run.
     
    -    For iddawc, asynchronous tests still hang, as expected. Bad-interval
    -    tests fail because iddawc apparently doesn't care that the interval is
    -    bad.
    -
         TODOs:
         - The --tap-stream option to pytest-tap is slightly broken during test
           failures (it suppresses error information), which impedes debugging.
    @@ src/test/python/client/test_oauth.py (new)
     +        ),
     +        pytest.param(
     +            (400, {}),
    -+            alt_patterns(
    -+                r'failed to parse token error response: field "error" is missing',
    -+                r"failed to obtain device authorization: \(iddawc error I_ERROR_PARAM\)",
    -+            ),
    ++            r'failed to parse token error response: field "error" is missing',
     +            id="broken error response",
     +        ),
     +        pytest.param(
     +            (200, RawResponse(r'{ "interval": 3.5.8 }')),
    -+            alt_patterns(
    -+                r"failed to parse device authorization: Token .* is invalid",
    -+                r"failed to obtain device authorization: \(iddawc error I_ERROR\)",
    -+            ),
    ++            r"failed to parse device authorization: Token .* is invalid",
     +            id="non-numeric interval",
     +        ),
     +        pytest.param(
     +            (200, RawResponse(r'{ "interval": 08 }')),
    -+            alt_patterns(
    -+                r"failed to parse device authorization: Token .* is invalid",
    -+                r"failed to obtain device authorization: \(iddawc error I_ERROR\)",
    -+            ),
    ++            r"failed to parse device authorization: Token .* is invalid",
     +            id="invalid numeric interval",
     +        ),
     +    ],
    @@ src/test/python/client/test_oauth.py (new)
     +    else:
     +        assert False, "update error_pattern for new failure mode"
     +
    -+    # XXX iddawc doesn't really check for problems in the device authorization
    -+    # response, leading to this patchwork:
    -+    if field_name == "verification_uri":
    -+        error_pattern = alt_patterns(
    -+            error_pattern,
    -+            "issuer did not provide a verification URI",
    -+        )
    -+    elif field_name == "user_code":
    -+        error_pattern = alt_patterns(
    -+            error_pattern,
    -+            "issuer did not provide a user code",
    -+        )
    -+    else:
    -+        error_pattern = alt_patterns(
    -+            error_pattern,
    -+            r"failed to obtain access token: \(iddawc error I_ERROR_PARAM\)",
    -+        )
    -+
     +    with pytest.raises(psycopg2.OperationalError, match=error_pattern):
     +        client.check_completed()
     +
    @@ src/test/python/client/test_oauth.py (new)
     +        ),
     +        pytest.param(
     +            (400, {}),
    -+            alt_patterns(
    -+                r'failed to parse token error response: field "error" is missing',
    -+                r"failed to obtain access token: \(iddawc error I_ERROR_PARAM\)",
    -+            ),
    ++            r'failed to parse token error response: field "error" is missing',
     +            id="empty error response",
     +        ),
     +        pytest.param(
     +            (200, {}, {}),
    -+            alt_patterns(
    -+                r"failed to parse access token response: no content type was provided",
    -+                r"failed to obtain access token: \(iddawc error I_ERROR\)",
    -+            ),
    ++            r"failed to parse access token response: no content type was provided",
     +            id="missing content type",
     +        ),
     +        pytest.param(
     +            (200, {"Content-Type": "text/plain"}, {}),
    -+            alt_patterns(
    -+                r"failed to parse access token response: unexpected content type",
    -+                r"failed to obtain access token: \(iddawc error I_ERROR\)",
    -+            ),
    ++            r"failed to parse access token response: unexpected content type",
     +            id="wrong content type",
     +        ),
     +    ],
    @@ src/test/python/client/test_oauth.py (new)
     +    else:
     +        assert False, "update error_pattern for new failure mode"
     +
    -+    # XXX iddawc is fairly silent on the topic.
    -+    error_pattern = alt_patterns(
    -+        error_pattern,
    -+        r"failed to obtain access token: \(iddawc error I_ERROR_PARAM\)",
    -+    )
    -+
     +    with pytest.raises(psycopg2.OperationalError, match=error_pattern):
     +        client.check_completed()
     +
    @@ src/test/python/client/test_oauth.py (new)
     +
     +    expect_disconnected_handshake(sock)
     +
    -+    # XXX iddawc doesn't differentiate...
    -+    expected_error = alt_patterns(
    -+        expected_error,
    -+        r"failed to fetch OpenID discovery document \(iddawc error I_ERROR(_PARAM)?\)",
    -+    )
    -+
     +    with pytest.raises(psycopg2.OperationalError, match=expected_error):
     +        client.check_completed()
     +
    @@ src/test/python/test_pq3.py (new)
     +import contextlib
     +import getpass
     +import io
    ++import os
     +import platform
     +import struct
     +import sys
 8:  8ad4ce3068 =  8:  116e17eeee XXX temporary patches to build and test
 9:  5630465578 =  9:  28756eda1c WIP: Python OAuth provider implementation