From 7f6b02652cd771a93ce4269607d498f4ac574e7f Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Tue, 13 Apr 2021 10:27:27 -0700
Subject: [PATCH v3 5/9] libpq: add OAUTHBEARER SASL mechanism

DO NOT USE THIS PROOF OF CONCEPT IN PRODUCTION.

Implement OAUTHBEARER (RFC 7628) and OAuth 2.0 Device Authorization
Grants (RFC 8628) on the client side. When speaking to a OAuth-enabled
server, it looks a bit like this:

    $ psql 'host=example.org oauth_client_id=f02c6361-0635-...'
    Visit https://oauth.example.org/login and enter the code: FPQ2-M4BG

The OAuth issuer must support device authorization. No other OAuth flows
are currently implemented.

The client implementation requires libiddawc and its development
headers. Configure --with-oauth (and --with-includes/--with-libraries to
point at the iddawc installation, if it's in a custom location).

Several TODOs:
- don't retry forever if the server won't accept our token
- perform several sanity checks on the OAuth issuer's responses
- handle cases where the client has been set up with an issuer and
  scope, but the Postgres server wants to use something different
- improve error debuggability during the OAuth handshake
- ...and more.
---
 configure                            | 100 ++++
 configure.ac                         |  19 +
 src/Makefile.global.in               |   1 +
 src/include/common/oauth-common.h    |  19 +
 src/include/pg_config.h.in           |   6 +
 src/interfaces/libpq/Makefile        |   7 +-
 src/interfaces/libpq/fe-auth-oauth.c | 744 +++++++++++++++++++++++++++
 src/interfaces/libpq/fe-auth-sasl.h  |   5 +-
 src/interfaces/libpq/fe-auth-scram.c |   6 +-
 src/interfaces/libpq/fe-auth.c       |  42 +-
 src/interfaces/libpq/fe-auth.h       |   3 +
 src/interfaces/libpq/fe-connect.c    |  38 ++
 src/interfaces/libpq/libpq-int.h     |   8 +
 13 files changed, 979 insertions(+), 19 deletions(-)
 create mode 100644 src/include/common/oauth-common.h
 create mode 100644 src/interfaces/libpq/fe-auth-oauth.c

diff --git a/configure b/configure
index f3cb5c2b51..cd0c50a951 100755
--- a/configure
+++ b/configure
@@ -718,6 +718,7 @@ with_uuid
 with_readline
 with_systemd
 with_selinux
+with_oauth
 with_ldap
 with_krb_srvnam
 krb_srvtab
@@ -861,6 +862,7 @@ with_krb_srvnam
 with_pam
 with_bsd_auth
 with_ldap
+with_oauth
 with_bonjour
 with_selinux
 with_systemd
@@ -1570,6 +1572,7 @@ Optional Packages:
   --with-pam              build with PAM support
   --with-bsd-auth         build with BSD Authentication support
   --with-ldap             build with LDAP support
+  --with-oauth            build with OAuth 2.0 support
   --with-bonjour          build with Bonjour support
   --with-selinux          build with SELinux support
   --with-systemd          build with systemd support
@@ -8377,6 +8380,42 @@ $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; }
+
+
+
+# Check whether --with-oauth was given.
+if test "${with_oauth+set}" = set; then :
+  withval=$with_oauth;
+  case $withval in
+    yes)
+
+$as_echo "#define USE_OAUTH 1" >>confdefs.h
+
+      ;;
+    no)
+      :
+      ;;
+    *)
+      as_fn_error $? "no argument expected for --with-oauth option" "$LINENO" 5
+      ;;
+  esac
+
+else
+  with_oauth=no
+
+fi
+
+
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_oauth" >&5
+$as_echo "$with_oauth" >&6; }
+
+
+
 #
 # Bonjour
 #
@@ -13500,6 +13539,56 @@ fi
 
 
 
+if test "$with_oauth" = yes ; 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 OAuth support" "$LINENO" 5
+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
@@ -14513,6 +14602,17 @@ fi
 
 done
 
+fi
+
+if test "$with_oauth" != no; 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
 
 if test "$PORTNAME" = "win32" ; then
diff --git a/configure.ac b/configure.ac
index 19d1a80367..922608065f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -887,6 +887,17 @@ AC_MSG_RESULT([$with_ldap])
 AC_SUBST(with_ldap)
 
 
+#
+# OAuth 2.0
+#
+AC_MSG_CHECKING([whether to build with OAuth support])
+PGAC_ARG_BOOL(with, oauth, no,
+              [build with OAuth 2.0 support],
+              [AC_DEFINE([USE_OAUTH], 1, [Define to 1 to build with OAuth 2.0 support. (--with-oauth)])])
+AC_MSG_RESULT([$with_oauth])
+AC_SUBST(with_oauth)
+
+
 #
 # Bonjour
 #
@@ -1385,6 +1396,10 @@ fi
 AC_SUBST(LDAP_LIBS_FE)
 AC_SUBST(LDAP_LIBS_BE)
 
+if test "$with_oauth" = yes ; then
+  AC_CHECK_LIB(iddawc, i_init_session, [], [AC_MSG_ERROR([library 'iddawc' is required for OAuth support])])
+fi
+
 # for contrib/sepgsql
 if test "$with_selinux" = yes; then
   AC_CHECK_LIB(selinux, security_compute_create_name, [],
@@ -1603,6 +1618,10 @@ 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" != no; then
+  AC_CHECK_HEADER(iddawc.h, [], [AC_MSG_ERROR([header file <iddawc.h> is required for OAuth])])
+fi
+
 if test "$PORTNAME" = "win32" ; then
    AC_CHECK_HEADERS(crtdefs.h)
 fi
diff --git a/src/Makefile.global.in b/src/Makefile.global.in
index bbdc1c4bda..c9c61a9c99 100644
--- a/src/Makefile.global.in
+++ b/src/Makefile.global.in
@@ -193,6 +193,7 @@ with_ldap	= @with_ldap@
 with_libxml	= @with_libxml@
 with_libxslt	= @with_libxslt@
 with_llvm	= @with_llvm@
+with_oauth	= @with_oauth@
 with_system_tzdata = @with_system_tzdata@
 with_uuid	= @with_uuid@
 with_zlib	= @with_zlib@
diff --git a/src/include/common/oauth-common.h b/src/include/common/oauth-common.h
new file mode 100644
index 0000000000..3fa95ac7e8
--- /dev/null
+++ b/src/include/common/oauth-common.h
@@ -0,0 +1,19 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth-common.h
+ *		Declarations for helper functions used for OAuth/OIDC authentication
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/common/oauth-common.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef OAUTH_COMMON_H
+#define OAUTH_COMMON_H
+
+/* Name of SASL mechanism per IANA */
+#define OAUTHBEARER_NAME "OAUTHBEARER"
+
+#endif /* OAUTH_COMMON_H */
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 635fbb2181..1b3332601e 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -319,6 +319,9 @@
 /* Define to 1 if you have the `crypto' library (-lcrypto). */
 #undef HAVE_LIBCRYPTO
 
+/* 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
 
@@ -922,6 +925,9 @@
 /* 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 build with OpenSSL support. (--with-ssl=openssl) */
 #undef USE_OPENSSL
 
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 3c53393fa4..727305c578 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -62,6 +62,11 @@ OBJS += \
 	fe-secure-gssapi.o
 endif
 
+ifeq ($(with_oauth),yes)
+OBJS += \
+	fe-auth-oauth.o
+endif
+
 ifeq ($(PORTNAME), cygwin)
 override shlib = cyg$(NAME)$(DLSUFFIX)
 endif
@@ -83,7 +88,7 @@ endif
 # that are built correctly for use in a shlib.
 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 -liddawc -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
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
new file mode 100644
index 0000000000..383c9d4bdb
--- /dev/null
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -0,0 +1,744 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-auth-oauth.c
+ *	   The front-end (client) implementation of OAuth/OIDC authentication.
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-auth-oauth.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include <iddawc.h>
+
+#include "postgres_fe.h"
+
+#include "common/base64.h"
+#include "common/hmac.h"
+#include "common/jsonapi.h"
+#include "common/oauth-common.h"
+#include "fe-auth.h"
+#include "mb/pg_wchar.h"
+
+/* The exported OAuth callback mechanism. */
+static void *oauth_init(PGconn *conn, const char *password,
+						const char *sasl_mechanism);
+static void oauth_exchange(void *opaq, bool final,
+						   char *input, int inputlen,
+						   char **output, int *outputlen,
+						   bool *done, bool *success);
+static bool oauth_channel_bound(void *opaq);
+static void oauth_free(void *opaq);
+
+const pg_fe_sasl_mech pg_oauth_mech = {
+	oauth_init,
+	oauth_exchange,
+	oauth_channel_bound,
+	oauth_free,
+};
+
+typedef enum
+{
+	FE_OAUTH_INIT,
+	FE_OAUTH_BEARER_SENT,
+	FE_OAUTH_SERVER_ERROR,
+} fe_oauth_state_enum;
+
+typedef struct
+{
+	fe_oauth_state_enum state;
+
+	PGconn	   *conn;
+} fe_oauth_state;
+
+static void *
+oauth_init(PGconn *conn, const char *password,
+		   const char *sasl_mechanism)
+{
+	fe_oauth_state *state;
+
+	/*
+	 * We only support one SASL mechanism here; anything else is programmer
+	 * error.
+	 */
+	Assert(sasl_mechanism != NULL);
+	Assert(!strcmp(sasl_mechanism, OAUTHBEARER_NAME));
+
+	state = malloc(sizeof(*state));
+	if (!state)
+		return NULL;
+
+	state->state = FE_OAUTH_INIT;
+	state->conn = conn;
+
+	return state;
+}
+
+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);
+}
+
+static char *
+get_auth_token(PGconn *conn)
+{
+	PQExpBuffer	token_buf = NULL;
+	struct _i_session session;
+	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;
+
+	if (!conn->oauth_discovery_uri)
+		return strdup(""); /* ask the server for one */
+
+	if (!conn->oauth_client_id)
+	{
+		/* We can't talk to a server without a client identifier. */
+		appendPQExpBufferStr(&conn->errorMessage,
+							 libpq_gettext("no oauth_client_id is set for the connection"));
+		return NULL;
+	}
+
+	i_init_session(&session);
+
+	token_buf = createPQExpBuffer();
+	if (!token_buf)
+		goto cleanup;
+
+	err = i_set_str_parameter(&session, I_OPT_OPENID_CONFIG_ENDPOINT, conn->oauth_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,
+								"OAuth token retrieval failed");
+			goto cleanup;
+		}
+
+		if (!user_prompted)
+		{
+			/*
+			 * Now that we know the token endpoint isn't broken, give the user
+			 * the login instructions.
+			 */
+			pqInternalNotice(&conn->noticeHooks,
+							 "Visit %s and enter the code: %s",
+							 verification_uri, user_code);
+
+			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;
+}
+
+#define kvsep "\x01"
+
+static char *
+client_initial_response(PGconn *conn)
+{
+	static const char * const resp_format = "n,," kvsep "auth=%s" kvsep kvsep;
+
+	PQExpBuffer	token_buf;
+	PQExpBuffer	discovery_buf = NULL;
+	char	   *token = NULL;
+	char	   *response = NULL;
+
+	token_buf = createPQExpBuffer();
+	if (!token_buf)
+		goto cleanup;
+
+	/*
+	 * If we don't yet have a discovery URI, but the user gave us an explicit
+	 * issuer, use the .well-known discovery URI for that issuer.
+	 */
+	if (!conn->oauth_discovery_uri && conn->oauth_issuer)
+	{
+		discovery_buf = createPQExpBuffer();
+		if (!discovery_buf)
+			goto cleanup;
+
+		appendPQExpBufferStr(discovery_buf, conn->oauth_issuer);
+		appendPQExpBufferStr(discovery_buf, "/.well-known/openid-configuration");
+
+		if (PQExpBufferBroken(discovery_buf))
+			goto cleanup;
+
+		conn->oauth_discovery_uri = strdup(discovery_buf->data);
+	}
+
+	token = get_auth_token(conn);
+	if (!token)
+		goto cleanup;
+
+	appendPQExpBuffer(token_buf, resp_format, token);
+	if (PQExpBufferBroken(token_buf))
+		goto cleanup;
+
+	response = strdup(token_buf->data);
+
+cleanup:
+	if (token)
+		free(token);
+	if (discovery_buf)
+		destroyPQExpBuffer(discovery_buf);
+	if (token_buf)
+		destroyPQExpBuffer(token_buf);
+
+	return response;
+}
+
+#define ERROR_STATUS_FIELD "status"
+#define ERROR_SCOPE_FIELD "scope"
+#define ERROR_OPENID_CONFIGURATION_FIELD "openid-configuration"
+
+struct json_ctx
+{
+	char		   *errmsg; /* any non-NULL value stops all processing */
+	PQExpBufferData errbuf; /* backing memory for errmsg */
+	int				nested; /* nesting level (zero is the top) */
+
+	const char	   *target_field_name; /* points to a static allocation */
+	char		  **target_field;      /* see below */
+
+	/* target_field, if set, points to one of the following: */
+	char		   *status;
+	char		   *scope;
+	char		   *discovery_uri;
+};
+
+#define oauth_json_has_error(ctx) \
+	(PQExpBufferDataBroken((ctx)->errbuf) || (ctx)->errmsg)
+
+#define oauth_json_set_error(ctx, ...) \
+	do { \
+		appendPQExpBuffer(&(ctx)->errbuf, __VA_ARGS__); \
+		(ctx)->errmsg = (ctx)->errbuf.data; \
+	} while (0)
+
+static void
+oauth_json_object_start(void *state)
+{
+	struct json_ctx	   *ctx = state;
+
+	if (oauth_json_has_error(ctx))
+		return; /* short-circuit */
+
+	if (ctx->target_field)
+	{
+		Assert(ctx->nested == 1);
+
+		oauth_json_set_error(ctx,
+							 libpq_gettext("field \"%s\" must be a string"),
+							 ctx->target_field_name);
+	}
+
+	++ctx->nested;
+}
+
+static void
+oauth_json_object_end(void *state)
+{
+	struct json_ctx	   *ctx = state;
+
+	if (oauth_json_has_error(ctx))
+		return; /* short-circuit */
+
+	--ctx->nested;
+}
+
+static void
+oauth_json_object_field_start(void *state, char *name, bool isnull)
+{
+	struct json_ctx	   *ctx = state;
+
+	if (oauth_json_has_error(ctx))
+	{
+		/* short-circuit */
+		free(name);
+		return;
+	}
+
+	if (ctx->nested == 1)
+	{
+		if (!strcmp(name, ERROR_STATUS_FIELD))
+		{
+			ctx->target_field_name = ERROR_STATUS_FIELD;
+			ctx->target_field = &ctx->status;
+		}
+		else if (!strcmp(name, ERROR_SCOPE_FIELD))
+		{
+			ctx->target_field_name = ERROR_SCOPE_FIELD;
+			ctx->target_field = &ctx->scope;
+		}
+		else if (!strcmp(name, ERROR_OPENID_CONFIGURATION_FIELD))
+		{
+			ctx->target_field_name = ERROR_OPENID_CONFIGURATION_FIELD;
+			ctx->target_field = &ctx->discovery_uri;
+		}
+	}
+
+	free(name);
+}
+
+static void
+oauth_json_array_start(void *state)
+{
+	struct json_ctx	   *ctx = state;
+
+	if (oauth_json_has_error(ctx))
+		return; /* short-circuit */
+
+	if (!ctx->nested)
+	{
+		ctx->errmsg = libpq_gettext("top-level element must be an object");
+	}
+	else if (ctx->target_field)
+	{
+		Assert(ctx->nested == 1);
+
+		oauth_json_set_error(ctx,
+							 libpq_gettext("field \"%s\" must be a string"),
+							 ctx->target_field_name);
+	}
+}
+
+static void
+oauth_json_scalar(void *state, char *token, JsonTokenType type)
+{
+	struct json_ctx	   *ctx = state;
+
+	if (oauth_json_has_error(ctx))
+	{
+		/* short-circuit */
+		free(token);
+		return;
+	}
+
+	if (!ctx->nested)
+	{
+		ctx->errmsg = libpq_gettext("top-level element must be an object");
+	}
+	else if (ctx->target_field)
+	{
+		Assert(ctx->nested == 1);
+
+		if (type == JSON_TOKEN_STRING)
+		{
+			*ctx->target_field = token;
+
+			ctx->target_field = NULL;
+			ctx->target_field_name = NULL;
+
+			return; /* don't free the token we're using */
+		}
+
+		oauth_json_set_error(ctx,
+							 libpq_gettext("field \"%s\" must be a string"),
+							 ctx->target_field_name);
+	}
+
+	free(token);
+}
+
+static bool
+handle_oauth_sasl_error(PGconn *conn, char *msg, int msglen)
+{
+	JsonLexContext		lex = {0};
+	JsonSemAction		sem = {0};
+	JsonParseErrorType	err;
+	struct json_ctx		ctx = {0};
+	char			   *errmsg = NULL;
+
+	/* Sanity check. */
+	if (strlen(msg) != msglen)
+	{
+		appendPQExpBufferStr(&conn->errorMessage,
+							 libpq_gettext("server's error message contained an embedded NULL"));
+		return false;
+	}
+
+	initJsonLexContextCstringLen(&lex, msg, msglen, PG_UTF8, true);
+
+	initPQExpBuffer(&ctx.errbuf);
+	sem.semstate = &ctx;
+
+	sem.object_start = oauth_json_object_start;
+	sem.object_end = oauth_json_object_end;
+	sem.object_field_start = oauth_json_object_field_start;
+	sem.array_start = oauth_json_array_start;
+	sem.scalar = oauth_json_scalar;
+
+	err = pg_parse_json(&lex, &sem);
+
+	if (err != JSON_SUCCESS)
+	{
+		errmsg = json_errdetail(err, &lex);
+	}
+	else if (PQExpBufferDataBroken(ctx.errbuf))
+	{
+		errmsg = libpq_gettext("out of memory");
+	}
+	else if (ctx.errmsg)
+	{
+		errmsg = ctx.errmsg;
+	}
+
+	if (errmsg)
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("failed to parse server's error response: %s"),
+						  errmsg);
+
+	/* Don't need the error buffer or the JSON lexer anymore. */
+	termPQExpBuffer(&ctx.errbuf);
+	termJsonLexContext(&lex);
+
+	if (errmsg)
+		return false;
+
+	/* TODO: what if these override what the user already specified? */
+	if (ctx.discovery_uri)
+	{
+		if (conn->oauth_discovery_uri)
+			free(conn->oauth_discovery_uri);
+
+		conn->oauth_discovery_uri = ctx.discovery_uri;
+	}
+
+	if (ctx.scope)
+	{
+		if (conn->oauth_scope)
+			free(conn->oauth_scope);
+
+		conn->oauth_scope = ctx.scope;
+	}
+	/* TODO: missing error scope should clear any existing connection scope */
+
+	if (!ctx.status)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("server sent error response without a status"));
+		return false;
+	}
+
+	if (!strcmp(ctx.status, "invalid_token"))
+	{
+		/*
+		 * invalid_token is the only error code we'll automatically retry for,
+		 * but only if we have enough information to do so.
+		 */
+		if (conn->oauth_discovery_uri)
+			conn->oauth_want_retry = true;
+	}
+	/* TODO: include status in hard failure message */
+
+	return true;
+}
+
+static void
+oauth_exchange(void *opaq, bool final,
+			   char *input, int inputlen,
+			   char **output, int *outputlen,
+			   bool *done, bool *success)
+{
+	fe_oauth_state *state = opaq;
+	PGconn	   *conn = state->conn;
+
+	*done = false;
+	*success = false;
+	*output = NULL;
+	*outputlen = 0;
+
+	switch (state->state)
+	{
+		case FE_OAUTH_INIT:
+			Assert(inputlen == -1);
+
+			*output = client_initial_response(conn);
+			if (!*output)
+				goto error;
+
+			*outputlen = strlen(*output);
+			state->state = FE_OAUTH_BEARER_SENT;
+
+			break;
+
+		case FE_OAUTH_BEARER_SENT:
+			if (final)
+			{
+				/* TODO: ensure there is no message content here. */
+				*done = true;
+				*success = true;
+
+				break;
+			}
+
+			/*
+			 * Error message sent by the server.
+			 */
+			if (!handle_oauth_sasl_error(conn, input, inputlen))
+				goto error;
+
+			/*
+			 * Respond with the required dummy message (RFC 7628, sec. 3.2.3).
+			 */
+			*output = strdup(kvsep);
+			*outputlen = strlen(*output); /* == 1 */
+
+			state->state = FE_OAUTH_SERVER_ERROR;
+			break;
+
+		case FE_OAUTH_SERVER_ERROR:
+			/*
+			 * After an error, the server should send an error response to fail
+			 * the SASL handshake, which is handled in higher layers.
+			 *
+			 * If we get here, the server either sent *another* challenge which
+			 * isn't defined in the RFC, or completed the handshake successfully
+			 * after telling us it was going to fail. Neither is acceptable.
+			 */
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("server sent additional OAuth data after error\n"));
+			goto error;
+
+		default:
+			appendPQExpBufferStr(&conn->errorMessage,
+								 libpq_gettext("invalid OAuth exchange state\n"));
+			goto error;
+	}
+
+	return;
+
+error:
+	*done = true;
+	*success = false;
+}
+
+static bool
+oauth_channel_bound(void *opaq)
+{
+	/* This mechanism does not support channel binding. */
+	return false;
+}
+
+static void
+oauth_free(void *opaq)
+{
+	fe_oauth_state *state = opaq;
+
+	free(state);
+}
diff --git a/src/interfaces/libpq/fe-auth-sasl.h b/src/interfaces/libpq/fe-auth-sasl.h
index da3c30b87b..b1bb382f70 100644
--- a/src/interfaces/libpq/fe-auth-sasl.h
+++ b/src/interfaces/libpq/fe-auth-sasl.h
@@ -65,6 +65,8 @@ typedef struct pg_fe_sasl_mech
 	 *
 	 *	state:	   The opaque mechanism state returned by init()
 	 *
+	 *	final:	   true if the server has sent a final exchange outcome
+	 *
 	 *	input:	   The challenge data sent by the server, or NULL when
 	 *			   generating a client-first initial response (that is, when
 	 *			   the server expects the client to send a message to start
@@ -92,7 +94,8 @@ typedef struct pg_fe_sasl_mech
 	 *			   Ignored if *done is false.
 	 *--------
 	 */
-	void		(*exchange) (void *state, char *input, int inputlen,
+	void		(*exchange) (void *state, bool final,
+							 char *input, int inputlen,
 							 char **output, int *outputlen,
 							 bool *done, bool *success);
 
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index e616200704..681b76adbe 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -24,7 +24,8 @@
 /* The exported SCRAM callback mechanism. */
 static void *scram_init(PGconn *conn, const char *password,
 						const char *sasl_mechanism);
-static void scram_exchange(void *opaq, char *input, int inputlen,
+static void scram_exchange(void *opaq, bool final,
+						   char *input, int inputlen,
 						   char **output, int *outputlen,
 						   bool *done, bool *success);
 static bool scram_channel_bound(void *opaq);
@@ -206,7 +207,8 @@ scram_free(void *opaq)
  * Exchange a SCRAM message with backend.
  */
 static void
-scram_exchange(void *opaq, char *input, int inputlen,
+scram_exchange(void *opaq, bool final,
+			   char *input, int inputlen,
 			   char **output, int *outputlen,
 			   bool *done, bool *success)
 {
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 6fceff561b..2567a34023 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -38,6 +38,7 @@
 #endif
 
 #include "common/md5.h"
+#include "common/oauth-common.h"
 #include "common/scram-common.h"
 #include "fe-auth.h"
 #include "fe-auth-sasl.h"
@@ -422,7 +423,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 	bool		success;
 	const char *selected_mechanism;
 	PQExpBufferData mechanism_buf;
-	char	   *password;
+	char	   *password = NULL;
 
 	initPQExpBuffer(&mechanism_buf);
 
@@ -444,8 +445,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 	/*
 	 * Parse the list of SASL authentication mechanisms in the
 	 * AuthenticationSASL message, and select the best mechanism that we
-	 * support.  SCRAM-SHA-256-PLUS and SCRAM-SHA-256 are the only ones
-	 * supported at the moment, listed by order of decreasing importance.
+	 * support.  Mechanisms are listed by order of decreasing importance.
 	 */
 	selected_mechanism = NULL;
 	for (;;)
@@ -485,6 +485,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 				{
 					selected_mechanism = SCRAM_SHA_256_PLUS_NAME;
 					conn->sasl = &pg_scram_mech;
+					conn->password_needed = true;
 				}
 #else
 				/*
@@ -522,7 +523,17 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		{
 			selected_mechanism = SCRAM_SHA_256_NAME;
 			conn->sasl = &pg_scram_mech;
+			conn->password_needed = true;
 		}
+#ifdef USE_OAUTH
+		else if (strcmp(mechanism_buf.data, OAUTHBEARER_NAME) == 0 &&
+				!selected_mechanism)
+		{
+			selected_mechanism = OAUTHBEARER_NAME;
+			conn->sasl = &pg_oauth_mech;
+			conn->password_needed = false;
+		}
+#endif
 	}
 
 	if (!selected_mechanism)
@@ -547,18 +558,19 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 
 	/*
 	 * First, select the password to use for the exchange, complaining if
-	 * there isn't one.  Currently, all supported SASL mechanisms require a
-	 * password, so we can just go ahead here without further distinction.
+	 * there isn't one and the SASL mechanism needs it.
 	 */
-	conn->password_needed = true;
-	password = conn->connhost[conn->whichhost].password;
-	if (password == NULL)
-		password = conn->pgpass;
-	if (password == NULL || password[0] == '\0')
+	if (conn->password_needed)
 	{
-		appendPQExpBufferStr(&conn->errorMessage,
-							 PQnoPasswordSupplied);
-		goto error;
+		password = conn->connhost[conn->whichhost].password;
+		if (password == NULL)
+			password = conn->pgpass;
+		if (password == NULL || password[0] == '\0')
+		{
+			appendPQExpBufferStr(&conn->errorMessage,
+								 PQnoPasswordSupplied);
+			goto error;
+		}
 	}
 
 	Assert(conn->sasl);
@@ -576,7 +588,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		goto oom_error;
 
 	/* Get the mechanism-specific Initial Client Response, if any */
-	conn->sasl->exchange(conn->sasl_state,
+	conn->sasl->exchange(conn->sasl_state, false,
 						 NULL, -1,
 						 &initialresponse, &initialresponselen,
 						 &done, &success);
@@ -657,7 +669,7 @@ pg_SASL_continue(PGconn *conn, int payloadlen, bool final)
 	/* For safety and convenience, ensure the buffer is NULL-terminated. */
 	challenge[payloadlen] = '\0';
 
-	conn->sasl->exchange(conn->sasl_state,
+	conn->sasl->exchange(conn->sasl_state, final,
 						 challenge, payloadlen,
 						 &output, &outputlen,
 						 &done, &success);
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 049a8bb1a1..2a56774019 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -28,4 +28,7 @@ extern const pg_fe_sasl_mech pg_scram_mech;
 extern char *pg_fe_scram_build_secret(const char *password,
 									  const char **errstr);
 
+/* Mechanisms in fe-auth-oauth.c */
+extern const pg_fe_sasl_mech pg_oauth_mech;
+
 #endif							/* FE_AUTH_H */
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 1c5a2b43e9..5f78439586 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -344,6 +344,23 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	/* OAuth v2 */
+	{"oauth_issuer", NULL, NULL, NULL,
+		"OAuth-Issuer", "", 40,
+	offsetof(struct pg_conn, oauth_issuer)},
+
+	{"oauth_client_id", NULL, NULL, NULL,
+		"OAuth-Client-ID", "", 40,
+	offsetof(struct pg_conn, oauth_client_id)},
+
+	{"oauth_client_secret", NULL, NULL, NULL,
+		"OAuth-Client-Secret", "", 40,
+	offsetof(struct pg_conn, oauth_client_secret)},
+
+	{"oauth_scope", NULL, NULL, NULL,
+		"OAuth-Scope", "", 15,
+	offsetof(struct pg_conn, oauth_scope)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -606,6 +623,7 @@ pqDropServerData(PGconn *conn)
 	conn->write_err_msg = NULL;
 	conn->be_pid = 0;
 	conn->be_key = 0;
+	/* conn->oauth_want_retry = false; TODO */
 }
 
 
@@ -3381,6 +3399,16 @@ keep_going:						/* We will come back to here until there is
 					/* Check to see if we should mention pgpassfile */
 					pgpassfileWarning(conn);
 
+#ifdef USE_OAUTH
+					if (conn->sasl == &pg_oauth_mech
+						&& conn->oauth_want_retry)
+					{
+						/* TODO: only allow retry once */
+						need_new_connection = true;
+						goto keep_going;
+					}
+#endif
+
 #ifdef ENABLE_GSS
 
 					/*
@@ -4161,6 +4189,16 @@ freePGconn(PGconn *conn)
 		free(conn->rowBuf);
 	if (conn->target_session_attrs)
 		free(conn->target_session_attrs);
+	if (conn->oauth_issuer)
+		free(conn->oauth_issuer);
+	if (conn->oauth_discovery_uri)
+		free(conn->oauth_discovery_uri);
+	if (conn->oauth_client_id)
+		free(conn->oauth_client_id);
+	if (conn->oauth_client_secret)
+		free(conn->oauth_client_secret);
+	if (conn->oauth_scope)
+		free(conn->oauth_scope);
 	termPQExpBuffer(&conn->errorMessage);
 	termPQExpBuffer(&conn->workBuffer);
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index e0cee4b142..0dff13505a 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -394,6 +394,14 @@ struct pg_conn
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
 
+	/* OAuth v2 */
+	char	   *oauth_issuer;			/* token issuer URL */
+	char	   *oauth_discovery_uri;	/* URI of the issuer's discovery document */
+	char	   *oauth_client_id;		/* client identifier */
+	char	   *oauth_client_secret;	/* client secret */
+	char	   *oauth_scope;			/* access token scope */
+	bool		oauth_want_retry;		/* should we retry on failure? */
+
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
 	int			traceFlags;
-- 
2.25.1

