since-v39.diff.txt

text/plain

Filename: since-v39.diff.txt
Type: text/plain
Part: 0
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER
1:  3dc642d68c8 ! 1:  7ee8628abac Add OAUTHBEARER SASL mechanism
    @@ Commit message
         Grants (RFC 8628). This adds a new auth method, oauth, to pg_hba. When
         speaking to a OAuth-enabled server, it looks a bit like this:
     
    -        $ psql 'host=example.org oauth_client_id=f02c6361-0635-...'
    +        $ psql 'host=example.org oauth_issuer=... oauth_client_id=...'
             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 (but clients may provide their own flows; see
    -    below).
    +    are currently implemented (but clients may provide their own flows).
     
         The client implementation requires libcurl and its development headers.
    -    Pass `curl` to --with-oauth/-Doauth during configuration. The server
    +    Pass --with-libcurl/-Dlibcurl=enabled during configuration. The server
         implementation does not require additional build-time dependencies, but
    -    an external validator module must be supplied (see below).
    +    an external validator module must be supplied.
     
         Thomas Munro wrote the kqueue() implementation for oauth-curl; thanks!
     
    -    = Debug Mode =
    -
    -    A "dangerous debugging mode" may be enabled in libpq, by setting the
    -    environment variable PGOAUTHDEBUG=UNSAFE. This will do several things
    -    that you will not want in a production system:
    -
    -    - permits the use of plaintext HTTP in the OAuth provider exchange
    -    - sprays HTTP traffic, containing several critical secrets, to stderr
    -    - permits the use of zero-second retry intervals, which can DoS the
    -      client
    -
    -    = PQauthDataHook =
    -
    -    Clients may override two pieces of OAuth handling using the new
    -    PQsetAuthDataHook():
    -
    -    - PQAUTHDATA_PROMPT_OAUTH_DEVICE: replaces the default user prompt to
    -      standard error when using the builtin device authorization flow
    -
    -    - PQAUTHDATA_OAUTH_BEARER_TOKEN: replaces the entire OAuth flow with a
    -      custom asynchronous implementation
    -
    -    In general, a hook implementation should examine the incoming `type` to
    -    decide whether or not to handle a specific piece of authdata; if not, it
    -    should delegate to the previous hook in the chain (retrievable via
    -    PQgetAuthDataHook()). Otherwise, it should return an integer > 0 and
    -    follow the authdata-specific instructions. Returning an integer < 0
    -    signals an error condition and abandons the connection attempt.
    -
    -    == PQAUTHDATA_PROMPT_OAUTH_DEVICE ==
    -
    -    The hook should display the device prompt (URL + code) using whatever
    -    method it prefers.
    -
    -    == PQAUTHDATA_OAUTH_BEARER_TOKEN ==
    -
    -    The hook should either directly return a Bearer token for the current
    -    user/issuer/scope combination, if one is available without blocking, or
    -    else set up an asynchronous callback to retrieve one. See the
    -    documentation for PQoauthBearerRequest.
    -
    -    = Server-Side Validation =
    -
    -    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 validator module using callbacks.
    -    The module is responsible for:
    -
    -    1. 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 authorized member of the
    -       ValidatorModuleResult struct is used to indicate failure.
    -       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 module wull return the
    -          authenticated identity in the authn_id member.
    -
    -       b. To optionally authorize the user, in combination with the HBA
    -          option trust_validator_authz=1 (see below).
    -
    -          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 validator module may
    -              return the authenticated ID and then fail with false authorized
    -              member.  (This can make it easier to see what's going on in the
    -              Postgres logs.)
    -
    -    = OAuth HBA Method =
    -
    -    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.
    -
         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
    @@ Commit message
         - fix libcurl initialization thread-safety
         - harden the libcurl flow implementation
         - figure out pgsocket/int difference on Windows
    -    - fix intermittent failure in the cleanup callback tests (race
    -      condition?)
    -    - support require_auth
         - fill in documentation stubs
         - support protocol "variants" implemented by major providers
         - implement more helpful handling of HBA misconfigurations
         - use logdetail during auth failures
    -    - allow passing the configured issuer to the oauth_validator_command, to
    -      deal with multi-issuer setups
    -    - fill in documentation stubs
         - ...and more.
     
         Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
    @@ src/backend/libpq/auth-oauth.c (new)
     + * 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.)
    ++ * header format; RFC 9110 Sec. 11), the "Bearer" scheme string must be
    ++ * compared case-insensitively. (This is not mentioned in RFC 6750, but the
    ++ * OAUTHBEARER spec points it out: RFC 7628 Sec. 4.)
     + *
     + * Invalid formats are technically a protocol violation, but we shouldn't
     + * reflect any information about the sensitive Bearer token back to the
     + * client; log at COMMERROR instead.
    -+ *
    -+ * TODO: handle the Authorization spec, RFC 7235 Sec. 2.1.
     + */
     +static const char *
     +validate_token_format(const char *header)
    @@ src/backend/libpq/auth-oauth.c (new)
     +		return false;
     +	}
     +
    ++	/*
    ++	 * Log any authentication results even if the token isn't authorized; it
    ++	 * might be useful for auditing or troubleshooting.
    ++	 */
    ++	if (ret->authn_id)
    ++		set_authn_id(port, ret->authn_id);
    ++
     +	if (!ret->authorized)
     +	{
    ++		ereport(LOG,
    ++				errmsg("OAuth bearer authentication failed for user \"%s\"",
    ++					   port->user_name),
    ++				errdetail_log("Validator failed to authorize the provided token."));
    ++
     +		status = false;
     +		goto cleanup;
     +	}
     +
    -+	if (ret->authn_id)
    -+		set_authn_id(port, ret->authn_id);
    -+
     +	if (port->hba->oauth_skip_usermap)
     +	{
     +		/*
    @@ src/interfaces/libpq/Makefile: backend_src = $(top_srcdir)/src/backend
      endif
     
      ## src/interfaces/libpq/exports.txt ##
    -@@ src/interfaces/libpq/exports.txt: PQcancelFinish            202
    - PQsocketPoll              203
    +@@ src/interfaces/libpq/exports.txt: PQsocketPoll              203
      PQsetChunkedRowsMode      204
      PQgetCurrentTimeUSec      205
    -+PQsetAuthDataHook         206
    -+PQgetAuthDataHook         207
    -+PQdefaultAuthDataHook     208
    + PQservice                 206
    ++PQsetAuthDataHook         207
    ++PQgetAuthDataHook         208
    ++PQdefaultAuthDataHook     209
     
      ## src/interfaces/libpq/fe-auth-oauth-curl.c (new) ##
     @@
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +
     +			++field;
     +		}
    ++
    ++		/*
    ++		 * We don't allow duplicate field names; error out if the target has
    ++		 * already been set.
    ++		 */
    ++		if (ctx->active)
    ++		{
    ++			field = ctx->active;
    ++
    ++			if ((field->type == JSON_TOKEN_ARRAY_START && *field->target.array)
    ++				|| (field->type != JSON_TOKEN_ARRAY_START && *field->target.scalar))
    ++			{
    ++				oauth_parse_set_error(ctx, "field \"%s\" is duplicated",
    ++									  field->name);
    ++				return JSON_SEM_ACTION_FAILED;
    ++			}
    ++		}
     +	}
     +
     +	return JSON_SUCCESS;
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +			return JSON_SEM_ACTION_FAILED;
     +		}
     +
    -+		/*
    -+		 * We don't allow duplicate field names; error out if the target has
    -+		 * already been set.
    -+		 */
    -+		if ((field->type == JSON_TOKEN_ARRAY_START && *field->target.array)
    -+			|| (field->type != JSON_TOKEN_ARRAY_START && *field->target.scalar))
    -+		{
    -+			oauth_parse_set_error(ctx, "field \"%s\" is duplicated",
    -+								  field->name);
    -+			return JSON_SEM_ACTION_FAILED;
    -+		}
    -+
     +		if (field->type != JSON_TOKEN_ARRAY_START)
     +		{
     +			Assert(ctx->nested == 1);
    ++			Assert(!*field->target.scalar);
     +
     +			*field->target.scalar = strdup(token);
     +			if (!*field->target.scalar)
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +
     +	/*
     +	 * Multi-threaded applications must set CURLOPT_NOSIGNAL. This requires us
    -+	 * to handle the possibility of SIGPIPE ourselves.
    -+	 *
    -+	 * TODO: handle SIGPIPE via pq_block_sigpipe(), or via a
    -+	 * CURLOPT_SOCKOPTFUNCTION maybe...
    ++	 * to handle the possibility of SIGPIPE ourselves using pq_block_sigpipe;
    ++	 * see pg_fe_run_oauth_flow().
     +	 */
     +	CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false);
     +	if (!curl_info->ares_num)
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +
     +
     +/*
    -+ * The top-level, nonblocking entry point for the libcurl implementation. This
    -+ * will be called several times to pump the async engine.
    ++ * The core nonblocking libcurl implementation. This will be called several
    ++ * times to pump the async engine.
     + *
     + * The architecture is based on PQconnectPoll(). The first half drives the
     + * connection state forward as necessary, returning if we're not ready to
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     + * OAUTH_STEP_TOKEN_REQUEST and OAUTH_STEP_WAIT_INTERVAL to regularly ping the
     + * provider.
     + */
    -+PostgresPollingStatusType
    -+pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock)
    ++static PostgresPollingStatusType
    ++pg_fe_run_oauth_flow_impl(PGconn *conn, pgsocket *altsock)
     +{
     +	fe_oauth_state *state = conn->sasl_state;
     +	struct async_ctx *actx;
    @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
     +	appendPQExpBufferStr(&conn->errorMessage, "\n");
     +
     +	return PGRES_POLLING_FAILED;
    ++}
    ++
    ++/*
    ++ * The top-level entry point. This is a convenient place to put necessary
    ++ * wrapper logic before handing off to the true implementation, above.
    ++ */
    ++PostgresPollingStatusType
    ++pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock)
    ++{
    ++	PostgresPollingStatusType result;
    ++#ifndef WIN32
    ++	sigset_t	osigset;
    ++	bool		sigpipe_pending;
    ++	bool		masked;
    ++
    ++	/*---
    ++	 * Ignore SIGPIPE on this thread during all Curl processing.
    ++	 *
    ++	 * Because we support multiple threads, we have to set up libcurl with
    ++	 * CURLOPT_NOSIGNAL, which disables its default global handling of
    ++	 * SIGPIPE. From the Curl docs:
    ++	 *
    ++	 *     libcurl makes an effort to never cause such SIGPIPE signals to
    ++	 *     trigger, but some operating systems have no way to avoid them and
    ++	 *     even on those that have there are some corner cases when they may
    ++	 *     still happen, contrary to our desire.
    ++	 *
    ++	 * Note that libcurl is also at the mercy of its DNS resolution and SSL
    ++	 * libraries; if any of them forget a MSG_NOSIGNAL then we're in trouble.
    ++	 * Modern platforms and libraries seem to get it right, so this is a
    ++	 * difficult corner case to exercise in practice, and unfortunately it's
    ++	 * not really clear whether it's necessary in all cases.
    ++	 */
    ++	masked = (pq_block_sigpipe(&osigset, &sigpipe_pending) == 0);
    ++#endif
    ++
    ++	result = pg_fe_run_oauth_flow_impl(conn, altsock);
    ++
    ++#ifndef WIN32
    ++	if (masked)
    ++	{
    ++		/*
    ++		 * Undo the SIGPIPE mask. Assume we may have gotten EPIPE (we have no
    ++		 * way of knowing at this level).
    ++		 */
    ++		pq_reset_sigpipe(&osigset, sigpipe_pending, true /* EPIPE, maybe */ );
    ++	}
    ++#endif
    ++
    ++	return result;
     +}
     
      ## src/interfaces/libpq/fe-auth-oauth.c (new) ##
    @@ src/interfaces/libpq/libpq-fe.h: extern int	PQenv2encoding(void);
     +	 */
     +	PostgresPollingStatusType (*async) (PGconn *conn,
     +										struct _PGoauthBearerRequest *request,
    -+										SOCKTYPE *altsock);
    ++										SOCKTYPE * altsock);
     +
     +	/*
     +	 * Callback to clean up custom allocations. A hook implementation may use
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
     +#include <stdio.h>
     +#include <stdlib.h>
     +
    ++#ifdef WIN32
    ++#include <winsock2.h>
    ++#else
    ++#include <sys/socket.h>
    ++#endif
    ++
     +#include "getopt_long.h"
     +#include "libpq-fe.h"
     +
     +static int	handle_auth_data(PGauthData type, PGconn *conn, void *data);
    ++static PostgresPollingStatusType async_cb(PGconn *conn,
    ++										  PGoauthBearerRequest *req,
    ++										  pgsocket *altsock);
     +
     +static void
     +usage(char *argv[])
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
     +	fprintf(stderr, " --expected-scope SCOPE	fail if received scopes do not match SCOPE\n");
     +	fprintf(stderr, " --expected-uri URI		fail if received configuration link does not match URI\n");
     +	fprintf(stderr, " --no-hook					don't install OAuth hooks (connection will fail)\n");
    ++	fprintf(stderr, " --hang-forever			don't ever return a token (combine with connect_timeout)\n");
     +	fprintf(stderr, " --token TOKEN				use the provided TOKEN value\n");
     +}
     +
    ++/* --options */
     +static bool no_hook = false;
    ++static bool hang_forever = false;
     +static const char *expected_uri = NULL;
     +static const char *expected_scope = NULL;
     +static char *token = NULL;
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
     +		{"expected-uri", required_argument, NULL, 1001},
     +		{"no-hook", no_argument, NULL, 1002},
     +		{"token", required_argument, NULL, 1003},
    ++		{"hang-forever", no_argument, NULL, 1004},
     +		{0}
     +	};
     +
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
     +				token = optarg;
     +				break;
     +
    ++			case 1004:			/* --hang-forever */
    ++				hang_forever = true;
    ++				break;
    ++
     +			default:
     +				usage(argv);
     +				return 1;
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
     +	if (no_hook || (type != PQAUTHDATA_OAUTH_BEARER_TOKEN))
     +		return 0;
     +
    ++	if (hang_forever)
    ++	{
    ++		/* Start asynchronous processing. */
    ++		req->async = async_cb;
    ++		return 1;
    ++	}
    ++
     +	if (expected_uri)
     +	{
     +		if (!req->openid_configuration)
    @@ src/test/modules/oauth_validator/oauth_hook_client.c (new)
     +
     +	req->token = token;
     +	return 1;
    ++}
    ++
    ++static PostgresPollingStatusType
    ++async_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock)
    ++{
    ++	if (hang_forever)
    ++	{
    ++		/*
    ++		 * This code tests that nothing is interfering with libpq's handling
    ++		 * of connect_timeout.
    ++		 */
    ++		static pgsocket sock = PGINVALID_SOCKET;
    ++
    ++		if (sock == PGINVALID_SOCKET)
    ++		{
    ++			/* First call. Create an unbound socket to wait on. */
    ++#ifdef WIN32
    ++			WSADATA		wsaData;
    ++			int			err;
    ++
    ++			err = WSAStartup(MAKEWORD(2, 2), &wsaData);
    ++			if (err)
    ++			{
    ++				perror("WSAStartup failed");
    ++				return PGRES_POLLING_FAILED;
    ++			}
    ++#endif
    ++			sock = socket(AF_INET, SOCK_DGRAM, 0);
    ++			if (sock == PGINVALID_SOCKET)
    ++			{
    ++				perror("failed to create datagram socket");
    ++				return PGRES_POLLING_FAILED;
    ++			}
    ++		}
    ++
    ++		/* Make libpq wait on the (unreadable) socket. */
    ++		*altsock = sock;
    ++		return PGRES_POLLING_READING;
    ++	}
    ++
    ++	req->token = token;
    ++	return PGRES_POLLING_OK;
     +}
     
      ## src/test/modules/oauth_validator/t/001_server.pl (new) ##
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +});
     +$node->reload;
     +
    -+my ($log_start, $log_end);
    -+$log_start = $node->wait_for_log(qr/reloading configuration files/);
    ++my $log_start = $node->wait_for_log(qr/reloading configuration files/);
     +
     +
     +# To test against HTTP rather than HTTPS, we need to enable PGOAUTHDEBUG. But
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +$ENV{PGOAUTHDEBUG} = "UNSAFE";
     +
     +my $user = "test";
    -+if ($node->connect_ok(
    -+		"user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
    -+		"connect",
    -+		expected_stderr =>
    -+		  qr@Visit https://example\.com/ and enter the code: postgresuser@))
    -+{
    -+	$log_end = $node->wait_for_log(qr/connection authorized/, $log_start);
    -+	$node->log_check(
    -+		"user $user: validator receives correct parameters",
    -+		$log_start,
    -+		log_like => [
    -+			qr/oauth_validator: token="9243959234", role="$user"/,
    -+			qr/oauth_validator: issuer="\Q$issuer\E", scope="openid postgres"/,
    -+		]);
    -+	$node->log_check(
    -+		"user $user: validator sets authenticated identity",
    -+		$log_start,
    -+		log_like =>
    -+		  [ qr/connection authenticated: identity="test" method=oauth/, ]);
    -+	$log_start = $log_end;
    -+}
    ++$node->connect_ok(
    ++	"user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
    ++	"connect as test",
    ++	expected_stderr =>
    ++	  qr@Visit https://example\.com/ and enter the code: postgresuser@,
    ++	log_like => [
    ++		qr/oauth_validator: token="9243959234", role="$user"/,
    ++		qr/oauth_validator: issuer="\Q$issuer\E", scope="openid postgres"/,
    ++		qr/connection authenticated: identity="test" method=oauth/,
    ++		qr/connection authorized/,
    ++	]);
     +
     +# The /alternate issuer uses slightly different parameters, along with an
     +# OAuth-style discovery document.
     +$user = "testalt";
    -+if ($node->connect_ok(
    -+		"user=$user dbname=postgres oauth_issuer=$issuer/alternate oauth_client_id=f02c6361-0636",
    -+		"connect",
    -+		expected_stderr =>
    -+		  qr@Visit https://example\.org/ and enter the code: postgresuser@))
    -+{
    -+	$log_end = $node->wait_for_log(qr/connection authorized/, $log_start);
    -+	$node->log_check(
    -+		"user $user: validator receives correct parameters",
    -+		$log_start,
    -+		log_like => [
    -+			qr/oauth_validator: token="9243959234-alt", role="$user"/,
    -+			qr|oauth_validator: issuer="\Q$issuer/alternate\E", scope="openid postgres alt"|,
    -+		]);
    -+	$node->log_check(
    -+		"user $user: validator sets authenticated identity",
    -+		$log_start,
    -+		log_like =>
    -+		  [ qr/connection authenticated: identity="testalt" method=oauth/, ]);
    -+	$log_start = $log_end;
    -+}
    ++$node->connect_ok(
    ++	"user=$user dbname=postgres oauth_issuer=$issuer/alternate oauth_client_id=f02c6361-0636",
    ++	"connect as testalt",
    ++	expected_stderr =>
    ++	  qr@Visit https://example\.org/ and enter the code: postgresuser@,
    ++	log_like => [
    ++		qr/oauth_validator: token="9243959234-alt", role="$user"/,
    ++		qr|oauth_validator: issuer="\Q$issuer/.well-known/oauth-authorization-server/alternate\E", scope="openid postgres alt"|,
    ++		qr/connection authenticated: identity="testalt" method=oauth/,
    ++		qr/connection authorized/,
    ++	]);
     +
     +# The issuer linked by the server must match the client's oauth_issuer setting.
     +$node->connect_fails(
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +$common_connstr =
     +  "dbname=postgres oauth_issuer=$issuer/.well-known/openid-configuration oauth_scope='' oauth_client_id=f02c6361-0635";
     +
    ++# Misbehaving validators must fail shut.
     +$bgconn->query_safe("ALTER SYSTEM SET oauth_validator.authn_id TO ''");
     +$node->reload;
     +$log_start =
     +  $node->wait_for_log(qr/reloading configuration files/, $log_start);
     +
    -+if ($node->connect_fails(
    -+		"$common_connstr user=test",
    -+		"validator must set authn_id",
    -+		expected_stderr => qr/OAuth bearer authentication failed/))
    -+{
    -+	$log_end =
    -+	  $node->wait_for_log(qr/FATAL:\s+OAuth bearer authentication failed/,
    -+		$log_start);
    -+
    -+	$node->log_check(
    -+		"validator must set authn_id: breadcrumbs are logged",
    -+		$log_start,
    -+		log_like => [
    -+			qr/connection authenticated: identity=""/,
    -+			qr/DETAIL:\s+Validator provided no identity/,
    -+			qr/FATAL:\s+OAuth bearer authentication failed/,
    -+		]);
    -+
    -+	$log_start = $log_end;
    -+}
    ++$node->connect_fails(
    ++	"$common_connstr user=test",
    ++	"validator must set authn_id",
    ++	expected_stderr => qr/OAuth bearer authentication failed/,
    ++	log_like => [
    ++		qr/connection authenticated: identity=""/,
    ++		qr/DETAIL:\s+Validator provided no identity/,
    ++		qr/FATAL:\s+OAuth bearer authentication failed/,
    ++	]);
    ++
    ++# Even if a validator authenticates the user, if the token isn't considered
    ++# valid, the connection fails.
    ++$bgconn->query_safe(
    ++	"ALTER SYSTEM SET oauth_validator.authn_id TO 'test\@example.org'");
    ++$bgconn->query_safe(
    ++	"ALTER SYSTEM SET oauth_validator.authorize_tokens TO false");
    ++$node->reload;
    ++$log_start =
    ++  $node->wait_for_log(qr/reloading configuration files/, $log_start);
    ++
    ++$node->connect_fails(
    ++	"$common_connstr user=test",
    ++	"validator must authorize token explicitly",
    ++	expected_stderr => qr/OAuth bearer authentication failed/,
    ++	log_like => [
    ++		qr/connection authenticated: identity="test\@example\.org"/,
    ++		qr/DETAIL:\s+Validator failed to authorize the provided token/,
    ++		qr/FATAL:\s+OAuth bearer authentication failed/,
    ++	]);
     +
     +#
     +# Test user mapping.
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +
     +# To start, have the validator use the role names as authn IDs.
     +$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authn_id");
    ++$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authorize_tokens");
     +
     +$node->reload;
     +$log_start =
    @@ src/test/modules/oauth_validator/t/001_server.pl (new)
     +
     +# The test user should work as before.
     +$user = "test";
    -+if ($node->connect_ok(
    -+		"user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
    -+		"validator is used for $user",
    -+		expected_stderr =>
    -+		  qr@Visit https://example\.com/ and enter the code: postgresuser@))
    -+{
    -+	$log_start = $node->wait_for_log(qr/connection authorized/, $log_start);
    -+}
    ++$node->connect_ok(
    ++	"user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
    ++	"validator is used for $user",
    ++	expected_stderr =>
    ++	  qr@Visit https://example\.com/ and enter the code: postgresuser@,
    ++	log_like => [qr/connection authorized/]);
     +
     +# testalt should be routed through the fail_validator.
     +$user = "testalt";
    @@ src/test/modules/oauth_validator/t/002_client.pl (new)
     +	);
     +}
     +
    ++# connect_timeout should work if the flow doesn't respond.
    ++$common_connstr = "$common_connstr connect_timeout=1";
    ++test(
    ++	"connect_timeout interrupts hung client flow",
    ++	flags => ["--hang-forever"],
    ++	expected_stderr => qr/failed: timeout expired/);
    ++
     +done_testing();
     
      ## src/test/modules/oauth_validator/t/OAuth/Server.pm (new) ##
    @@ src/test/modules/oauth_validator/t/oauth_server.py (new)
     +            "response_types_supported": ["token"],
     +            "subject_types_supported": ["public"],
     +            "id_token_signing_alg_values_supported": ["RS256"],
    -+            "grant_types_supported": ["urn:ietf:params:oauth:grant-type:device_code"],
    ++            "grant_types_supported": [
    ++                "authorization_code",
    ++                "urn:ietf:params:oauth:grant-type:device_code",
    ++            ],
     +        }
     +
     +    @property
    @@ src/test/modules/oauth_validator/validator.c (new)
     +	.validate_cb = validate_token
     +};
     +
    ++/* GUCs */
     +static char *authn_id = NULL;
    ++static bool authorize_tokens = true;
     +
     +/*---
     + * Extension entry point. Sets up GUCs for use by tests:
    @@ src/test/modules/oauth_validator/validator.c (new)
     + * - oauth_validator.authn_id	Sets the user identifier to return during token
     + *								validation. Defaults to the username in the
     + *								startup packet.
    ++ *
    ++ * - oauth_validator.authorize_tokens
    ++ *								Sets whether to successfully validate incoming
    ++ *								tokens. Defaults to true.
     + */
     +void
     +_PG_init(void)
    @@ src/test/modules/oauth_validator/validator.c (new)
     +							   PGC_SIGHUP,
     +							   0,
     +							   NULL, NULL, NULL);
    ++	DefineCustomBoolVariable("oauth_validator.authorize_tokens",
    ++							 "Should tokens be marked valid?",
    ++							 NULL,
    ++							 &authorize_tokens,
    ++							 true,
    ++							 PGC_SIGHUP,
    ++							 0,
    ++							 NULL, NULL, NULL);
     +
     +	MarkGUCPrefixReserved("oauth_validator");
     +}
    @@ src/test/modules/oauth_validator/validator.c (new)
     +}
     +
     +/*
    -+ * Validator implementation. Logs the incoming data and authorizes the token;
    -+ * the behavior can be modified via the module's GUC settings.
    ++ * Validator implementation. Logs the incoming data and authorizes the token by
    ++ * default; the behavior can be modified via the module's GUC settings.
     + */
     +static ValidatorModuleResult *
     +validate_token(ValidatorModuleState *state, const char *token, const char *role)
    @@ src/test/modules/oauth_validator/validator.c (new)
     +		 MyProcPort->hba->oauth_issuer,
     +		 MyProcPort->hba->oauth_scope);
     +
    -+	res->authorized = true;
    ++	res->authorized = authorize_tokens;
     +	if (authn_id)
     +		res->authn_id = pstrdup(authn_id);
     +	else
-:  ----------- > 2:  de155343c81 squash! Add OAUTHBEARER SASL mechanism
2:  566d90d30a7 ! 3:  661de01c4ed DO NOT MERGE: Add pytest suite for OAuth
    @@ src/test/python/client/test_oauth.py (new)
     +                "subject_types_supported": ["public"],
     +                "id_token_signing_alg_values_supported": ["RS256"],
     +                "grant_types_supported": [
    -+                    "urn:ietf:params:oauth:grant-type:device_code"
    ++                    "authorization_code",
    ++                    "urn:ietf:params:oauth:grant-type:device_code",
     +                ],
     +            }
     +
    @@ src/test/python/client/test_oauth.py (new)
     +                # that break the HTTP protocol. Just return and have the server
     +                # close the socket.
     +                return
    ++            except ssl.SSLError as err:
    ++                # FIXME OpenSSL 3.4 introduced an incompatibility with Python's
    ++                # TLS error handling, resulting in a bogus "[SYS] unknown error"
    ++                # on some platforms. Hopefully this is fixed in 2025's set of
    ++                # maintenance releases and this case can be removed.
    ++                #
    ++                #     https://github.com/python/cpython/issues/127257
    ++                #
    ++                if "[SYS] unknown error" in str(err):
    ++                    return
    ++                raise
     +
     +            super().shutdown_request(request)
     +