v31-0005-v30-review-comments.patch

application/octet-stream

Filename: v31-0005-v30-review-comments.patch
Type: application/octet-stream
Part: 4
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v31-0005
Subject: v30-review-comments
File+
doc/src/sgml/client-auth.sgml 97 1
doc/src/sgml/config.sgml 17 0
doc/src/sgml/installation.sgml 29 0
doc/src/sgml/libpq.sgml 10 4
doc/src/sgml/oauth-validators.sgml 34 2
doc/src/sgml/postgres.sgml 1 1
doc/src/sgml/regress.sgml 10 0
src/backend/libpq/auth-oauth.c 102 70
src/backend/libpq/hba.c 26 0
src/interfaces/libpq/fe-auth-oauth.c 4 1
src/test/modules/oauth_validator/t/001_server.pl 8 1
src/test/modules/oauth_validator/validator.c 5 2
From e77e769792a3280e759aefc162fbcaa790b98123 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <daniel@yesql.se>
Date: Fri, 18 Oct 2024 12:40:26 +0200
Subject: [PATCH v31 5/5] v30-review-comments

---
 doc/src/sgml/client-auth.sgml                 |  98 +++++++++-
 doc/src/sgml/config.sgml                      |  17 ++
 doc/src/sgml/installation.sgml                |  29 +++
 doc/src/sgml/libpq.sgml                       |  14 +-
 doc/src/sgml/oauth-validators.sgml            |  36 +++-
 doc/src/sgml/postgres.sgml                    |   2 +-
 doc/src/sgml/regress.sgml                     |  10 +
 src/backend/libpq/auth-oauth.c                | 172 +++++++++++-------
 src/backend/libpq/hba.c                       |  26 +++
 src/interfaces/libpq/fe-auth-oauth.c          |   5 +-
 .../modules/oauth_validator/t/001_server.pl   |   9 +-
 src/test/modules/oauth_validator/validator.c  |   7 +-
 12 files changed, 343 insertions(+), 82 deletions(-)

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index fb78b6c886..8d351c2089 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -2336,7 +2336,103 @@ host ... radius radiusservers="server1,server2" radiussecrets="""secret one"",""
    </indexterm>
 
    <para>
-    TODO
+    OAuth 2.0 is an industry-standard framework, defined in
+    <ulink url="https://datatracker.ietf.org/doc/html/rfc6749">RFC 6749</ulink>,
+    to enable third-party applications to obtain limited access to a protected
+    resource.
+
+    OAuth support has to be enabled when <productname>PostgreSQL</productname>
+    is built, see <xref linkend="installation"/> for more information.
+
+    <itemizedlist>
+     <listitem>
+      <para>
+       Resource owner: The user or system who owns protected resources and can
+       grant access to them.
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       Client: The system which accesses to the protected resources using access
+       tokens.  Applications using libpq are the clients in connecting to a
+       <productname>PostgreSQL</productname> cluster.
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       Authentication server: The system which recieves requests from, and
+       issues access tokens to, the client upon successful authentication by
+       the resource owner.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Resource server: The system which owns the protected resources and can
+       grant access to them. The <productname>PostgreSQL</productname> cluster
+       being connected to is the resource server.
+      </para>
+     </listitem>
+
+    </itemizedlist>
+   </para>
+
+   <para>
+    <productname>PostgreSQL</productname> supports bearer tokens, defined in
+    <ulink url="https://datatracker.ietf.org/doc/html/rfc6750">RFC 6750</ulink>
+    which are a type of access token used with OAuth 2.0 where the token is an
+    opaque string.  The format of the access token is implementation specific
+    and is chosen by each authentication server.
+   </para>
+
+   <para>
+    The following configuration options are supported for OAuth:
+    <variablelist>
+     <varlistentry>
+      <term><literal>issuer</literal></term>
+      <listitem>
+       <para>
+        The <acronym>URL</acronym> of the OAuth issuing party, which the client
+        must contact to receive a bearer token.  This parameter is required.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><literal>scope</literal></term>
+      <listitem>
+       <para>
+        The OAuth scope required for the server to authenticate and/or authorize
+        the user. The value of the scope is dependent on the OAuth validation
+        module used (see <xref linkend="oauth-validators" /> for more information
+        on validators).  This parameter is required.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><literal>map</literal></term>
+      <listitem>
+       <para>
+        Allows for mapping between OAuth identity provider and database user
+        names.  See <xref linkend="auth-username-maps"/> for details.  If a
+        map is not specified the user name returned from the OAuth validator
+        must match the role being requested.  This parameter is optional.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><literal>trust_validator_authz</literal></term>
+      <listitem>
+       <para>
+        When set to <literal>1</literal> standard user mapping is skipped. If
+        the OAuth token is validated the user can connect under its desired
+        role.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
    </para>
   </sect1>
 
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f089a8ff4c..73bf21c599 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1214,6 +1214,23 @@ include_dir 'conf.d'
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="guc-oauth-validator-library" xreflabel="oauth_validator_library">
+      <term><varname>oauth_validator_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>oauth_validator_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for validating OAuth connection tokens. If set to
+        an empty string (the default), OAuth connections will be refused. For
+        more information on implementing OAuth validators see
+        <xref linkend="oauth-validators" />. This parameter can only be set in
+        the <filename>postgresql.conf</filename> file.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
      </sect2>
 
diff --git a/doc/src/sgml/installation.sgml b/doc/src/sgml/installation.sgml
index 3a491b5989..9a76aac08b 100644
--- a/doc/src/sgml/installation.sgml
+++ b/doc/src/sgml/installation.sgml
@@ -1064,6 +1064,20 @@ build-postgresql:
        </listitem>
       </varlistentry>
 
+      <varlistentry id="configure-option-with-oauth">
+       <term><option>--with-oauth=<replaceable>LIBRARY</replaceable></option></term>
+       <listitem>
+        <para>
+         Build with OAuth authentication and authorization support.  The only
+         <replaceable>LIBRARY</replaceable> supported is <option>curl</option>.
+         This requires the <productname>curl</productname> package to be
+         installed.  Building with this will check for the required header files
+         and libraries to make sure that your <productname>curl</productname>
+         installation is sufficient before proceeding.
+        </para>
+       </listitem>
+      </varlistentry>
+
       <varlistentry id="configure-option-with-systemd">
        <term><option>--with-systemd</option></term>
        <listitem>
@@ -2508,6 +2522,21 @@ ninja install
       </listitem>
      </varlistentry>
 
+     <varlistentry id="configure-with-oauth">
+      <term><option>-Doauth={ auto | <replaceable>LIBRARY</replaceable> | disabled }</option></term>
+      <listitem>
+       <para>
+        Build with OAuth authentication and authorization support.  The only
+        <replaceable>LIBRARY</replaceable> supported is <option>curl</option>.
+        This requires the <productname>curl</productname> package to be
+        installed.  Building with this will check for the required header files
+        and libraries to make sure that your <productname>curl</productname>
+        installation is sufficient before proceeding. The default for this
+        option is auto.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="configure-with-systemd-meson">
       <term><option>-Dsystemd={ auto | enabled | disabled }</option></term>
       <listitem>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index ffec0431e3..86fd146af2 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2341,7 +2341,7 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       <term><literal>oauth_client_id</literal></term>
       <listitem>
        <para>
-        TODO
+        The client identifier as issued by the authorization server.
        </para>
       </listitem>
      </varlistentry>
@@ -2350,7 +2350,7 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       <term><literal>oauth_client_secret</literal></term>
       <listitem>
        <para>
-        TODO
+        The client password.
        </para>
       </listitem>
      </varlistentry>
@@ -2368,7 +2368,8 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       <term><literal>oauth_scope</literal></term>
       <listitem>
        <para>
-        TODO
+        The scope of the access request sent to the authorization server.
+        This parameter is optional.
        </para>
       </listitem>
      </varlistentry>
@@ -10017,6 +10018,11 @@ void PQinitSSL(int do_ssl);
 void PQsetAuthDataHook(PQauthDataHook_type hook);
 </synopsis>
       </para>
+
+      <para>
+       If <replaceable>hook</replaceable> is set to a null pointer instead of
+       a function pointer, the default hook will be installed.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -10025,7 +10031,7 @@ void PQsetAuthDataHook(PQauthDataHook_type hook);
 
      <listitem>
       <para>
-       TODO
+       Retrieves the current value of <literal>PGauthDataHook</literal>.
 <synopsis>
 PQauthDataHook_type PQgetAuthDataHook(void);
 </synopsis>
diff --git a/doc/src/sgml/oauth-validators.sgml b/doc/src/sgml/oauth-validators.sgml
index c9914519fc..4615159a9f 100644
--- a/doc/src/sgml/oauth-validators.sgml
+++ b/doc/src/sgml/oauth-validators.sgml
@@ -1,13 +1,13 @@
 <!-- doc/src/sgml/oauth-validators.sgml -->
 
 <chapter id="oauth-validators">
- <title>Implementing OAuth Validator Modules</title>
+ <title>OAuth Validator Modules</title>
  <indexterm zone="oauth-validators">
   <primary>OAuth Validators</primary>
  </indexterm>
  <para>
   <productname>PostgreSQL</productname> provides infrastructure for creating
-  custom modules to perform server-side validation of OAuth tokens.
+  custom modules to perform server-side validation of OAuth bearer tokens.
  </para>
  <para>
   OAuth validation modules must at least consist of an initialization function
@@ -74,9 +74,41 @@ typedef void (*ValidatorStartupCB) (ValidatorModuleState *state);
   <sect2 id="oauth-validator-callback-validate">
    <title>Validate Callback</title>
    <para>
+    The <function>validate_cb</function> callback is executed during the OAuth
+    exchange when a user attempts to authenticate using OAuth. The token is
+    parsed to ensure being well-formed syntactically, but no semantical check
+    has been performed. Any state set in previous calls will be available in
+    <structfield>state->privata_data</structfield>.
+
 <programlisting>
 typedef ValidatorModuleResult *(*ValidatorValidateCB) (ValidatorModuleState *state, const char *token, const char *role);
 </programlisting>
+
+    <replaceable>token</replaceable> will contain the bearer token to validate,
+    <replaceable>role</replaceable> will contain the role the user request to
+    log in as. The callback must return a <literal>ValidatorModuleResult</literal>
+    struct which is defined as below:
+
+<programlisting>
+typedef struct ValidatorModuleResult
+{
+    bool        authorized;
+    char       *authn_id;
+} ValidatorModuleResult;
+</programlisting>
+
+    If <structfield>authorized</structfield> is set to <literal>true</literal>
+    the bearer token is defined to be valid.
+    To authenticate the user, the authenticated user name shall be returned in
+    the <structfield>authn_id</structfield> field. When authenticating against
+    a HBA rule with <literal>trust_validator_authz</literal> turned on the
+    <structfield>authn_id</structfield> user name must exactly match the role
+    expected to login as.
+   </para>
+   <para>
+    The caller assumes ownership of the returned memory allocation, the
+    validator module should not in any way access the memory after it has been
+    returned.
    </para>
   </sect2>
 
diff --git a/doc/src/sgml/postgres.sgml b/doc/src/sgml/postgres.sgml
index 321d4590a3..af476c82fc 100644
--- a/doc/src/sgml/postgres.sgml
+++ b/doc/src/sgml/postgres.sgml
@@ -229,6 +229,7 @@ break is not needed in a wider output rendering.
   &logicaldecoding;
   &replication-origins;
   &archive-modules;
+  &oauth-validators;
 
  </part>
 
@@ -264,7 +265,6 @@ break is not needed in a wider output rendering.
   &bki;
   &planstats;
   &backup-manifest;
-  &oauth-validators;
 
  </part>
 
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index f4cef9e80f..ae4732df65 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -336,6 +336,16 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><literal>oauth</literal></term>
+     <listitem>
+      <para>
+       Runs the test suite under <filename>src/test/modules/oauth_validator</filename>.
+       This opens TCP/IP listen sockets for a test-server running HTTPS.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
 
    Tests for features that are not supported by the current build
diff --git a/src/backend/libpq/auth-oauth.c b/src/backend/libpq/auth-oauth.c
index dea973247a..cfa5769b10 100644
--- a/src/backend/libpq/auth-oauth.c
+++ b/src/backend/libpq/auth-oauth.c
@@ -90,10 +90,10 @@ oauth_init(Port *port, const char *selected_mech, const char *shadow_pass)
 {
 	struct oauth_ctx *ctx;
 
-	if (strcmp(selected_mech, OAUTHBEARER_NAME))
+	if (strcmp(selected_mech, OAUTHBEARER_NAME) != 0)
 		ereport(ERROR,
-				(errcode(ERRCODE_PROTOCOL_VIOLATION),
-				 errmsg("client selected an invalid SASL authentication mechanism")));
+				errcode(ERRCODE_PROTOCOL_VIOLATION),
+				errmsg("client selected an invalid SASL authentication mechanism"));
 
 	ctx = palloc0(sizeof(*ctx));
 
@@ -142,14 +142,14 @@ oauth_exchange(void *opaq, const char *input, int inputlen,
 	 */
 	if (inputlen == 0)
 		ereport(ERROR,
-				(errcode(ERRCODE_PROTOCOL_VIOLATION),
-				 errmsg("malformed OAUTHBEARER message"),
-				 errdetail("The message is empty.")));
+				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.")));
+				errcode(ERRCODE_PROTOCOL_VIOLATION),
+				errmsg("malformed OAUTHBEARER message"),
+				errdetail("Message length does not match input length."));
 
 	switch (ctx->state)
 	{
@@ -165,9 +165,9 @@ oauth_exchange(void *opaq, const char *input, int inputlen,
 			 */
 			if (inputlen != 1 || *input != KVSEP)
 				ereport(ERROR,
-						(errcode(ERRCODE_PROTOCOL_VIOLATION),
-						 errmsg("malformed OAUTHBEARER message"),
-						 errdetail("Client did not send a kvsep response.")));
+						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;
@@ -193,9 +193,9 @@ oauth_exchange(void *opaq, const char *input, int inputlen,
 	{
 		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.")));
+					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 */
@@ -203,19 +203,19 @@ oauth_exchange(void *opaq, const char *input, int inputlen,
 			p++;
 			if (*p != ',')
 				ereport(ERROR,
-						(errcode(ERRCODE_PROTOCOL_VIOLATION),
-						 errmsg("malformed OAUTHBEARER message"),
-						 errdetail("Comma expected, but found character \"%s\".",
-								   sanitize_char(*p))));
+						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))));
+					errcode(ERRCODE_PROTOCOL_VIOLATION),
+					errmsg("malformed OAUTHBEARER message"),
+					errdetail("Unexpected channel-binding flag \"%s\".",
+							  sanitize_char(cbind_flag)));
 	}
 
 	/*
@@ -223,38 +223,38 @@ oauth_exchange(void *opaq, const char *input, int inputlen,
 	 */
 	if (*p == 'a')
 		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("client uses authorization identity, but it is not supported")));
+				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))));
+				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))));
+				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.")));
+				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.")));
+				errcode(ERRCODE_PROTOCOL_VIOLATION),
+				errmsg("malformed OAUTHBEARER message"),
+				errdetail("Message contains additional data after the final terminator."));
 
 	if (!validate(ctx->port, auth))
 	{
@@ -308,16 +308,16 @@ validate_kvpair(const char *key, const char *val)
 
 	if (!key[0])
 		ereport(ERROR,
-				(errcode(ERRCODE_PROTOCOL_VIOLATION),
-				 errmsg("malformed OAUTHBEARER message"),
-				 errdetail("Message contains an empty key name.")));
+				errcode(ERRCODE_PROTOCOL_VIOLATION),
+				errmsg("malformed OAUTHBEARER message"),
+				errdetail("Message contains an empty key name."));
 
 	span = strspn(key, key_allowed_set);
 	if (key[span] != '\0')
 		ereport(ERROR,
-				(errcode(ERRCODE_PROTOCOL_VIOLATION),
-				 errmsg("malformed OAUTHBEARER message"),
-				 errdetail("Message contains an invalid key name.")));
+				errcode(ERRCODE_PROTOCOL_VIOLATION),
+				errmsg("malformed OAUTHBEARER message"),
+				errdetail("Message contains an invalid key name."));
 
 	/*-----
 	 * From Sec 3.1:
@@ -341,9 +341,9 @@ validate_kvpair(const char *key, const char *val)
 
 			default:
 				ereport(ERROR,
-						(errcode(ERRCODE_PROTOCOL_VIOLATION),
-						 errmsg("malformed OAUTHBEARER message"),
-						 errdetail("Message contains an invalid value.")));
+						errcode(ERRCODE_PROTOCOL_VIOLATION),
+						errmsg("malformed OAUTHBEARER message"),
+						errdetail("Message contains an invalid value."));
 		}
 	}
 }
@@ -386,9 +386,9 @@ parse_kvpairs_for_auth(char **input)
 		end = strchr(pos, KVSEP);
 		if (!end)
 			ereport(ERROR,
-					(errcode(ERRCODE_PROTOCOL_VIOLATION),
-					 errmsg("malformed OAUTHBEARER message"),
-					 errdetail("Message contains an unterminated key/value pair.")));
+					errcode(ERRCODE_PROTOCOL_VIOLATION),
+					errmsg("malformed OAUTHBEARER message"),
+					errdetail("Message contains an unterminated key/value pair."));
 		*end = '\0';
 
 		if (pos == end)
@@ -404,9 +404,9 @@ parse_kvpairs_for_auth(char **input)
 		sep = strchr(pos, '=');
 		if (!sep)
 			ereport(ERROR,
-					(errcode(ERRCODE_PROTOCOL_VIOLATION),
-					 errmsg("malformed OAUTHBEARER message"),
-					 errdetail("Message contains a key without a value.")));
+					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. */
@@ -418,9 +418,9 @@ parse_kvpairs_for_auth(char **input)
 		{
 			if (auth)
 				ereport(ERROR,
-						(errcode(ERRCODE_PROTOCOL_VIOLATION),
-						 errmsg("malformed OAUTHBEARER message"),
-						 errdetail("Message contains multiple auth values.")));
+						errcode(ERRCODE_PROTOCOL_VIOLATION),
+						errmsg("malformed OAUTHBEARER message"),
+						errdetail("Message contains multiple auth values."));
 
 			auth = value;
 		}
@@ -438,9 +438,9 @@ parse_kvpairs_for_auth(char **input)
 	}
 
 	ereport(ERROR,
-			(errcode(ERRCODE_PROTOCOL_VIOLATION),
-			 errmsg("malformed OAUTHBEARER message"),
-			 errdetail("Message did not contain a final terminator.")));
+			errcode(ERRCODE_PROTOCOL_VIOLATION),
+			errmsg("malformed OAUTHBEARER message"),
+			errdetail("Message did not contain a final terminator."));
 
 	pg_unreachable();
 	return NULL;
@@ -461,9 +461,9 @@ generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen)
 	 */
 	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.")));
+				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."));
 
 	/*------
 	 * Build the .well-known URI based on our issuer.
@@ -603,6 +603,7 @@ validate(Port *port, const char *auth)
 	int			map_status;
 	ValidatorModuleResult *ret;
 	const char *token;
+	bool		status;
 
 	/* Ensure that we have a correct token to validate */
 	if (!(token = validate_token_format(auth)))
@@ -613,7 +614,10 @@ validate(Port *port, const char *auth)
 										  token, port->user_name);
 
 	if (!ret->authorized)
-		return false;
+	{
+		status = false;
+		goto cleanup;
+	}
 
 	if (ret->authn_id)
 		set_authn_id(port, ret->authn_id);
@@ -626,7 +630,8 @@ validate(Port *port, const char *auth)
 		 * validator implementation; all that matters is that the validator
 		 * says the user can log in with the target role.
 		 */
-		return true;
+		status = true;
+		goto cleanup;
 	}
 
 	/* Make sure the validator authenticated the user. */
@@ -642,9 +647,31 @@ validate(Port *port, const char *auth)
 	/* Finally, check the user map. */
 	map_status = check_usermap(port->hba->usermap, port->user_name,
 							   MyClientConnectionInfo.authn_id, false);
-	return (map_status == STATUS_OK);
+	status = (map_status == STATUS_OK);
+
+cleanup:
+	/*
+	 * Clear and free the validation result from the validator module once
+	 * we're done with it to avoid accidental re-use.
+	 */
+	if (ret->authn_id != NULL)
+	{
+		explicit_bzero(ret->authn_id, strlen(ret->authn_id));
+		pfree(ret->authn_id);
+	}
+	pfree(ret);
+
+	return status;
 }
 
+/*
+ * load_validator_library
+ *
+ * Load the configured validator library in order to perform token validation.
+ * There is no built-in fallback since validation is implementation specific. If
+ * no validator library is configured, or of it fails to load, then error out
+ * since token validation won't be possible.
+ */
 static void
 load_validator_library(void)
 {
@@ -652,20 +679,25 @@ load_validator_library(void)
 
 	if (OAuthValidatorLibrary[0] == '\0')
 		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("oauth_validator_library is not set")));
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("oauth_validator_library is not set"));
 
 	validator_init = (OAuthValidatorModuleInit)
 		load_external_function(OAuthValidatorLibrary,
 							   "_PG_oauth_validator_module_init", false, NULL);
 
+	/*
+	 * The validator init function is required since it will set the callbacks
+	 * for the validator library.
+	 */
 	if (validator_init == NULL)
 		ereport(ERROR,
-				(errmsg("%s module \"%s\" have to define the symbol %s",
-						"OAuth validator", OAuthValidatorLibrary, "_PG_oauth_validator_module_init")));
+				errmsg("%s modules \"%s\" have to define the symbol %s",
+					   "OAuth validator", OAuthValidatorLibrary, "_PG_oauth_validator_module_init"));
 
 	ValidatorCallbacks = (*validator_init) ();
 
+	/* Allocate memory for validator library private state data */
 	validator_module_state = (ValidatorModuleState *) palloc0(sizeof(ValidatorModuleState));
 	if (ValidatorCallbacks->startup_cb != NULL)
 		ValidatorCallbacks->startup_cb(validator_module_state);
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 735fd05373..dcb1558ad3 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -2042,6 +2042,32 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 		parsedline->clientcert = clientCertFull;
 	}
 
+	/*
+	 * Enforce proper configuration of OAuth authentication.
+	 */
+	if (parsedline->auth_method == uaOAuth)
+	{
+		MANDATORY_AUTH_ARG(parsedline->oauth_scope, "scope", "oauth");
+		MANDATORY_AUTH_ARG(parsedline->oauth_issuer, "issuer", "oauth");
+
+		/*
+		 * Supplying a usermap combined with the option to skip usermapping
+		 * is nonsensical and indicates a configuration error.
+		 */
+		if (parsedline->oauth_skip_usermap && parsedline->usermap != NULL)
+		{
+			ereport(elevel,
+					errcode(ERRCODE_CONFIG_FILE_ERROR),
+					/* translator: strings are replaced with hba options */
+					errmsg("%s cannot be used in combination with %s",
+						   "map", "trust_validator_authz"),
+					errcontext("line %d of configuration file \"%s\"",
+							   line_num, file_name));
+			*err_msg = "map cannot be used in combination with trust_validator_authz";
+			return NULL;
+		}
+	}
+
 	return parsedline;
 }
 
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 73718ac3b1..f5fc6ebc23 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -89,8 +89,11 @@ client_initial_response(PGconn *conn, const char *token)
 
 	if (!PQExpBufferDataBroken(buf))
 		response = strdup(buf.data);
-
 	termPQExpBuffer(&buf);
+
+	if (!response)
+		libpq_append_conn_error(conn, "out of memory");
+
 	return response;
 }
 
diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index 3b8f057a26..3f06f5be76 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -11,11 +11,18 @@ use PostgreSQL::Test::Utils;
 use PostgreSQL::Test::OAuthServer;
 use Test::More;
 
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\boauth\b/)
+{
+	plan skip_all =>
+	  'Potentially unsafe test oauth not enabled in PG_TEST_EXTRA';
+}
+
 if ($ENV{with_oauth} ne 'curl')
 {
 	plan skip_all => 'client-side OAuth not supported by this build';
 }
-elsif ($ENV{with_python} ne 'yes')
+
+if ($ENV{with_python} ne 'yes')
 {
 	plan skip_all => 'OAuth tests require --with-python to run';
 }
diff --git a/src/test/modules/oauth_validator/validator.c b/src/test/modules/oauth_validator/validator.c
index d12c79e2a2..dbba326bc4 100644
--- a/src/test/modules/oauth_validator/validator.c
+++ b/src/test/modules/oauth_validator/validator.c
@@ -67,7 +67,10 @@ validator_startup(ValidatorModuleState *state)
 static void
 validator_shutdown(ValidatorModuleState *state)
 {
-	/* do nothing */
+	/* Check to make sure our private state still exists. */
+	if (state->private_data != PRIVATE_COOKIE)
+		elog(ERROR, "oauth_validator: private state cookie changed to %p in shutdown",
+			 state->private_data);
 }
 
 static ValidatorModuleResult *
@@ -77,7 +80,7 @@ validate_token(ValidatorModuleState *state, const char *token, const char *role)
 
 	/* Check to make sure our private state still exists. */
 	if (state->private_data != PRIVATE_COOKIE)
-		elog(ERROR, "oauth_validator: private state cookie changed to %p",
+		elog(ERROR, "oauth_validator: private state cookie changed to %p in validate",
 			 state->private_data);
 
 	res = palloc(sizeof(ValidatorModuleResult));
-- 
2.39.3 (Apple Git-146)