From ee8e85d3416f381ba9d44f8d4a681e5006bd5b82 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchampion@vmware.com>
Date: Tue, 4 May 2021 16:21:11 -0700
Subject: [PATCH 5/7] backend: add OAUTHBEARER SASL mechanism

DO NOT USE THIS PROOF OF CONCEPT IN PRODUCTION.

Implement OAUTHBEARER (RFC 7628) on the server side. This adds a new
auth method, oauth, to pg_hba.

Because OAuth implementations vary so wildly, and bearer token
validation is heavily dependent on the issuing party, authn/z is done by
communicating with an external program: the oauth_validator_command.
This command must do the following:

1. Receive the bearer token by reading its contents from a file
   descriptor passed from the server. (The numeric value of this
   descriptor may be inserted into the oauth_validator_command using the
   %f specifier.)

   This MUST be the first action the command performs. The server will
   not begin reading stdout from the command until the token has been
   read in full, so if the command tries to print anything and hits a
   buffer limit, the backend will deadlock and time out.

2. Validate the bearer token. The correct way to do this depends on the
   issuer, but it generally involves either cryptographic operations to
   prove that the token was issued by a trusted party, or the
   presentation of the bearer token to some other party so that _it_ can
   perform validation.

   The command MUST maintain confidentiality of the bearer token, since
   in most cases it can be used just like a password. (There are ways to
   cryptographically bind tokens to client certificates, but they are
   way beyond the scope of this commit message.)

   If the token cannot be validated, the command must exit with a
   non-zero status. Further authentication/authorization is pointless if
   the bearer token wasn't issued by someone you trust.

3. Authenticate the user, authorize the user, or both:

   a. To authenticate the user, use the bearer token to retrieve some
      trusted identifier string for the end user. The exact process for
      this is, again, issuer-dependent. The command should print the
      authenticated identity string to stdout, followed by a newline.

      If the user cannot be authenticated, the validator should not
      print anything to stdout. It should also exit with a non-zero
      status, unless the token may be used to authorize the connection
      through some other means (see below).

      On a success, the command may then exit with a zero success code.
      By default, the server will then check to make sure the identity
      string matches the role that is being used (or matches a usermap
      entry, if one is in use).

   b. To optionally authorize the user, in combination with the HBA
      option trust_validator_authz=1 (see below), the validator simply
      returns a zero exit code if the client should be allowed to
      connect with its presented role (which can be passed to the
      command using the %r specifier), or a non-zero code otherwise.

      The hard part is in determining whether the given token truly
      authorizes the client to use the given role, which must
      unfortunately be left as an exercise to the reader.

      This obviously requires some care, as a poorly implemented token
      validator may silently open the entire database to anyone with a
      bearer token. But it may be a more portable approach, since OAuth
      is designed as an authorization framework, not an authentication
      framework. For example, the user's bearer token could carry an
      "allow_superuser_access" claim, which would authorize pseudonymous
      database access as any role. It's then up to the OAuth system
      administrators to ensure that allow_superuser_access is doled out
      only to the proper users.

   c. It's possible that the user can be successfully authenticated but
      isn't authorized to connect. In this case, the command may print
      the authenticated ID and then fail with a non-zero exit code.
      (This makes it easier to see what's going on in the Postgres
      logs.)

4. Token validators may optionally log to stderr. This will be printed
   verbatim into the Postgres server logs.

The oauth method supports the following HBA options (but note that two
of them are not optional, since we have no way of choosing sensible
defaults):

  issuer: Required. The URL of the OAuth issuing party, which the client
          must contact to receive a bearer token.

          Some real-world examples as of time of writing:
          - https://accounts.google.com
          - https://login.microsoft.com/[tenant-id]/v2.0

  scope:  Required. The OAuth scope(s) required for the server to
          authenticate and/or authorize the user. This is heavily
          deployment-specific, but a simple example is "openid email".

  map:    Optional. Specify a standard PostgreSQL user map; this works
          the same as with other auth methods such as peer. If a map is
          not specified, the user ID returned by the token validator
          must exactly match the role that's being requested (but see
          trust_validator_authz, below).

  trust_validator_authz:
          Optional. When set to 1, this allows the token validator to
          take full control of the authorization process. Standard user
          mapping is skipped: if the validator command succeeds, the
          client is allowed to connect under its desired role and no
          further checks are done.

Unlike the client, servers support OAuth without needing to be built
against libiddawc (since the responsibility for "speaking" OAuth/OIDC
correctly is delegated entirely to the oauth_validator_command).

Several TODOs:
- port to platforms other than "modern Linux"
- overhaul the communication with oauth_validator_command, which is
  currently a bad hack on OpenPipeStream()
- implement more sanity checks on the OAUTHBEARER message format and
  tokens sent by the client
- implement more helpful handling of HBA misconfigurations
- properly interpolate JSON when generating error responses
- use logdetail during auth failures
- deal with role names that can't be safely passed to system() without
  shell-escaping
- allow passing the configured issuer to the oauth_validator_command, to
  deal with multi-issuer setups
- ...and more.
---
 src/backend/libpq/Makefile     |   1 +
 src/backend/libpq/auth-oauth.c | 797 +++++++++++++++++++++++++++++++++
 src/backend/libpq/auth-scram.c |   2 +
 src/backend/libpq/auth.c       |  43 +-
 src/backend/libpq/hba.c        |  29 +-
 src/backend/utils/misc/guc.c   |  12 +
 src/include/libpq/auth.h       |   1 +
 src/include/libpq/hba.h        |   8 +-
 src/include/libpq/oauth.h      |  24 +
 src/include/libpq/sasl.h       |  26 ++
 10 files changed, 915 insertions(+), 28 deletions(-)
 create mode 100644 src/backend/libpq/auth-oauth.c
 create mode 100644 src/include/libpq/oauth.h

diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile
index 8d1d16b0fc..40f2c50c3c 100644
--- a/src/backend/libpq/Makefile
+++ b/src/backend/libpq/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
 # be-fsstubs is here for historical reasons, probably belongs elsewhere
 
 OBJS = \
+	auth-oauth.o \
 	auth-scram.o \
 	auth.o \
 	be-fsstubs.o \
diff --git a/src/backend/libpq/auth-oauth.c b/src/backend/libpq/auth-oauth.c
new file mode 100644
index 0000000000..b2b9d56e7c
--- /dev/null
+++ b/src/backend/libpq/auth-oauth.c
@@ -0,0 +1,797 @@
+/*-------------------------------------------------------------------------
+ *
+ * auth-oauth.c
+ *	  Server-side implementation of the SASL OAUTHBEARER mechanism.
+ *
+ * See the following RFC for more details:
+ * - RFC 7628: https://tools.ietf.org/html/rfc7628
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/backend/libpq/auth-oauth.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "common/oauth-common.h"
+#include "lib/stringinfo.h"
+#include "libpq/auth.h"
+#include "libpq/hba.h"
+#include "libpq/oauth.h"
+#include "libpq/sasl.h"
+#include "storage/fd.h"
+
+/* GUC */
+char *oauth_validator_command;
+
+static void  oauth_get_mechanisms(Port *port, StringInfo buf);
+static void *oauth_init(Port *port, const char *selected_mech, const char *shadow_pass);
+static int   oauth_exchange(void *opaq, const char *input, int inputlen,
+							char **output, int *outputlen, char **logdetail);
+
+/* Mechanism declaration */
+const pg_be_sasl_mech pg_be_oauth_mech = {
+	oauth_get_mechanisms,
+	oauth_init,
+	oauth_exchange,
+
+	PG_MAX_AUTH_TOKEN_LENGTH,
+};
+
+
+typedef enum
+{
+	OAUTH_STATE_INIT = 0,
+	OAUTH_STATE_ERROR,
+	OAUTH_STATE_FINISHED,
+} oauth_state;
+
+struct oauth_ctx
+{
+	oauth_state	state;
+	Port	   *port;
+	const char *issuer;
+	const char *scope;
+};
+
+static char *sanitize_char(char c);
+static char *parse_kvpairs_for_auth(char **input);
+static void generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen);
+static bool validate(Port *port, const char *auth, char **logdetail);
+static bool run_validator_command(Port *port, const char *token);
+static bool check_exit(FILE **fh, const char *command);
+static bool unset_cloexec(int fd);
+static bool username_ok_for_shell(const char *username);
+
+#define KVSEP 0x01
+#define AUTH_KEY "auth"
+#define BEARER_SCHEME "Bearer "
+
+static void
+oauth_get_mechanisms(Port *port, StringInfo buf)
+{
+	/* Only OAUTHBEARER is supported. */
+	appendStringInfoString(buf, OAUTHBEARER_NAME);
+	appendStringInfoChar(buf, '\0');
+}
+
+static void *
+oauth_init(Port *port, const char *selected_mech, const char *shadow_pass)
+{
+	struct oauth_ctx *ctx;
+
+	if (strcmp(selected_mech, OAUTHBEARER_NAME))
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("client selected an invalid SASL authentication mechanism")));
+
+	ctx = palloc0(sizeof(*ctx));
+
+	ctx->state = OAUTH_STATE_INIT;
+	ctx->port = port;
+
+	Assert(port->hba);
+	ctx->issuer = port->hba->oauth_issuer;
+	ctx->scope = port->hba->oauth_scope;
+
+	return ctx;
+}
+
+static int
+oauth_exchange(void *opaq, const char *input, int inputlen,
+			   char **output, int *outputlen, char **logdetail)
+{
+	char   *p;
+	char	cbind_flag;
+	char   *auth;
+
+	struct oauth_ctx *ctx = opaq;
+
+	*output = NULL;
+	*outputlen = -1;
+
+	/*
+	 * If the client didn't include an "Initial Client Response" in the
+	 * SASLInitialResponse message, send an empty challenge, to which the
+	 * client will respond with the same data that usually comes in the
+	 * Initial Client Response.
+	 */
+	if (input == NULL)
+	{
+		Assert(ctx->state == OAUTH_STATE_INIT);
+
+		*output = pstrdup("");
+		*outputlen = 0;
+		return SASL_EXCHANGE_CONTINUE;
+	}
+
+	/*
+	 * Check that the input length agrees with the string length of the input.
+	 */
+	if (inputlen == 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("malformed OAUTHBEARER message"),
+				 errdetail("The message is empty.")));
+	if (inputlen != strlen(input))
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("malformed OAUTHBEARER message"),
+				 errdetail("Message length does not match input length.")));
+
+	switch (ctx->state)
+	{
+		case OAUTH_STATE_INIT:
+			/* Handle this case below. */
+			break;
+
+		case OAUTH_STATE_ERROR:
+			/*
+			 * Only one response is valid for the client during authentication
+			 * failure: a single kvsep.
+			 */
+			if (inputlen != 1 || *input != KVSEP)
+				ereport(ERROR,
+						(errcode(ERRCODE_PROTOCOL_VIOLATION),
+						 errmsg("malformed OAUTHBEARER message"),
+						 errdetail("Client did not send a kvsep response.")));
+
+			/* The (failed) handshake is now complete. */
+			ctx->state = OAUTH_STATE_FINISHED;
+			return SASL_EXCHANGE_FAILURE;
+
+		default:
+			elog(ERROR, "invalid OAUTHBEARER exchange state");
+			return SASL_EXCHANGE_FAILURE;
+	}
+
+	/* Handle the client's initial message. */
+	p = strdup(input);
+
+	/*
+	 * OAUTHBEARER does not currently define a channel binding (so there is no
+	 * OAUTHBEARER-PLUS, and we do not accept a 'p' specifier). We accept a 'y'
+	 * specifier purely for the remote chance that a future specification could
+	 * define one; then future clients can still interoperate with this server
+	 * implementation. 'n' is the expected case.
+	 */
+	cbind_flag = *p;
+	switch (cbind_flag)
+	{
+		case 'p':
+			ereport(ERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("malformed OAUTHBEARER message"),
+					 errdetail("The server does not support channel binding for OAuth, but the client message includes channel binding data.")));
+			break;
+
+		case 'y': /* fall through */
+		case 'n':
+			p++;
+			if (*p != ',')
+				ereport(ERROR,
+						(errcode(ERRCODE_PROTOCOL_VIOLATION),
+						 errmsg("malformed OAUTHBEARER message"),
+						 errdetail("Comma expected, but found character \"%s\".",
+								   sanitize_char(*p))));
+			p++;
+			break;
+
+		default:
+			ereport(ERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("malformed OAUTHBEARER message"),
+					 errdetail("Unexpected channel-binding flag \"%s\".",
+							   sanitize_char(cbind_flag))));
+	}
+
+	/*
+	 * Forbid optional authzid (authorization identity).  We don't support it.
+	 */
+	if (*p == 'a')
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("client uses authorization identity, but it is not supported")));
+	if (*p != ',')
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("malformed OAUTHBEARER message"),
+				 errdetail("Unexpected attribute \"%s\" in client-first-message.",
+						   sanitize_char(*p))));
+	p++;
+
+	/* All remaining fields are separated by the RFC's kvsep (\x01). */
+	if (*p != KVSEP)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("malformed OAUTHBEARER message"),
+				 errdetail("Key-value separator expected, but found character \"%s\".",
+						   sanitize_char(*p))));
+	p++;
+
+	auth = parse_kvpairs_for_auth(&p);
+	if (!auth)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("malformed OAUTHBEARER message"),
+				 errdetail("Message does not contain an auth value.")));
+
+	/* We should be at the end of our message. */
+	if (*p)
+		ereport(ERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("malformed OAUTHBEARER message"),
+				 errdetail("Message contains additional data after the final terminator.")));
+
+	if (!validate(ctx->port, auth, logdetail))
+	{
+		generate_error_response(ctx, output, outputlen);
+
+		ctx->state = OAUTH_STATE_ERROR;
+		return SASL_EXCHANGE_CONTINUE;
+	}
+
+	ctx->state = OAUTH_STATE_FINISHED;
+	return SASL_EXCHANGE_SUCCESS;
+}
+
+/*
+ * Convert an arbitrary byte to printable form.  For error messages.
+ *
+ * If it's a printable ASCII character, print it as a single character.
+ * otherwise, print it in hex.
+ *
+ * The returned pointer points to a static buffer.
+ */
+static char *
+sanitize_char(char c)
+{
+	static char buf[5];
+
+	if (c >= 0x21 && c <= 0x7E)
+		snprintf(buf, sizeof(buf), "'%c'", c);
+	else
+		snprintf(buf, sizeof(buf), "0x%02x", (unsigned char) c);
+	return buf;
+}
+
+/*
+ * Consumes all kvpairs in an OAUTHBEARER exchange message. If the "auth" key is
+ * found, its value is returned.
+ */
+static char *
+parse_kvpairs_for_auth(char **input)
+{
+	char   *pos = *input;
+	char   *auth = NULL;
+
+	/*
+	 * The relevant ABNF, from Sec. 3.1:
+	 *
+	 *     kvsep          = %x01
+	 *     key            = 1*(ALPHA)
+	 *     value          = *(VCHAR / SP / HTAB / CR / LF )
+	 *     kvpair         = key "=" value kvsep
+	 *   ;;gs2-header     = See RFC 5801
+	 *     client-resp    = (gs2-header kvsep *kvpair kvsep) / kvsep
+	 *
+	 * By the time we reach this code, the gs2-header and initial kvsep have
+	 * already been validated. We start at the beginning of the first kvpair.
+	 */
+
+	while (*pos)
+	{
+		char   *end;
+		char   *sep;
+		char   *key;
+		char   *value;
+
+		/*
+		 * Find the end of this kvpair. Note that input is null-terminated by
+		 * the SASL code, so the strchr() is bounded.
+		 */
+		end = strchr(pos, KVSEP);
+		if (!end)
+			ereport(ERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("malformed OAUTHBEARER message"),
+					 errdetail("Message contains an unterminated key/value pair.")));
+		*end = '\0';
+
+		if (pos == end)
+		{
+			/* Empty kvpair, signifying the end of the list. */
+			*input = pos + 1;
+			return auth;
+		}
+
+		/*
+		 * Find the end of the key name.
+		 *
+		 * TODO further validate the key/value grammar? empty keys, bad chars...
+		 */
+		sep = strchr(pos, '=');
+		if (!sep)
+			ereport(ERROR,
+					(errcode(ERRCODE_PROTOCOL_VIOLATION),
+					 errmsg("malformed OAUTHBEARER message"),
+					 errdetail("Message contains a key without a value.")));
+		*sep = '\0';
+
+		/* Both key and value are now safely terminated. */
+		key = pos;
+		value = sep + 1;
+
+		if (!strcmp(key, AUTH_KEY))
+		{
+			if (auth)
+				ereport(ERROR,
+						(errcode(ERRCODE_PROTOCOL_VIOLATION),
+						 errmsg("malformed OAUTHBEARER message"),
+						 errdetail("Message contains multiple auth values.")));
+
+			auth = value;
+		}
+		else
+		{
+			/*
+			 * The RFC also defines the host and port keys, but they are not
+			 * required for OAUTHBEARER and we do not use them. Also, per
+			 * Sec. 3.1, any key/value pairs we don't recognize must be ignored.
+			 */
+		}
+
+		/* Move to the next pair. */
+		pos = end + 1;
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_PROTOCOL_VIOLATION),
+			 errmsg("malformed OAUTHBEARER message"),
+			 errdetail("Message did not contain a final terminator.")));
+
+	return NULL; /* unreachable */
+}
+
+static void
+generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen)
+{
+	StringInfoData	buf;
+
+	/*
+	 * The admin needs to set an issuer and scope for OAuth to work. There's not
+	 * really a way to hide this from the user, either, because we can't choose
+	 * a "default" issuer, so be honest in the failure message.
+	 *
+	 * TODO: see if there's a better place to fail, earlier than this.
+	 */
+	if (!ctx->issuer || !ctx->scope)
+		ereport(FATAL,
+				(errcode(ERRCODE_INTERNAL_ERROR),
+				 errmsg("OAuth is not properly configured for this user"),
+				 errdetail_log("The issuer and scope parameters must be set in pg_hba.conf.")));
+
+
+	initStringInfo(&buf);
+
+	/*
+	 * TODO: JSON escaping
+	 */
+	appendStringInfo(&buf,
+		"{ "
+			"\"status\": \"invalid_token\", "
+			"\"openid-configuration\": \"%s/.well-known/openid-configuration\","
+			"\"scope\": \"%s\" "
+		"}",
+		ctx->issuer, ctx->scope);
+
+	*output = buf.data;
+	*outputlen = buf.len;
+}
+
+static bool
+validate(Port *port, const char *auth, char **logdetail)
+{
+	static const char * const b64_set = "abcdefghijklmnopqrstuvwxyz"
+										"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+										"0123456789-._~+/";
+
+	const char *token;
+	size_t		span;
+	int			ret;
+
+	/* TODO: handle logdetail when the test framework can check it */
+
+	/*
+	 * Only Bearer tokens are accepted. The ABNF is defined in RFC 6750, Sec.
+	 * 2.1:
+	 *
+	 *      b64token    = 1*( ALPHA / DIGIT /
+	 *                        "-" / "." / "_" / "~" / "+" / "/" ) *"="
+	 *      credentials = "Bearer" 1*SP b64token
+	 *
+	 * The "credentials" construction is what we receive in our auth value.
+	 *
+	 * Since that spec is subordinate to HTTP (i.e. the HTTP Authorization
+	 * header format; RFC 7235 Sec. 2), the "Bearer" scheme string must be
+	 * compared case-insensitively. (This is not mentioned in RFC 6750, but it's
+	 * pointed out in RFC 7628 Sec. 4.)
+	 *
+	 * TODO: handle the Authorization spec, RFC 7235 Sec. 2.1.
+	 */
+	if (strncasecmp(auth, BEARER_SCHEME, strlen(BEARER_SCHEME)))
+		return false;
+
+	/* Pull the bearer token out of the auth value. */
+	token = auth + strlen(BEARER_SCHEME);
+
+	/* Swallow any additional spaces. */
+	while (*token == ' ')
+		token++;
+
+	/*
+	 * Before invoking the validator command, sanity-check the token format to
+	 * avoid any injection attacks later in the chain. Invalid formats are
+	 * technically a protocol violation, but don't reflect any information about
+	 * the sensitive Bearer token back to the client; log at COMMERROR instead.
+	 */
+
+	/* Tokens must not be empty. */
+	if (!*token)
+	{
+		ereport(COMMERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("malformed OAUTHBEARER message"),
+				 errdetail("Bearer token is empty.")));
+		return false;
+	}
+
+	/*
+	 * Make sure the token contains only allowed characters. Tokens may end with
+	 * any number of '=' characters.
+	 */
+	span = strspn(token, b64_set);
+	while (token[span] == '=')
+		span++;
+
+	if (token[span] != '\0')
+	{
+		/*
+		 * This error message could be more helpful by printing the problematic
+		 * character(s), but that'd be a bit like printing a piece of someone's
+		 * password into the logs.
+		 */
+		ereport(COMMERROR,
+				(errcode(ERRCODE_PROTOCOL_VIOLATION),
+				 errmsg("malformed OAUTHBEARER message"),
+				 errdetail("Bearer token is not in the correct format.")));
+		return false;
+	}
+
+	/* Have the validator check the token. */
+	if (!run_validator_command(port, token))
+		return false;
+
+	if (port->hba->oauth_skip_usermap)
+	{
+		/*
+		 * If the validator is our authorization authority, we're done.
+		 * Authentication may or may not have been performed depending on the
+		 * validator implementation; all that matters is that the validator says
+		 * the user can log in with the target role.
+		 */
+		return true;
+	}
+
+	/* Make sure the validator authenticated the user. */
+	if (!port->authn_id)
+	{
+		/* TODO: use logdetail; reduce message duplication */
+		ereport(LOG,
+				(errmsg("OAuth bearer authentication failed for user \"%s\": validator provided no identity",
+						port->user_name)));
+		return false;
+	}
+
+	/* Finally, check the user map. */
+	ret = check_usermap(port->hba->usermap, port->user_name, port->authn_id,
+						false);
+	return (ret == STATUS_OK);
+}
+
+static bool
+run_validator_command(Port *port, const char *token)
+{
+	bool		success = false;
+	int			rc;
+	int			pipefd[2];
+	int			rfd = -1;
+	int			wfd = -1;
+
+	StringInfoData command = { 0 };
+	char	   *p;
+	FILE	   *fh = NULL;
+
+	ssize_t		written;
+	char	   *line = NULL;
+	size_t		size = 0;
+	ssize_t		len;
+
+	Assert(oauth_validator_command);
+
+	if (!oauth_validator_command[0])
+	{
+		ereport(COMMERROR,
+				(errmsg("oauth_validator_command is not set"),
+				 errhint("To allow OAuth authenticated connections, set "
+						 "oauth_validator_command in postgresql.conf.")));
+		return false;
+	}
+
+	/*
+	 * Since popen() is unidirectional, open up a pipe for the other direction.
+	 * Use CLOEXEC to ensure that our write end doesn't accidentally get copied
+	 * into child processes, which would prevent us from closing it cleanly.
+	 *
+	 * XXX this is ugly. We should just read from the child process's stdout,
+	 * but that's a lot more code.
+	 * XXX by bypassing the popen API, we open the potential of process
+	 * deadlock. Clearly document child process requirements (i.e. the child
+	 * MUST read all data off of the pipe before writing anything).
+	 * TODO: port to Windows using _pipe().
+	 */
+	rc = pipe2(pipefd, O_CLOEXEC);
+	if (rc < 0)
+	{
+		ereport(COMMERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not create child pipe: %m")));
+		return false;
+	}
+
+	rfd = pipefd[0];
+	wfd = pipefd[1];
+
+	/* Allow the read pipe be passed to the child. */
+	if (!unset_cloexec(rfd))
+	{
+		/* error message was already logged */
+		goto cleanup;
+	}
+
+	/*
+	 * Construct the command, substituting any recognized %-specifiers:
+	 *
+	 *   %f: the file descriptor of the input pipe
+	 *   %r: the role that the client wants to assume (port->user_name)
+	 *   %%: a literal '%'
+	 */
+	initStringInfo(&command);
+
+	for (p = oauth_validator_command; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'f':
+					appendStringInfo(&command, "%d", rfd);
+					p++;
+					break;
+				case 'r':
+					/*
+					 * TODO: decide how this string should be escaped. The role
+					 * is controlled by the client, so if we don't escape it,
+					 * command injections are inevitable.
+					 *
+					 * This is probably an indication that the role name needs
+					 * to be communicated to the validator process in some other
+					 * way. For this proof of concept, just be incredibly strict
+					 * about the characters that are allowed in user names.
+					 */
+					if (!username_ok_for_shell(port->user_name))
+						goto cleanup;
+
+					appendStringInfoString(&command, port->user_name);
+					p++;
+					break;
+				case '%':
+					appendStringInfoChar(&command, '%');
+					p++;
+					break;
+				default:
+					appendStringInfoChar(&command, p[0]);
+			}
+		}
+		else
+			appendStringInfoChar(&command, p[0]);
+	}
+
+	/* Execute the command. */
+	fh = OpenPipeStream(command.data, "re");
+	/* TODO: handle failures */
+
+	/* We don't need the read end of the pipe anymore. */
+	close(rfd);
+	rfd = -1;
+
+	/* Give the command the token to validate. */
+	written = write(wfd, token, strlen(token));
+	if (written != strlen(token))
+	{
+		/* TODO must loop for short writes, EINTR et al */
+		ereport(COMMERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not write token to child pipe: %m")));
+		goto cleanup;
+	}
+
+	close(wfd);
+	wfd = -1;
+
+	/*
+	 * Read the command's response.
+	 *
+	 * TODO: getline() is probably too new to use, unfortunately.
+	 * TODO: loop over all lines
+	 */
+	if ((len = getline(&line, &size, fh)) >= 0)
+	{
+		/* TODO: fail if the authn_id doesn't end with a newline */
+		if (len > 0)
+			line[len - 1] = '\0';
+
+		set_authn_id(port, line);
+	}
+	else if (ferror(fh))
+	{
+		ereport(COMMERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not read from command \"%s\": %m",
+						command.data)));
+		goto cleanup;
+	}
+
+	/* Make sure the command exits cleanly. */
+	if (!check_exit(&fh, command.data))
+	{
+		/* error message already logged */
+		goto cleanup;
+	}
+
+	/* Done. */
+	success = true;
+
+cleanup:
+	if (line)
+		free(line);
+
+	/*
+	 * In the successful case, the pipe fds are already closed. For the error
+	 * case, always close out the pipe before waiting for the command, to
+	 * prevent deadlock.
+	 */
+	if (rfd >= 0)
+		close(rfd);
+	if (wfd >= 0)
+		close(wfd);
+
+	if (fh)
+	{
+		Assert(!success);
+		check_exit(&fh, command.data);
+	}
+
+	if (command.data)
+		pfree(command.data);
+
+	return success;
+}
+
+static bool
+check_exit(FILE **fh, const char *command)
+{
+	int rc;
+
+	rc = ClosePipeStream(*fh);
+	*fh = NULL;
+
+	if (rc == -1)
+	{
+		/* pclose() itself failed. */
+		ereport(COMMERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not close pipe to command \"%s\": %m",
+						command)));
+	}
+	else if (rc != 0)
+	{
+		char *reason = wait_result_to_str(rc);
+
+		ereport(COMMERROR,
+				(errmsg("failed to execute command \"%s\": %s",
+						command, reason)));
+
+		pfree(reason);
+	}
+
+	return (rc == 0);
+}
+
+static bool
+unset_cloexec(int fd)
+{
+	int			flags;
+	int			rc;
+
+	flags = fcntl(fd, F_GETFD);
+	if (flags == -1)
+	{
+		ereport(COMMERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not get fd flags for child pipe: %m")));
+		return false;
+	}
+
+	rc = fcntl(fd, F_SETFD, flags & ~FD_CLOEXEC);
+	if (rc < 0)
+	{
+		ereport(COMMERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not unset FD_CLOEXEC for child pipe: %m")));
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * XXX This should go away eventually and be replaced with either a proper
+ * escape or a different strategy for communication with the validator command.
+ */
+static bool
+username_ok_for_shell(const char *username)
+{
+	/* This set is borrowed from fe_utils' appendShellStringNoError(). */
+	static const char * const allowed = "abcdefghijklmnopqrstuvwxyz"
+										"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+										"0123456789-_./:";
+	size_t	span;
+
+	Assert(username && username[0]); /* should have already been checked */
+
+	span = strspn(username, allowed);
+	if (username[span] != '\0')
+	{
+		ereport(COMMERROR,
+				(errmsg("PostgreSQL user name contains unsafe characters and cannot be passed to the OAuth validator")));
+		return false;
+	}
+
+	return true;
+}
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index db3ca75a60..9e4482dc27 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -118,6 +118,8 @@ const pg_be_sasl_mech pg_be_scram_mech = {
 	scram_get_mechanisms,
 	scram_init,
 	scram_exchange,
+
+	PG_MAX_SASL_MESSAGE_LENGTH,
 };
 
 /*
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index e20740a7c5..354c7b0fc8 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -29,6 +29,7 @@
 #include "libpq/auth.h"
 #include "libpq/crypt.h"
 #include "libpq/libpq.h"
+#include "libpq/oauth.h"
 #include "libpq/pqformat.h"
 #include "libpq/sasl.h"
 #include "libpq/scram.h"
@@ -49,7 +50,6 @@ static void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
 							int extralen);
 static void auth_failed(Port *port, int status, char *logdetail);
 static char *recv_password_packet(Port *port);
-static void set_authn_id(Port *port, const char *id);
 
 /*----------------------------------------------------------------
  * SASL common authentication
@@ -215,29 +215,12 @@ static int	CheckRADIUSAuth(Port *port);
 static int	PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd);
 
 
-/*
- * Maximum accepted size of GSS and SSPI authentication tokens.
- * We also use this as a limit on ordinary password packet lengths.
- *
- * Kerberos tickets are usually quite small, but the TGTs issued by Windows
- * domain controllers include an authorization field known as the Privilege
- * Attribute Certificate (PAC), which contains the user's Windows permissions
- * (group memberships etc.). The PAC is copied into all tickets obtained on
- * the basis of this TGT (even those issued by Unix realms which the Windows
- * realm trusts), and can be several kB in size. The maximum token size
- * accepted by Windows systems is determined by the MaxAuthToken Windows
- * registry setting. Microsoft recommends that it is not set higher than
- * 65535 bytes, so that seems like a reasonable limit for us as well.
+/*----------------------------------------------------------------
+ * OAuth v2 Bearer Authentication
+ *----------------------------------------------------------------
  */
-#define PG_MAX_AUTH_TOKEN_LENGTH	65535
+static int	CheckOAuthBearer(Port *port);
 
-/*
- * Maximum accepted size of SASL messages.
- *
- * The messages that the server or libpq generate are much smaller than this,
- * but have some headroom.
- */
-#define PG_MAX_SASL_MESSAGE_LENGTH	1024
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -327,6 +310,9 @@ auth_failed(Port *port, int status, char *logdetail)
 		case uaRADIUS:
 			errstr = gettext_noop("RADIUS authentication failed for user \"%s\"");
 			break;
+		case uaOAuth:
+			errstr = gettext_noop("OAuth bearer authentication failed for user \"%s\"");
+			break;
 		default:
 			errstr = gettext_noop("authentication failed for user \"%s\": invalid authentication method");
 			break;
@@ -361,7 +347,7 @@ auth_failed(Port *port, int status, char *logdetail)
  * lifetime of the Port, so it is safe to pass a string that is managed by an
  * external library.
  */
-static void
+void
 set_authn_id(Port *port, const char *id)
 {
 	Assert(id);
@@ -646,6 +632,9 @@ ClientAuthentication(Port *port)
 		case uaTrust:
 			status = STATUS_OK;
 			break;
+		case uaOAuth:
+			status = CheckOAuthBearer(port);
+			break;
 	}
 
 	if ((status == STATUS_OK && port->hba->clientcert == clientCertFull)
@@ -973,7 +962,7 @@ SASL_exchange(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass,
 
 		/* Get the actual SASL message */
 		initStringInfo(&buf);
-		if (pq_getmessage(&buf, PG_MAX_SASL_MESSAGE_LENGTH))
+		if (pq_getmessage(&buf, mech->max_message_length))
 		{
 			/* EOF - pq_getmessage already logged error */
 			pfree(buf.data);
@@ -3495,3 +3484,9 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		}
 	}							/* while (true) */
 }
+
+static int
+CheckOAuthBearer(Port *port)
+{
+	return SASL_exchange(&pg_be_oauth_mech, port, NULL, NULL);
+}
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 3be8778d21..98147700dd 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -134,7 +134,8 @@ static const char *const UserAuthName[] =
 	"ldap",
 	"cert",
 	"radius",
-	"peer"
+	"peer",
+	"oauth",
 };
 
 
@@ -1399,6 +1400,8 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 #endif
 	else if (strcmp(token->string, "radius") == 0)
 		parsedline->auth_method = uaRADIUS;
+	else if (strcmp(token->string, "oauth") == 0)
+		parsedline->auth_method = uaOAuth;
 	else
 	{
 		ereport(elevel,
@@ -1713,8 +1716,9 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 			hbaline->auth_method != uaPeer &&
 			hbaline->auth_method != uaGSS &&
 			hbaline->auth_method != uaSSPI &&
+			hbaline->auth_method != uaOAuth &&
 			hbaline->auth_method != uaCert)
-			INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, and cert"));
+			INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, oauth, and cert"));
 		hbaline->usermap = pstrdup(val);
 	}
 	else if (strcmp(name, "clientcert") == 0)
@@ -2098,6 +2102,27 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 		hbaline->radiusidentifiers = parsed_identifiers;
 		hbaline->radiusidentifiers_s = pstrdup(val);
 	}
+	else if (strcmp(name, "issuer") == 0)
+	{
+		if (hbaline->auth_method != uaOAuth)
+			INVALID_AUTH_OPTION("issuer", gettext_noop("oauth"));
+		hbaline->oauth_issuer = pstrdup(val);
+	}
+	else if (strcmp(name, "scope") == 0)
+	{
+		if (hbaline->auth_method != uaOAuth)
+			INVALID_AUTH_OPTION("scope", gettext_noop("oauth"));
+		hbaline->oauth_scope = pstrdup(val);
+	}
+	else if (strcmp(name, "trust_validator_authz") == 0)
+	{
+		if (hbaline->auth_method != uaOAuth)
+			INVALID_AUTH_OPTION("trust_validator_authz", gettext_noop("oauth"));
+		if (strcmp(val, "1") == 0)
+			hbaline->oauth_skip_usermap = true;
+		else
+			hbaline->oauth_skip_usermap = false;
+	}
 	else
 	{
 		ereport(elevel,
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 68b62d523d..1ef6b3c41e 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -56,6 +56,7 @@
 #include "libpq/auth.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
+#include "libpq/oauth.h"
 #include "miscadmin.h"
 #include "optimizer/cost.h"
 #include "optimizer/geqo.h"
@@ -4587,6 +4588,17 @@ static struct config_string ConfigureNamesString[] =
 		check_backtrace_functions, assign_backtrace_functions, NULL
 	},
 
+	{
+		{"oauth_validator_command", PGC_SIGHUP, CONN_AUTH_AUTH,
+			gettext_noop("Command to validate OAuth v2 bearer tokens."),
+			NULL,
+			GUC_SUPERUSER_ONLY
+		},
+		&oauth_validator_command,
+		"",
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h
index 3610fae3ff..785cc5d16f 100644
--- a/src/include/libpq/auth.h
+++ b/src/include/libpq/auth.h
@@ -21,6 +21,7 @@ extern bool pg_krb_caseins_users;
 extern char *pg_krb_realm;
 
 extern void ClientAuthentication(Port *port);
+extern void set_authn_id(Port *port, const char *id);
 
 /* Hook for plugins to get control in ClientAuthentication() */
 typedef void (*ClientAuthentication_hook_type) (Port *, int);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8d9f3821b1..441dd5623e 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -38,8 +38,9 @@ typedef enum UserAuth
 	uaLDAP,
 	uaCert,
 	uaRADIUS,
-	uaPeer
-#define USER_AUTH_LAST uaPeer	/* Must be last value of this enum */
+	uaPeer,
+	uaOAuth
+#define USER_AUTH_LAST uaOAuth	/* Must be last value of this enum */
 } UserAuth;
 
 /*
@@ -120,6 +121,9 @@ typedef struct HbaLine
 	char	   *radiusidentifiers_s;
 	List	   *radiusports;
 	char	   *radiusports_s;
+	char	   *oauth_issuer;
+	char	   *oauth_scope;
+	bool		oauth_skip_usermap;
 } HbaLine;
 
 typedef struct IdentLine
diff --git a/src/include/libpq/oauth.h b/src/include/libpq/oauth.h
new file mode 100644
index 0000000000..870e426af1
--- /dev/null
+++ b/src/include/libpq/oauth.h
@@ -0,0 +1,24 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth.h
+ *	  Interface to libpq/auth-oauth.c
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/libpq/oauth.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_OAUTH_H
+#define PG_OAUTH_H
+
+#include "libpq/libpq-be.h"
+#include "libpq/sasl.h"
+
+extern char *oauth_validator_command;
+
+/* Implementation */
+extern const pg_be_sasl_mech pg_be_oauth_mech;
+
+#endif /* PG_OAUTH_H */
diff --git a/src/include/libpq/sasl.h b/src/include/libpq/sasl.h
index 8c9c9983d4..f1341d0c54 100644
--- a/src/include/libpq/sasl.h
+++ b/src/include/libpq/sasl.h
@@ -19,6 +19,30 @@
 #define SASL_EXCHANGE_SUCCESS		1
 #define SASL_EXCHANGE_FAILURE		2
 
+/*
+ * Maximum accepted size of GSS and SSPI authentication tokens.
+ * We also use this as a limit on ordinary password packet lengths.
+ *
+ * Kerberos tickets are usually quite small, but the TGTs issued by Windows
+ * domain controllers include an authorization field known as the Privilege
+ * Attribute Certificate (PAC), which contains the user's Windows permissions
+ * (group memberships etc.). The PAC is copied into all tickets obtained on
+ * the basis of this TGT (even those issued by Unix realms which the Windows
+ * realm trusts), and can be several kB in size. The maximum token size
+ * accepted by Windows systems is determined by the MaxAuthToken Windows
+ * registry setting. Microsoft recommends that it is not set higher than
+ * 65535 bytes, so that seems like a reasonable limit for us as well.
+ */
+#define PG_MAX_AUTH_TOKEN_LENGTH	65535
+
+/*
+ * Maximum accepted size of SASL messages.
+ *
+ * The messages that the server or libpq generate are much smaller than this,
+ * but have some headroom.
+ */
+#define PG_MAX_SASL_MESSAGE_LENGTH	1024
+
 /* Backend mechanism API */
 typedef void  (*pg_be_sasl_mechanism_func)(Port *, StringInfo);
 typedef void *(*pg_be_sasl_init_func)(Port *, const char *, const char *);
@@ -29,6 +53,8 @@ typedef struct
 	pg_be_sasl_mechanism_func	get_mechanisms;
 	pg_be_sasl_init_func		init;
 	pg_be_sasl_exchange_func	exchange;
+
+	int							max_message_length;
 } pg_be_sasl_mech;
 
 #endif /* PG_SASL_H */
-- 
2.25.1

