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 @@ 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 +#include + ++#ifdef WIN32 ++#include ++#else ++#include ++#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) +