1: 785add80157 ! 1: 3dc642d68c8 Add OAUTHBEARER SASL mechanism @@ doc/src/sgml/client-auth.sgml: host ... radius radiusservers="server1,server2" r + + OAuth client support has to be enabled when PostgreSQL + is built, see for more information. ++ + -+ -+ -+ -+ Resource owner: The user or system who owns protected resources and can -+ grant access to them. -+ -+ -+ -+ -+ Client: The system which accesses the protected resources using access -+ tokens. Applications using libpq are the clients in connecting to a -+ PostgreSQL cluster. -+ -+ -+ -+ -+ Authorization server: The system which receives requests from, and -+ issues access tokens to, the client after the authenticated resource -+ owner has given approval. -+ -+ ++ ++ This documentation uses the following terminology when discussing the OAuth ++ ecosystem: + -+ -+ -+ Resource server: The system which hosts the protected resources which are -+ accessed by the client. The PostgreSQL cluster -+ being connected to is the resource server. -+ -+ ++ + -+ ++ ++ Resource Owner (or End User) ++ ++ ++ The user or system who owns protected resources and can grant access to ++ them. This documentation also uses the term end user ++ when the resource owner is a person. When you use ++ psql to connect to the database using OAuth, ++ you are the resource owner/end user. ++ ++ ++ ++ ++ ++ Client ++ ++ ++ The system which accesses the protected resources using access ++ tokens. Applications using libpq, such as psql, ++ are the OAuth clients when connecting to a ++ PostgreSQL cluster. ++ ++ ++ ++ ++ ++ Resource Server ++ ++ ++ The system which hosts the protected resources which are ++ accessed by the client. The PostgreSQL ++ cluster being connected to is the resource server. ++ ++ ++ ++ ++ ++ Provider ++ ++ ++ The organization, product vendor, or other entity which develops and/or ++ administers the OAuth servers and clients for a given application. ++ Different providers typically choose different implementation details ++ for their OAuth systems; a client of one provider is not generally ++ guaranteed to have access to the servers of another. ++ ++ ++ This use of the term "provider" is not standard, but it seems to be in ++ wide use colloquially. (It should not be confused with OpenID's similar ++ term "Identity Provider". While the implementation of OAuth in ++ PostgreSQL is intended to be interoperable ++ and compatible with OpenID Connect/OIDC, it is not itself an OIDC client ++ and does not require its use.) ++ ++ ++ ++ ++ ++ Authorization Server ++ ++ ++ The system which receives requests from, and issues access tokens to, ++ the client after the authenticated resource owner has given approval. ++ PostgreSQL does not provide an authorization ++ server; it's obtained from the OAuth provider. ++ ++ ++ ++ ++ ++ Issuer ++ ++ ++ An identifier for an authorization server, printed as an ++ https:// URL, which provides a trusted "namespace" ++ for OAuth clients and applications. The issuer identifier allows a ++ single authorization server to talk to the clients of mutually ++ untrusting entities, as long as they maintain separate issuers. ++ ++ ++ ++ ++ ++ ++ ++ ++ For small deployments, there may not be a meaningful distinction between ++ the "provider", "authorization server", and "issuer". However, for more ++ complicated setups, there may be a one-to-many (or many-to-many) ++ relationship: a provider may rent out multiple issuer identifiers to ++ separate tenants, then provide multiple authorization servers, possibly ++ with different supported feature sets, to interact with their clients. ++ ++ + + + @@ doc/src/sgml/client-auth.sgml: host ... radius radiusservers="server1,server2" r + RFC 6750, + 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. ++ and is chosen by each authorization server. + + + @@ doc/src/sgml/client-auth.sgml: host ... radius radiusservers="server1,server2" r + + + -+ trust_validator_authz ++ ++ delegate_ident_mapping ++ + + + An advanced option which is not intended for common use. + + -+ When set to 1, standard user mapping is skipped, and -+ the OAuth validator takes full responsibility for mapping end user -+ identities to database roles. If the validator authorizes the token, -+ the server trusts that the user is allowed to connect under the -+ requested role, and the connection is allowed to proceed regardless of -+ the authentication status of the user. ++ When set to 1, standard user mapping with ++ pg_ident.conf is skipped, and the OAuth validator ++ takes full responsibility for mapping end user identities to database ++ roles. If the validator authorizes the token, the server trusts that ++ the user is allowed to connect under the requested role, and the ++ connection is allowed to proceed regardless of the authentication ++ status of the user. + + + This parameter is incompatible with map. + + + -+ trust_validator_authz provides additional ++ delegate_ident_mapping provides additional + flexibility in the design of the authentication system, but it also + requires careful implementation of the OAuth validator, which must + determine whether the provided token carries sufficient end-user @@ doc/src/sgml/config.sgml: include_dir 'conf.d' + oauth HBA entries + must explicitly set a validator chosen from this + list. If set to an empty string (the default), OAuth connections will be -+ refused. For more information on implementing OAuth validators see -+ . This parameter can only be set in -+ the postgresql.conf file. ++ refused. This parameter can only be set in the ++ postgresql.conf file. ++ ++ ++ Validator modules must be implemented/obtained separately; ++ PostgreSQL does not ship with any default ++ implementations. For more information on implementing OAuth validators, ++ see . + + + @@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + setting in the server's HBA configuration. + + -+ As part of the standard authentication handshake, libpq will ask the -+ server for a discovery document: a URI providing a -+ set of OAuth configuration parameters. The server must provide a URI -+ that is directly constructed from the components of the ++ As part of the standard authentication handshake, libpq ++ will ask the server for a discovery document: a URI ++ providing a set of OAuth configuration parameters. The server must ++ provide a URI that is directly constructed from the components of the + oauth_issuer, and this value must exactly match the + issuer identifier that is declared in the discovery document itself, or + the connection will fail. This is required to prevent a class of "mix-up @@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + be set as well, since the + client will not have a chance to ask the server for a correct scope + setting, and the default scopes for a token may not be sufficient to -+ connect.) libpq currently supports the following well-known endpoints: -+ ++ connect.) libpq currently supports the ++ following well-known endpoints: ++ + /.well-known/openid-configuration + /.well-known/oauth-authorization-server + @@ doc/src/sgml/libpq.sgml: void PQinitSSL(int do_ssl); + TODO + + -+ -+ -+ -+ PQsetAuthDataHookPQsetAuthDataHook ++ ++ Authdata Hooks + -+ -+ -+ TODO ++ ++ The behavior of the OAuth flow may be modified or replaced by a client using ++ the following hook API: ++ ++ ++ ++ PQsetAuthDataHookPQsetAuthDataHook ++ ++ ++ ++ Sets the PGauthDataHook, overriding ++ libpq's handling of one or more aspects of ++ its OAuth client flow. + +void PQsetAuthDataHook(PQauthDataHook_type hook); + -+ ++ If hook is NULL, the ++ default handler will be reinstalled. Otherwise, the application passes ++ a pointer to a callback function with the signature: ++ ++int hook_fn(PGauthData type, PGconn *conn, void *data); ++ ++ which libpq will call when when action is ++ required of the application. type describes ++ the request being made, conn is the ++ connection handle being authenticated, and data ++ points to request-specific metadata. The contents of this pointer are ++ determined by type; see ++ for the supported ++ list. ++ ++ ++ Hooks can be chained together to allow cooperative and/or fallback ++ behavior. In general, a hook implementation should examine the incoming ++ type (and, potentially, the request metadata ++ and/or the settings for the particular conn ++ in use) 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). ++ ++ ++ Success is indicated by returning an integer greater than zero. ++ Returning a negative integer signals an error condition and abandons the ++ connection attempt. (A zero value is reserved for the default ++ implementation.) ++ ++ ++ ++ ++ ++ PQgetAuthDataHookPQgetAuthDataHook ++ ++ ++ ++ Retrieves the current value of PGauthDataHook. ++ ++PQauthDataHook_type PQgetAuthDataHook(void); ++ ++ At initialization time (before the first call to ++ PQsetAuthDataHook), this function will return ++ PQdefaultAuthDataHook. ++ ++ ++ ++ ++ ++ ++ ++ Hook Types ++ ++ The following PGauthData types and their corresponding ++ data structures are defined: ++ ++ ++ ++ ++ PQAUTHDATA_PROMPT_OAUTH_DEVICE ++ PQAUTHDATA_PROMPT_OAUTH_DEVICE ++ ++ ++ ++ Replaces the default user prompt during the builtin device ++ authorization client flow. data points to ++ an instance of PGpromptOAuthDevice: ++ ++typedef struct _PGpromptOAuthDevice ++{ ++ const char *verification_uri; /* verification URI to visit */ ++ const char *user_code; /* user code to enter */ ++} PGpromptOAuthDevice; ++ ++ ++ ++ The OAuth Device Authorization flow included in libpq ++ requires the end user to visit a URL with a browser, then enter a code ++ which permits libpq to connect to the server ++ on their behalf. The default prompt simply prints the ++ verification_uri and user_code ++ on standard error. Replacement implementations may display this ++ information using any preferred method, for example with a GUI. ++ ++ ++ This callback is only invoked during the builtin device ++ authorization flow. If the application installs a ++ custom OAuth ++ flow, this authdata type will not be used. ++ ++ ++ ++ ++ ++ ++ PQAUTHDATA_OAUTH_BEARER_TOKEN ++ PQAUTHDATA_OAUTH_BEARER_TOKEN ++ ++ ++ ++ Replaces the entire OAuth flow with a custom implementation. 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. ++ ++ ++ data points to an instance ++ of PGoauthBearerRequest, which should be filled in ++ by the implementation: ++ ++typedef struct _PGoauthBearerRequest ++{ ++ /* Hook inputs (constant across all calls) */ ++ const char *const openid_configuration; /* OIDC discovery URI */ ++ const char *const scope; /* required scope(s), or NULL */ + ++ /* Hook outputs */ ++ ++ /* Callback implementing a custom asynchronous OAuth flow. */ ++ PostgresPollingStatusType (*async) (PGconn *conn, ++ struct _PGoauthBearerRequest *request, ++ SOCKTYPE *altsock); ++ ++ /* Callback to clean up custom allocations. */ ++ void (*cleanup) (PGconn *conn, struct _PGoauthBearerRequest *request); ++ ++ char *token; /* acquired Bearer token */ ++ void *user; /* hook-defined allocated data */ ++} PGoauthBearerRequest; ++ ++ ++ ++ Two pieces of information are provided to the hook by ++ libpq: ++ openid_configuration contains the URL of an ++ OAuth discovery document describing the authorization server's ++ supported flows, and scope contains a ++ (possibly empty) space-separated list of OAuth scopes which are ++ required to access the server. Either or both may be ++ NULL to indicate that the information was not ++ discoverable. (In this case, implementations may be able to establish ++ the requirements using some other preconfigured knowledge, or they may ++ choose to fail.) ++ ++ ++ The final output of the hook is token, which ++ must point to a valid Bearer token for use on the connection. (This ++ token should be issued by the ++ and hold the requested ++ scopes, or the connection will be rejected by the server's validator ++ module.) The allocated token string must remain valid until ++ libpq is finished connecting; the hook ++ should set a cleanup callback which will be ++ called when libpq no longer requires it. ++ ++ ++ If an implementation cannot immediately produce a ++ token during the initial call to the hook, ++ it should set the async callback to handle ++ nonblocking communication with the authorization server. ++ ++ ++ Performing blocking operations during the ++ PQAUTHDATA_OAUTH_BEARER_TOKEN hook callback will ++ interfere with nonblocking connection APIs such as ++ PQconnectPoll and prevent concurrent connections ++ from making progress. Applications which only ever use the ++ synchronous connection primitives, such as ++ PQconnectdb, may synchronously retrieve a token ++ during the hook instead of implementing the ++ async callback, but they will necessarily ++ be limited to one connection at a time. ++ ++ ++ This will be called to begin the flow immediately upon return from the ++ hook. When the callback cannot make further progress without blocking, ++ it should return either PGRES_POLLING_READING or ++ PGRES_POLLING_WRITING after setting ++ *pgsocket to the file descriptor that will be marked ++ ready to read/write when progress can be made again. (This descriptor ++ is then provided to the top-level polling loop via ++ PQsocket().) Return PGRES_POLLING_OK ++ after setting token when the flow is ++ complete, or PGRES_POLLING_FAILED to indicate failure. ++ ++ ++ Implementations may wish to store additional data for bookkeeping ++ across calls to the async and ++ cleanup callbacks. The ++ user pointer is provided for this purpose; ++ libpq will not touch its contents and the ++ application may use it at its convenience. (Remember to free any ++ allocations during token cleanup.) ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Debugging and Developer Settings ++ ++ ++ A "dangerous debugging mode" may be enabled by setting the environment ++ variable PGOAUTHDEBUG=UNSAFE. This functionality is provided ++ for ease of local development and testing only. It does several things that ++ you will not want a production system to do: ++ ++ ++ + -+ If hook is set to a null pointer instead of -+ a function pointer, the default hook will be installed. ++ permits the use of unencrypted HTTP during the OAuth provider exchange + + -+ -+ -+ -+ PQgetAuthDataHookPQgetAuthDataHook -+ + + -+ Retrieves the current value of PGauthDataHook. -+ -+PQauthDataHook_type PQgetAuthDataHook(void); -+ ++ allows the system's trusted CA list to be completely replaced using the ++ PGOAUTHCAFILE environment variable + + -+ -+ -+ -+ ++ ++ ++ sprays HTTP traffic (containing several critical secrets) to standard ++ error during the OAuth flow ++ ++ ++ ++ ++ permits the use of zero-second retry intervals, which can cause the ++ client to busy-loop and pointlessly consume CPU ++ ++ ++ ++ ++ ++ ++ Do not share the output of the OAuth flow traffic with third parties. It ++ contains secrets that can be used to attack your clients and servers. ++ ++ ++ + + @@ doc/src/sgml/oauth-validators.sgml (new) + + PostgreSQL provides infrastructure for creating + custom modules to perform server-side validation of OAuth bearer tokens. ++ Because OAuth implementations vary so wildly, and bearer token validation is ++ heavily dependent on the issuing party, the server cannot check the token ++ itself; validator modules provide the glue between the server and the OAuth ++ provider in use. + + -+ OAuth validation modules must at least consist of an initialization function ++ OAuth validator modules must at least consist of an initialization function + (see ) and the required callback for + performing validation (see ). + ++ ++ ++ Since a misbehaving validator might let unauthorized users into the database, ++ correct implementation is critical. See ++ for design considerations. ++ ++ ++ ++ ++ Safely Designing a Validator Module ++ ++ ++ Read and understand the entirety of this section before implementing a ++ validator module. A malfunctioning validator is potentially worse than no ++ authentication at all, both because of the false sense of security it ++ provides, and because it may contribute to attacks against other pieces of ++ an OAuth ecosystem. ++ ++ ++ ++ ++ Validator Responsibilities ++ ++ TODO ++ ++ ++ ++ Validate the Token ++ ++ ++ The validator must first ensure that the presented token is in fact a ++ valid Bearer token for use in client authentication. The correct way to ++ do this depends on the provider, but it generally involves either ++ cryptographic operations to prove that the token was created by a trusted ++ party (offline validation), or the presentation of the token to that ++ trusted party so that it can perform validation for you (online ++ validation). ++ ++ ++ Online validation, usually implemented via ++ OAuth Token ++ Introspection, requires fewer steps of a validator module and ++ allows central revocation of a token in the event that it is stolen ++ or misissued. However, it does require the module to make at least one ++ network call per authentication attempt (all of which must complete ++ within the configured ). ++ Additionally, your provider may not provide introspection endpoints for ++ use by external resource servers. ++ ++ ++ Offline validation is much more involved, typically requiring a validator ++ to maintain a list of trusted signing keys for a provider and then ++ check the token's cryptographic signature along with its contents. ++ Implementations must follow the provider's instructions to the letter, ++ including any verification of issuer ("where is this token from?"), ++ audience ("who is this token for?"), and validity period ("when can this ++ token be used?"). Since there is no communication between the module and ++ the provider, tokens cannot be centrally revoked using this method; ++ offline validator implementations may wish to place restrictions on the ++ maximum length of a token's validity period. ++ ++ ++ If the token cannot be validated, the module should immediately fail. ++ Further authentication/authorization is pointless if the bearer token ++ wasn't issued by a trusted party. ++ ++ ++ ++ ++ Authorize the Client ++ ++ ++ Next the validator must ensure that the end user has given the client ++ permission to access the server on their behalf. This generally involves ++ checking the scopes that have been assigned to the token, to make sure ++ that they cover database access for the current HBA parameters. ++ ++ ++ The purpose of this step is to prevent an OAuth client from obtaining a ++ token under false pretenses. If the validator requires all tokens to ++ carry scopes that cover database access, the provider should then loudly ++ prompt the user to grant that access during the flow. This gives them the ++ opportunity to reject the request if the client isn't supposed to be ++ using their credentials to connect to databases. ++ ++ ++ While it is possible to establish client authorization without explicit ++ scopes by using out-of-band knowledge of the deployed architecture, doing ++ so removes the user from the loop, which prevents them from catching ++ deployment mistakes and allows any such mistakes to be exploited ++ silently. Access to the database must be tightly restricted to only ++ trusted clients ++ ++ ++ That is, "trusted" in the sense that the OAuth client and the ++ PostgreSQL server are controlled by the same ++ entity. Notably, the Device Authorization client flow supported by ++ libpq does not usually meet this bar, since it's designed for use by ++ public/untrusted clients. ++ ++ ++ if users are not prompted for additional scopes. ++ ++ ++ ++ ++ Authenticate the End User ++ ++ ++ Finally, the validator should determine a user identifier for the token, ++ either by asking the provider for this information or by extracting it ++ from the token itself, and return that identifier to the server (which ++ will then make a final authorization decision using the HBA ++ configuration). This identifier will be available within the session via ++ system_user ++ and recorded in the server logs if ++ is enabled. ++ ++ ++ Different providers may record a variety of different authentication ++ information for an end user, typically referred to as ++ claims. Providers usually document which of these ++ claims are trustworthy enough to use for authorization decisions and ++ which are not. (For instance, it would probably not be wise to use an ++ end user's full name as the identifier for authentication, since many ++ providers allow users to change their display names arbitrarily.) ++ Ultimately, the choice of which claim (or combination of claims) to use ++ comes down to the provider implementation and application requirements. ++ ++ ++ Note that anonymous/pseudonymous login is possible as well, by enabling ++ usermap delegation; see ++ . ++ ++ ++ ++ ++ ++ ++ ++ General Coding Guidelines ++ ++ TODO ++ ++ ++ ++ Token Confidentiality ++ ++ ++ Modules should not write tokens, or pieces of tokens, into the server ++ log. This is true even if the module considers the token invalid; an ++ attacker who confuses a client into communicating with the wrong provider ++ should not be able to retrieve that (otherwise valid) token from the ++ disk. ++ ++ ++ Implementations that send tokens over the network (for example, to ++ perform online token validation with a provider) must authenticate the ++ peer and ensure that strong transport security is in use. ++ ++ ++ ++ ++ Logging ++ ++ ++ Modules may use the same logging ++ facilities as standard extensions; however, the rules for emitting ++ log entries to the client are subtly different during the authentication ++ phase of the connection. Generally speaking, modules should log ++ verification problems at the COMMERROR level and return ++ normally, instead of using ERROR/FATAL ++ to unwind the stack, to avoid leaking information to unauthenticated ++ clients. ++ ++ ++ ++ ++ Interruptibility ++ ++ ++ Modules must remain interruptible by signals so that the server can ++ correctly handle authentication timeouts and shutdown signals from ++ pg_ctl. For example, a module receiving ++ EINTR/EAGAIN from a blocking call ++ should call CHECK_FOR_INTERRUPTS() before retrying. ++ The same should be done during any long-running loops. Failure to follow ++ this guidance may result in hung sessions. ++ ++ ++ ++ ++ Testing ++ ++ ++ The breadth of testing an OAuth system is well beyond the scope of this ++ documentation, but note that implementers should consider negative ++ testing to be mandatory. It's trivial to design a module that lets ++ authorized users in; the whole point of the system is to keep ++ unauthorized users out. ++ ++ ++ ++ ++ ++ ++ ++ Authorizing Users (Usermap Delegation) ++ ++ The standard deliverable of a validation module is the user identifier, ++ which the server will then compare to any configured ++ pg_ident.conf ++ mappings and determine whether the end user is authorized to connect. ++ However, OAuth is itself an authorization framework, and tokens may carry ++ information about user privileges. For example, a token may be associated ++ with the organizational groups that a user belongs to, or list the roles ++ that a user may assume, and duplicating that knowledge into local usermaps ++ for every server may not be desirable. ++ ++ ++ To bypass username mapping entirely, and have the validator module assume ++ the additional responsibility of authorizing user connections, the HBA may ++ be configured with . ++ The module may then use token scopes or an equivalent method to decide ++ whether the user is allowed to connect under their desired role. The user ++ identifier will still be recorded by the server, but it plays no part in ++ determining whether to continue the connection. ++ ++ ++ Using this scheme, authentication itself is optional. As long as the module ++ reports that the connection is authorized, login will continue even if there ++ is no recorded user identifier at all. This makes it possible to implement ++ anonymous or pseudonymous access to the database, where the third-party ++ provider performs all necessary authentication but does not provide any ++ user-identifying information to the server. (Some providers may create an ++ anonymized ID number that can be recorded instead, for later auditing.) ++ ++ ++ Usermap delegation provides the most architectural flexibility, but it turns ++ the validator module into a single point of failure for connection ++ authorization. Use with caution. ++ ++ ++ + + + Initialization Functions @@ doc/src/sgml/oauth-validators.sgml (new) + validator module a function named + _PG_oauth_validator_module_init must be provided. The + return value of the function must be a pointer to a struct of type -+ OAuthValidatorCallbacks which contains all that -+ libpq need to perform token validation using the module. The returned ++ OAuthValidatorCallbacks, which contains pointers to ++ the module's token validation functions. The returned + pointer must be of server lifetime, which is typically achieved by defining + it as a static const variable in global scope. + @@ doc/src/sgml/oauth-validators.sgml (new) + OAuth Validator Callbacks + + OAuth validator modules implement their functionality by defining a set of -+ callbacks, libpq will call them as required to process the authentication -+ request from the user. ++ callbacks. The server will call them as required to process the ++ authentication request from the user. + + + @@ src/backend/libpq/auth-oauth.c (new) + .max_message_length = PG_MAX_AUTH_TOKEN_LENGTH, +}; + -+ ++/* Valid states for the oauth_exchange() machine. */ +typedef enum +{ + OAUTH_STATE_INIT = 0, @@ src/backend/libpq/auth-oauth.c (new) + OAUTH_STATE_FINISHED, +} oauth_state; + ++/* Mechanism callback state. */ +struct oauth_ctx +{ + oauth_state state; @@ src/backend/libpq/auth-oauth.c (new) +static void generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen); +static bool validate(Port *port, const char *auth); + -+#define KVSEP 0x01 -+#define AUTH_KEY "auth" -+#define BEARER_SCHEME "Bearer " ++/* Constants seen in an OAUTHBEARER client initial response. */ ++#define KVSEP 0x01 /* separator byte for key/value pairs */ ++#define AUTH_KEY "auth" /* key containing the Authorization header */ ++#define BEARER_SCHEME "Bearer " /* required header scheme (case-insensitive!) */ + ++/* ++ * Retrieves the OAUTHBEARER mechanism list (currently a single item). ++ * ++ * For a full description of the API, see libpq/sasl.h. ++ */ +static void +oauth_get_mechanisms(Port *port, StringInfo buf) +{ @@ src/backend/libpq/auth-oauth.c (new) + appendStringInfoChar(buf, '\0'); +} + ++/* ++ * Initializes mechanism state and loads the configured validator module. ++ * ++ * For a full description of the API, see libpq/sasl.h. ++ */ +static void * +oauth_init(Port *port, const char *selected_mech, const char *shadow_pass) +{ @@ src/backend/libpq/auth-oauth.c (new) + return ctx; +} + ++/* ++ * Implements the OAUTHBEARER SASL exchange (RFC 7628, Sec. 3.2). This pulls ++ * apart the client initial response and validates the Bearer token. It also ++ * handles the dummy error response for a failed handshake, as described in ++ * Sec. 3.2.3. ++ * ++ * For a full description of the API, see libpq/sasl.h. ++ */ +static int +oauth_exchange(void *opaq, const char *input, int inputlen, + char **output, int *outputlen, const char **logdetail) @@ src/backend/libpq/auth-oauth.c (new) + return NULL; +} + ++/* ++ * Builds the JSON response for failed authentication (RFC 7628, Sec. 3.2.2). ++ * This contains the required scopes for entry and a pointer to the OAuth/OpenID ++ * discovery document, which the client may use to conduct its OAuth flow. ++ */ +static void +generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen) +{ @@ src/backend/libpq/auth-oauth.c (new) + return token; +} + ++/* ++ * Checks that the "auth" kvpair in the client response contains a syntactically ++ * valid Bearer token, then passes it along to the loaded validator module for ++ * authorization. Returns true if validation succeeds. ++ */ +static bool +validate(Port *port, const char *auth) +{ @@ src/backend/libpq/auth-oauth.c (new) + before_shmem_exit(shutdown_validator_library, 0); +} + ++/* ++ * Call the validator module's shutdown callback, if one is provided. This is ++ * invoked via before_shmem_exit(). ++ */ +static void +shutdown_validator_library(int code, Datum arg) +{ @@ src/backend/libpq/hba.c: parse_hba_line(TokenizedAuthLine *tok_line, int 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"), ++ "map", "delegate_ident_mapping"), + errcontext("line %d of configuration file \"%s\"", + line_num, file_name)); -+ *err_msg = "map cannot be used in combination with trust_validator_authz"; ++ *err_msg = "map cannot be used in combination with delegate_ident_mapping"; + return NULL; + } + } @@ src/backend/libpq/hba.c: parse_hba_auth_opt(char *name, char *val, HbaLine *hbal + REQUIRE_AUTH_OPTION(uaOAuth, "validator", "oauth"); + hbaline->oauth_validator = pstrdup(val); + } -+ else if (strcmp(name, "trust_validator_authz") == 0) ++ else if (strcmp(name, "delegate_ident_mapping") == 0) + { -+ REQUIRE_AUTH_OPTION(uaOAuth, "trust_validator_authz", "oauth"); ++ REQUIRE_AUTH_OPTION(uaOAuth, "delegate_ident_mapping", "oauth"); + if (strcmp(val, "1") == 0) + hbaline->oauth_skip_usermap = true; + else @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) +/*------------------------------------------------------------------------- + * + * fe-auth-oauth-curl.c -+ * The libcurl implementation of OAuth/OIDC authentication. ++ * The libcurl implementation of OAuth/OIDC authentication, using the ++ * OAuth Device Authorization Grant (RFC 8628). + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + */ + +/* States for the overall async machine. */ -+typedef enum ++enum OAuthStep +{ + OAUTH_STEP_INIT = 0, + OAUTH_STEP_DISCOVERY, + OAUTH_STEP_DEVICE_AUTHORIZATION, + OAUTH_STEP_TOKEN_REQUEST, + OAUTH_STEP_WAIT_INTERVAL, -+} OAuthStep; ++}; + +/* + * The async_ctx holds onto state that needs to persist across multiple calls @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + */ +struct async_ctx +{ -+ OAuthStep step; /* where are we in the flow? */ ++ enum OAuthStep step; /* where are we in the flow? */ + +#ifdef HAVE_SYS_EPOLL_H + int timerfd; /* a timerfd for signaling async timeouts */ @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + * JSON Parser Definitions + */ + ++/* ++ * Parses authorization server metadata. Fields are defined by OIDC Discovery ++ * 1.0 and RFC 8414. ++ */ +static bool +parse_provider(struct async_ctx *actx, struct provider *provider) +{ @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + return parsed; +} + ++/* ++ * Parses the Device Authorization Response (RFC 8628, Sec. 3.2). ++ */ +static bool +parse_device_authz(struct async_ctx *actx, struct device_authz *authz) +{ @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + return true; +} + ++/* ++ * Parses the device access token error response (RFC 8628, Sec. 3.5, which ++ * uses the error response defined in RFC 6749, Sec. 5.2). ++ */ +static bool +parse_token_error(struct async_ctx *actx, struct token_error *err) +{ @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + appendPQExpBuffer(&actx->errbuf, "(%s)", err->error); +} + ++/* ++ * Parses the device access token response (RFC 8628, Sec. 3.5, which uses the ++ * success response defined in RFC 6749, Sec. 5.1). ++ */ +static bool +parse_access_token(struct async_ctx *actx, struct token *tok) +{ @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + + /* + * authorization_pending and slow_down are the only acceptable errors; -+ * anything else and we bail. ++ * anything else and we bail. These are defined in RFC 8628, Sec. 3.5. + */ + err = &tok.err; + if (strcmp(err->error, "authorization_pending") != 0 && @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) + + /* + * A slow_down error requires us to permanently increase our retry -+ * interval by five seconds. RFC 8628, Sec. 3.5. ++ * interval by five seconds. + */ + if (strcmp(err->error, "slow_down") == 0) + { @@ src/interfaces/libpq/fe-auth-oauth-curl.c (new) +prompt_user(struct async_ctx *actx, PGconn *conn) +{ + int res; -+ PQpromptOAuthDevice prompt = { ++ PGpromptOAuthDevice prompt = { + .verification_uri = actx->authz.verification_uri, + .user_code = actx->authz.user_code, + /* TODO: optional fields */ @@ src/interfaces/libpq/fe-auth-oauth.c (new) +/*------------------------------------------------------------------------- + * + * fe-auth-oauth.c -+ * The front-end (client) implementation of OAuth/OIDC authentication. ++ * The front-end (client) implementation of OAuth/OIDC authentication ++ * using the SASL OAUTHBEARER mechanism. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California @@ src/interfaces/libpq/fe-auth-oauth.c (new) + oauth_free, +}; + ++/* ++ * Initializes mechanism state for OAUTHBEARER. ++ * ++ * For a full description of the API, see libpq/fe-auth-sasl.h. ++ */ +static void * +oauth_init(PGconn *conn, const char *password, + const char *sasl_mechanism) @@ src/interfaces/libpq/fe-auth-oauth.c (new) + if (!state) + return NULL; + -+ state->state = FE_OAUTH_INIT; ++ state->step = FE_OAUTH_INIT; + state->conn = conn; + + return state; +} + ++/* ++ * Frees the state allocated by oauth_init(). ++ */ ++static void ++oauth_free(void *opaq) ++{ ++ fe_oauth_state *state = opaq; ++ ++ free(state->token); ++ if (state->async_ctx) ++ state->free_async_ctx(state->conn, state->async_ctx); ++ ++ free(state); ++} ++ +#define kvsep "\x01" + +/* @@ src/interfaces/libpq/fe-auth-oauth.c (new) + return response; +} + ++/* ++ * JSON Parser (for the OAUTHBEARER error result) ++ */ ++ ++/* Relevant JSON fields in the error result object. */ +#define ERROR_STATUS_FIELD "status" +#define ERROR_SCOPE_FIELD "scope" +#define ERROR_OPENID_CONFIGURATION_FIELD "openid-configuration" @@ src/interfaces/libpq/fe-auth-oauth.c (new) + return issuer; +} + ++/* ++ * Parses the server error result (RFC 7628, Sec. 3.2.2) contained in msg and ++ * stores any discovered openid_configuration and scope settings for the ++ * connection. conn->oauth_want_retry will be set if the error status is ++ * suitable for a second attempt. ++ */ +static bool +handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen) +{ @@ src/interfaces/libpq/fe-auth-oauth.c (new) + return success; +} + -+static void -+free_request(PGconn *conn, void *vreq) -+{ -+ PQoauthBearerRequest *request = vreq; -+ -+ if (request->cleanup) -+ request->cleanup(conn, request); -+ -+ free(request); -+} -+ ++/* ++ * Callback implementation of conn->async_auth() for a user-defined OAuth flow. ++ * Delegates the retrieval of the token to the application's async callback. ++ * ++ * This will be called multiple times as needed; the application is responsible ++ * for setting an altsock to signal and returning the correct PGRES_POLLING_* ++ * statuses for use by PQconnectPoll(). ++ */ +static PostgresPollingStatusType +run_user_oauth_flow(PGconn *conn, pgsocket *altsock) +{ + fe_oauth_state *state = conn->sasl_state; -+ PQoauthBearerRequest *request = state->async_ctx; ++ PGoauthBearerRequest *request = state->async_ctx; + PostgresPollingStatusType status; + + if (!request->async) @@ src/interfaces/libpq/fe-auth-oauth.c (new) + return status; +} + ++/* ++ * Cleanup callback for the user flow. Delegates most of its job to the ++ * user-provided cleanup implementation. ++ */ ++static void ++free_request(PGconn *conn, void *vreq) ++{ ++ PGoauthBearerRequest *request = vreq; ++ ++ if (request->cleanup) ++ request->cleanup(conn, request); ++ ++ free(request); ++} ++ ++/* ++ * Chooses an OAuth client flow for the connection, which will retrieve a Bearer ++ * token for presentation to the server. ++ * ++ * If the application has registered a custom flow handler using ++ * PQAUTHDATA_OAUTH_BEARER_TOKEN, it may either return a token immediately (e.g. ++ * if it has one cached for immediate use), or set up for a series of ++ * asynchronous callbacks which will be managed by run_user_oauth_flow(). ++ * ++ * If the default handler is used instead, a Device Authorization flow is used ++ * for the connection if support has been compiled in. (See ++ * fe-auth-oauth-curl.c for implementation details.) ++ * ++ * If neither a custom handler nor the builtin flow is available, the connection ++ * fails here. ++ */ +static bool +setup_token_request(PGconn *conn, fe_oauth_state *state) +{ + int res; -+ PQoauthBearerRequest request = { ++ PGoauthBearerRequest request = { + .openid_configuration = conn->oauth_discovery_uri, + .scope = conn->oauth_scope, + }; @@ src/interfaces/libpq/fe-auth-oauth.c (new) + res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request); + if (res > 0) + { -+ PQoauthBearerRequest *request_copy; ++ PGoauthBearerRequest *request_copy; + + if (request.token) + { @@ src/interfaces/libpq/fe-auth-oauth.c (new) + return true; +} + ++/* ++ * Implements the OAUTHBEARER SASL exchange (RFC 7628, Sec. 3.2). ++ * ++ * If the necessary OAuth parameters are set up on the connection, this will run ++ * the client flow asynchronously and present the resulting token to the server. ++ * Otherwise, an empty discovery response will be sent and any parameters sent ++ * back by the server will be stored for a second attempt. ++ * ++ * For a full description of the API, see libpq/sasl.h. ++ */ +static SASLStatus +oauth_exchange(void *opaq, bool final, + char *input, int inputlen, @@ src/interfaces/libpq/fe-auth-oauth.c (new) + *output = NULL; + *outputlen = 0; + -+ switch (state->state) ++ switch (state->step) + { + case FE_OAUTH_INIT: + /* We begin in the initial response phase. */ @@ src/interfaces/libpq/fe-auth-oauth.c (new) + */ + Assert(conn->async_auth); /* should have been set + * already */ -+ state->state = FE_OAUTH_REQUESTING_TOKEN; ++ state->step = FE_OAUTH_REQUESTING_TOKEN; + return SASL_ASYNC; + } + } @@ src/interfaces/libpq/fe-auth-oauth.c (new) + return SASL_FAILED; + + *outputlen = strlen(*output); -+ state->state = FE_OAUTH_BEARER_SENT; ++ state->step = FE_OAUTH_BEARER_SENT; + + return SASL_CONTINUE; + @@ src/interfaces/libpq/fe-auth-oauth.c (new) + } + *outputlen = strlen(*output); /* == 1 */ + -+ state->state = FE_OAUTH_SERVER_ERROR; ++ state->step = FE_OAUTH_SERVER_ERROR; + return SASL_CONTINUE; + + case FE_OAUTH_SERVER_ERROR: @@ src/interfaces/libpq/fe-auth-oauth.c (new) + return false; +} + -+static void -+oauth_free(void *opaq) -+{ -+ fe_oauth_state *state = opaq; -+ -+ free(state->token); -+ if (state->async_ctx) -+ state->free_async_ctx(state->conn, state->async_ctx); -+ -+ free(state); -+} -+ +/* + * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment. + */ @@ src/interfaces/libpq/fe-auth-oauth.h (new) +#include "libpq-int.h" + + -+typedef enum ++enum fe_oauth_step +{ + FE_OAUTH_INIT, + FE_OAUTH_REQUESTING_TOKEN, + FE_OAUTH_BEARER_SENT, + FE_OAUTH_SERVER_ERROR, -+} fe_oauth_state_enum; ++}; + +typedef struct +{ -+ fe_oauth_state_enum state; ++ enum fe_oauth_step step; + + PGconn *conn; + char *token; @@ src/interfaces/libpq/fe-auth-oauth.h (new) +extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock); +extern bool oauth_unsafe_debugging_enabled(void); + ++/* Mechanisms in fe-auth-oauth.c */ ++extern const pg_fe_sasl_mech pg_oauth_mech; ++ +#endif /* FE_AUTH_OAUTH_H */ ## src/interfaces/libpq/fe-auth-sasl.h ## @@ src/interfaces/libpq/fe-auth.c #include "common/scram-common.h" #include "fe-auth.h" #include "fe-auth-sasl.h" ++#include "fe-auth-oauth.h" + #include "libpq-fe.h" + + #ifdef ENABLE_GSS @@ src/interfaces/libpq/fe-auth.c: pg_SSPI_startup(PGconn *conn, int use_negotiate, int payloadlen) * Initialize SASL authentication exchange. */ @@ src/interfaces/libpq/fe-auth.c: PQchangePassword(PGconn *conn, const char *user, +} + +int -+PQdefaultAuthDataHook(PGAuthData type, PGconn *conn, void *data) ++PQdefaultAuthDataHook(PGauthData type, PGconn *conn, void *data) +{ + return 0; /* handle nothing */ +} @@ src/interfaces/libpq/fe-auth.h extern char *pg_fe_getusername(uid_t user_id, PQExpBuffer errorMessage); extern char *pg_fe_getauthname(PQExpBuffer errorMessage); -@@ src/interfaces/libpq/fe-auth.h: extern char *pg_fe_scram_build_secret(const char *password, - int iterations, - const char **errstr); - -+/* Mechanisms in fe-auth-oauth.c */ -+extern const pg_fe_sasl_mech pg_oauth_mech; -+ - #endif /* FE_AUTH_H */ ## src/interfaces/libpq/fe-connect.c ## +@@ + #include "common/scram-common.h" + #include "common/string.h" + #include "fe-auth.h" ++#include "fe-auth-oauth.h" + #include "libpq-fe.h" + #include "libpq-int.h" + #include "mb/pg_wchar.h" @@ src/interfaces/libpq/fe-connect.c: static const internalPQconninfoOption PQconninfoOptions[] = { "Load-Balance-Hosts", "", 8, /* sizeof("disable") = 8 */ offsetof(struct pg_conn, load_balance_hosts)}, @@ src/interfaces/libpq/libpq-fe.h: typedef enum + PQAUTHDATA_PROMPT_OAUTH_DEVICE, /* user must visit a device-authorization + * URL */ + PQAUTHDATA_OAUTH_BEARER_TOKEN, /* server requests an OAuth Bearer token */ -+} PGAuthData; ++} PGauthData; + /* PGconn encapsulates a connection to the backend. * The contents of this struct are not supposed to be known to applications. @@ src/interfaces/libpq/libpq-fe.h: extern int PQenv2encoding(void); /* === in fe-auth.c === */ -+typedef struct _PQpromptOAuthDevice ++typedef struct _PGpromptOAuthDevice +{ + const char *verification_uri; /* verification URI to visit */ + const char *user_code; /* user code to enter */ -+} PQpromptOAuthDevice; ++} PGpromptOAuthDevice; + -+/* for _PQoauthBearerRequest.async() */ ++/* for PGoauthBearerRequest.async() */ +#ifdef WIN32 +#define SOCKTYPE SOCKET +#else +#define SOCKTYPE int +#endif + -+typedef struct _PQoauthBearerRequest ++typedef struct _PGoauthBearerRequest +{ + /* Hook inputs (constant across all calls) */ + const char *const openid_configuration; /* OIDC discovery URI */ @@ src/interfaces/libpq/libpq-fe.h: extern int PQenv2encoding(void); + * request->token must be set by the hook. + */ + PostgresPollingStatusType (*async) (PGconn *conn, -+ struct _PQoauthBearerRequest *request, ++ struct _PGoauthBearerRequest *request, + SOCKTYPE *altsock); + + /* @@ src/interfaces/libpq/libpq-fe.h: extern int PQenv2encoding(void); + * This is technically optional, but highly recommended, because there is + * no other indication as to when it is safe to free the token. + */ -+ void (*cleanup) (PGconn *conn, struct _PQoauthBearerRequest *request); ++ void (*cleanup) (PGconn *conn, struct _PGoauthBearerRequest *request); + + /* + * The hook should set this to the Bearer token contents for the @@ src/interfaces/libpq/libpq-fe.h: extern int PQenv2encoding(void); + * the cleanup callback. + */ + void *user; -+} PQoauthBearerRequest; ++} PGoauthBearerRequest; + +#undef SOCKTYPE + @@ src/interfaces/libpq/libpq-fe.h: extern int PQenv2encoding(void); extern char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm); extern PGresult *PQchangePassword(PGconn *conn, const char *user, const char *passwd); -+typedef int (*PQauthDataHook_type) (PGAuthData type, PGconn *conn, void *data); ++typedef int (*PQauthDataHook_type) (PGauthData type, PGconn *conn, void *data); +extern void PQsetAuthDataHook(PQauthDataHook_type hook); +extern PQauthDataHook_type PQgetAuthDataHook(void); -+extern int PQdefaultAuthDataHook(PGAuthData type, PGconn *conn, void *data); ++extern int PQdefaultAuthDataHook(PGauthData type, PGconn *conn, void *data); + /* === in encnames.c === */ extern int pg_char_to_encoding(const char *name); ## src/interfaces/libpq/libpq-int.h ## -@@ src/interfaces/libpq/libpq-int.h: typedef struct pg_conn_host - * found in password file. */ - } pg_conn_host; - -+typedef PostgresPollingStatusType (*AsyncAuthFunc) (PGconn *conn, pgsocket *altsock); -+ - /* - * PGconn stores all the state data associated with a single connection - * to a backend. @@ src/interfaces/libpq/libpq-int.h: struct pg_conn * cancel request, instead of being a normal * connection that's used for queries */ @@ src/interfaces/libpq/libpq-int.h: struct pg_conn * know which auth response we're * sending */ -+ AsyncAuthFunc async_auth; /* callback for external async authentication */ ++ /* Callback for external async authentication */ ++ PostgresPollingStatusType (*async_auth) (PGconn *conn, pgsocket *altsock); + pgsocket altsock; /* alternative socket for client to poll */ + + @@ src/test/modules/oauth_validator/Makefile (new) + +endif + ## src/test/modules/oauth_validator/README (new) ## +@@ ++Test programs and libraries for OAuth ++------------------------------------- ++ ++This folder contains tests for the client- and server-side OAuth ++implementations. Most tests are run end-to-end to test both simultaneously. The ++tests in t/001_server use a mock OAuth authorization server, implemented jointly ++by t/OAuth/Server.pm and t/oauth_server.py, to run the libpq Device ++Authorization flow. The tests in t/002_client exercise custom OAuth flows and ++don't need an authorization server. ++ ++Tests in this folder generally require 'oauth' to be present in PG_TEST_EXTRA, ++since localhost HTTP servers will be started. A Python installation is required ++to run the mock authorization server. + ## src/test/modules/oauth_validator/fail_validator.c (new) ## @@ +/*------------------------------------------------------------------------- @@ src/test/modules/oauth_validator/fail_validator.c (new) + const char *token, + const char *role); + ++/* Callback implementations (we only need the main one) */ +static const OAuthValidatorCallbacks validator_callbacks = { + .validate_cb = fail_token, +}; @@ src/test/modules/oauth_validator/oauth_hook_client.c (new) +/*------------------------------------------------------------------------- + * + * oauth_hook_client.c -+ * Verify OAuth hook functionality in libpq ++ * Test driver for t/002_client.pl, which verifies OAuth hook ++ * functionality in libpq. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California @@ src/test/modules/oauth_validator/oauth_hook_client.c (new) +#include "getopt_long.h" +#include "libpq-fe.h" + -+static int handle_auth_data(PGAuthData type, PGconn *conn, void *data); ++static int handle_auth_data(PGauthData type, PGconn *conn, void *data); + +static void +usage(char *argv[]) @@ src/test/modules/oauth_validator/oauth_hook_client.c (new) + return 0; +} + ++/* ++ * PQauthDataHook implementation. Replaces the default client flow by handling ++ * PQAUTHDATA_OAUTH_BEARER_TOKEN. ++ */ +static int -+handle_auth_data(PGAuthData type, PGconn *conn, void *data) ++handle_auth_data(PGauthData type, PGconn *conn, void *data) +{ -+ PQoauthBearerRequest *req = data; ++ PGoauthBearerRequest *req = data; + + if (no_hook || (type != PQAUTHDATA_OAUTH_BEARER_TOKEN)) + return 0; @@ src/test/modules/oauth_validator/oauth_hook_client.c (new) ## src/test/modules/oauth_validator/t/001_server.pl (new) ## @@ + ++# ++# Tests the libpq builtin OAuth flow, as well as server-side HBA and validator ++# setup. ++# +# Copyright (c) 2021-2024, PostgreSQL Global Development Group ++# + +use strict; +use warnings FATAL => 'all'; @@ src/test/modules/oauth_validator/t/001_server.pl (new) +use MIME::Base64 qw(encode_base64); +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; -+use PostgreSQL::Test::OAuthServer; +use Test::More; + ++use FindBin; ++use lib $FindBin::RealBin; ++ ++use OAuth::Server; ++ +if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\boauth\b/) +{ + plan skip_all => @@ src/test/modules/oauth_validator/t/001_server.pl (new) +# Save a background connection for later configuration changes. +my $bgconn = $node->background_psql('postgres'); + -+my $webserver = PostgreSQL::Test::OAuthServer->new(); ++my $webserver = OAuth::Server->new(); +$webserver->run(); + +END @@ src/test/modules/oauth_validator/t/001_server.pl (new) +# things up; hardcode the discovery URI. (Scope is hardcoded to empty to cover +# that case as well.) +$common_connstr = -+ "user=test dbname=postgres oauth_issuer=$issuer/.well-known/openid-configuration oauth_scope=''"; ++ "dbname=postgres oauth_issuer=$issuer/.well-known/openid-configuration oauth_scope='' oauth_client_id=f02c6361-0635"; + +$bgconn->query_safe("ALTER SYSTEM SET oauth_validator.authn_id TO ''"); +$node->reload; @@ src/test/modules/oauth_validator/t/001_server.pl (new) + $node->wait_for_log(qr/reloading configuration files/, $log_start); + +if ($node->connect_fails( -+ "$common_connstr oauth_client_id=f02c6361-0635", ++ "$common_connstr user=test", + "validator must set authn_id", + expected_stderr => qr/OAuth bearer authentication failed/)) +{ @@ src/test/modules/oauth_validator/t/001_server.pl (new) + $log_start = $log_end; +} + ++# ++# Test user mapping. ++# ++ ++# Allow "user@example.com" to log in under the test role. ++unlink($node->data_dir . '/pg_ident.conf'); ++$node->append_conf( ++ 'pg_ident.conf', qq{ ++oauthmap user\@example.com test ++}); ++ ++# test and testalt use the map; testparam uses ident delegation. ++unlink($node->data_dir . '/pg_hba.conf'); ++$node->append_conf( ++ 'pg_hba.conf', qq{ ++local all test oauth issuer="$issuer" scope="" map=oauthmap ++local all testalt oauth issuer="$issuer" scope="" map=oauthmap ++local all testparam oauth issuer="$issuer" scope="" delegate_ident_mapping=1 ++}); ++ ++# To start, have the validator use the role names as authn IDs. ++$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authn_id"); ++ ++$node->reload; ++$log_start = ++ $node->wait_for_log(qr/reloading configuration files/, $log_start); ++ ++# The test and testalt roles should no longer map correctly. ++$node->connect_fails( ++ "$common_connstr user=test", ++ "mismatched username map (test)", ++ expected_stderr => qr/OAuth bearer authentication failed/); ++$node->connect_fails( ++ "$common_connstr user=testalt", ++ "mismatched username map (testalt)", ++ expected_stderr => qr/OAuth bearer authentication failed/); ++ ++# Have the validator identify the end user as user@example.com. ++$bgconn->query_safe( ++ "ALTER SYSTEM SET oauth_validator.authn_id TO 'user\@example.com'"); ++$node->reload; ++$log_start = ++ $node->wait_for_log(qr/reloading configuration files/, $log_start); ++ ++# Now the test role can be logged into. (testalt still can't be mapped.) ++$node->connect_ok( ++ "$common_connstr user=test", ++ "matched username map (test)", ++ expected_stderr => ++ qr@Visit https://example\.com/ and enter the code: postgresuser@); ++$node->connect_fails( ++ "$common_connstr user=testalt", ++ "mismatched username map (testalt)", ++ expected_stderr => qr/OAuth bearer authentication failed/); ++ ++# testparam ignores the map entirely. ++$node->connect_ok( ++ "$common_connstr user=testparam", ++ "delegated ident (testparam)", ++ expected_stderr => ++ qr@Visit https://example\.com/ and enter the code: postgresuser@); ++ +$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authn_id"); +$node->reload; +$log_start = @@ src/test/modules/oauth_validator/t/001_server.pl (new) ## src/test/modules/oauth_validator/t/002_client.pl (new) ## @@ -+ ++# ++# Exercises the API for custom OAuth client flows, using the oauth_hook_client ++# test driver. ++# +# Copyright (c) 2021-2024, PostgreSQL Global Development Group ++# + +use strict; +use warnings FATAL => 'all'; @@ src/test/modules/oauth_validator/t/002_client.pl (new) + } + + my @cmd = ("oauth_hook_client", @{$flags}, $common_connstr); -+ diag "running '" . join("' '", @cmd) . "'"; ++ note "running '" . join("' '", @cmd) . "'"; + + my ($stdout, $stderr) = run_command(\@cmd); + @@ src/test/modules/oauth_validator/t/002_client.pl (new) + +done_testing(); + ## src/test/modules/oauth_validator/t/OAuth/Server.pm (new) ## +@@ ++ ++# Copyright (c) 2024, PostgreSQL Global Development Group ++ ++=pod ++ ++=head1 NAME ++ ++OAuth::Server - runs a mock OAuth authorization server for testing ++ ++=head1 SYNOPSIS ++ ++ use OAuth::Server; ++ ++ my $server = OAuth::Server->new(); ++ $server->run; ++ ++ my $port = $server->port; ++ my $issuer = "http://localhost:$port"; ++ ++ # test against $issuer... ++ ++ $server->stop; ++ ++=head1 DESCRIPTION ++ ++This is glue API between the Perl tests and the Python authorization server ++daemon implemented in t/oauth_server.py. (Python has a fairly usable HTTP server ++in its standard library, so the implementation was ported from Perl.) ++ ++This authorization server does not use TLS (it implements a nonstandard, unsafe ++issuer at "http://localhost:"), so libpq in particular will need to set ++PGOAUTHDEBUG=UNSAFE to be able to talk to it. ++ ++=cut ++ ++package OAuth::Server; ++ ++use warnings; ++use strict; ++use Scalar::Util; ++use Test::More; ++ ++=pod ++ ++=head1 METHODS ++ ++=over ++ ++=item SSL::Server->new() ++ ++Create a new OAuth Server object. ++ ++=cut ++ ++sub new ++{ ++ my $class = shift; ++ ++ my $self = {}; ++ bless($self, $class); ++ ++ return $self; ++} ++ ++=pod ++ ++=item $server->port() ++ ++Returns the port in use by the server. ++ ++=cut ++ ++sub port ++{ ++ my $self = shift; ++ ++ return $self->{'port'}; ++} ++ ++=pod ++ ++=item $server->run() ++ ++Runs the authorization server daemon in t/oauth_server.py. ++ ++=cut ++ ++sub run ++{ ++ my $self = shift; ++ my $port; ++ ++ my $pid = open(my $read_fh, "-|", $ENV{PYTHON}, "t/oauth_server.py") ++ or die "failed to start OAuth server: $!"; ++ ++ # Get the port number from the daemon. It closes stdout afterwards; that way ++ # we can slurp in the entire contents here rather than worrying about the ++ # number of bytes to read. ++ $port = do { local $/ = undef; <$read_fh> } ++ // die "failed to read port number: $!"; ++ chomp $port; ++ die "server did not advertise a valid port" ++ unless Scalar::Util::looks_like_number($port); ++ ++ $self->{'pid'} = $pid; ++ $self->{'port'} = $port; ++ $self->{'child'} = $read_fh; ++ ++ note("OAuth provider (PID $pid) is listening on port $port\n"); ++} ++ ++=pod ++ ++=item $server->stop() ++ ++Sends SIGTERM to the authorization server and waits for it to exit. ++ ++=cut ++ ++sub stop ++{ ++ my $self = shift; ++ ++ note("Sending SIGTERM to OAuth provider PID: $self->{'pid'}\n"); ++ ++ kill(15, $self->{'pid'}); ++ $self->{'pid'} = undef; ++ ++ # Closing the popen() handle waits for the process to exit. ++ close($self->{'child'}); ++ $self->{'child'} = undef; ++} ++ ++=pod ++ ++=back ++ ++=cut ++ ++1; + ## src/test/modules/oauth_validator/t/oauth_server.py (new) ## @@ +#! /usr/bin/env python3 ++# ++# A mock OAuth authorization server, designed to be invoked from ++# OAuth/Server.pm. This listens on an ephemeral port number (printed to stdout ++# so that the Perl tests can contact it) and runs as a daemon until it is ++# signaled. ++# + +import base64 +import http.server @@ src/test/modules/oauth_validator/t/oauth_server.py (new) + + +class OAuthHandler(http.server.BaseHTTPRequestHandler): ++ """ ++ Core implementation of the authorization server. The API is ++ inheritance-based, with entry points at do_GET() and do_POST(). See the ++ documentation for BaseHTTPRequestHandler. ++ """ ++ + JsonObject = dict[str, object] # TypeAlias is not available until 3.10 + + def _check_issuer(self): @@ src/test/modules/oauth_validator/t/oauth_server.py (new) + + +def main(): ++ """ ++ Starts the authorization server on localhost. The ephemeral port in use will ++ be printed to stdout. ++ """ ++ + s = http.server.HTTPServer(("127.0.0.1", 0), OAuthHandler) + + # Attach a "cache" dictionary to the server to allow the OAuthHandlers to @@ src/test/modules/oauth_validator/t/oauth_server.py (new) + port = s.socket.getsockname()[1] + print(port) + ++ # stdout is closed to allow the parent to just "read to the end". + stdout = sys.stdout.fileno() + sys.stdout.close() + os.close(stdout) @@ src/test/modules/oauth_validator/validator.c (new) + const char *token, + const char *role); + ++/* Callback implementations (exercise all three) */ +static const OAuthValidatorCallbacks validator_callbacks = { + .startup_cb = validator_startup, + .shutdown_cb = validator_shutdown, @@ src/test/modules/oauth_validator/validator.c (new) + +static char *authn_id = NULL; + ++/*--- ++ * Extension entry point. Sets up GUCs for use by tests: ++ * ++ * - oauth_validator.authn_id Sets the user identifier to return during token ++ * validation. Defaults to the username in the ++ * startup packet. ++ */ +void +_PG_init(void) +{ @@ src/test/modules/oauth_validator/validator.c (new) + MarkGUCPrefixReserved("oauth_validator"); +} + ++/* ++ * Validator module entry point. ++ */ +const OAuthValidatorCallbacks * +_PG_oauth_validator_module_init(void) +{ @@ src/test/modules/oauth_validator/validator.c (new) + +#define PRIVATE_COOKIE ((void *) 13579) + ++/* ++ * Startup callback, to set up private data for the validator. ++ */ +static void +validator_startup(ValidatorModuleState *state) +{ + state->private_data = PRIVATE_COOKIE; +} + ++/* ++ * Shutdown callback, to tear down the validator. ++ */ +static void +validator_shutdown(ValidatorModuleState *state) +{ @@ src/test/modules/oauth_validator/validator.c (new) + state->private_data); +} + ++/* ++ * Validator implementation. Logs the incoming data and authorizes the token; ++ * 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/perl/PostgreSQL/Test/Cluster.pm: sub connect_ok $self->log_check($test_name, $log_location, %params); } - ## src/test/perl/PostgreSQL/Test/OAuthServer.pm (new) ## -@@ -+#!/usr/bin/perl -+ -+package PostgreSQL::Test::OAuthServer; -+ -+use warnings; -+use strict; -+use Scalar::Util; -+use Socket; -+use IO::Select; -+use Test::More; -+ -+local *server_socket; -+ -+sub new -+{ -+ my $class = shift; -+ -+ my $self = {}; -+ bless($self, $class); -+ -+ return $self; -+} -+ -+sub port -+{ -+ my $self = shift; -+ -+ return $self->{'port'}; -+} -+ -+sub run -+{ -+ my $self = shift; -+ my $port; -+ -+ my $pid = open(my $read_fh, "-|", $ENV{PYTHON}, "t/oauth_server.py") -+ or die "failed to start OAuth server: $!"; -+ -+ read($read_fh, $port, 7) // die "failed to read port number: $!"; -+ chomp $port; -+ die "server did not advertise a valid port" -+ unless Scalar::Util::looks_like_number($port); -+ -+ $self->{'pid'} = $pid; -+ $self->{'port'} = $port; -+ $self->{'child'} = $read_fh; -+ -+ note("OAuth provider (PID $pid) is listening on port $port\n"); -+} -+ -+sub stop -+{ -+ my $self = shift; -+ -+ note("Sending SIGTERM to OAuth provider PID: $self->{'pid'}\n"); -+ -+ kill(15, $self->{'pid'}); -+ $self->{'pid'} = undef; -+ -+ # Closing the popen() handle waits for the process to exit. -+ close($self->{'child'}); -+ $self->{'child'} = undef; -+} -+ -+1; - ## src/tools/pgindent/pgindent ## @@ src/tools/pgindent/pgindent: sub pre_indent # Protect wrapping in CATALOG() @@ src/tools/pgindent/pgindent: sub post_indent # Undo change of dash-protected block comments ## src/tools/pgindent/typedefs.list ## -@@ src/tools/pgindent/typedefs.list: ArrayMetaState - ArraySubWorkspace - ArrayToken - ArrayType -+AsyncAuthFunc - AsyncQueueControl - AsyncQueueEntry - AsyncRequest @@ src/tools/pgindent/typedefs.list: CState CTECycleClause CTEMaterialize @@ src/tools/pgindent/typedefs.list: NumericDigit NumericSortSupport NumericSumAccum NumericVar -+OAuthStep +OAuthValidatorCallbacks OM_uint32 OP OSAPerGroupState -@@ src/tools/pgindent/typedefs.list: PFN - PGAlignedBlock - PGAlignedXLogBlock - PGAsyncStatusType -+PGAuthData - PGCALL2 - PGChecksummablePage - PGContextVisibility +@@ src/tools/pgindent/typedefs.list: PGVerbosity + PG_Locale_Strategy + PG_Lock_Status + PG_init_t ++PGauthData + PGcancel + PGcancelConn + PGcmdQueueEntry +@@ src/tools/pgindent/typedefs.list: PGconn + PGdataValue + PGlobjfuncs + PGnotify ++PGoauthBearerRequest + PGpipelineStatus ++PGpromptOAuthDevice + PGresAttDesc + PGresAttValue + PGresParamDesc @@ src/tools/pgindent/typedefs.list: PQArgBlock PQEnvironmentOption PQExpBuffer @@ src/tools/pgindent/typedefs.list: PQArgBlock PQcommMethods PQconninfoOption PQnoticeProcessor - PQnoticeReceiver -+PQoauthBearerRequest - PQprintOpt -+PQpromptOAuthDevice - PQsslKeyPassHook_OpenSSL_type - PREDICATELOCK - PREDICATELOCKTAG @@ src/tools/pgindent/typedefs.list: VacuumRelation VacuumStmt ValidIOData @@ src/tools/pgindent/typedefs.list: explain_get_index_name_hook_type fasthash_state fd_set +fe_oauth_state -+fe_oauth_state_enum fe_scram_state fe_scram_state_enum fetch_range_request 2: 28cc3463aaf ! 2: 566d90d30a7 DO NOT MERGE: Add pytest suite for OAuth @@ src/test/python/client/test_oauth.py (new) +PGRES_POLLING_OK = 3 + + -+class PQPromptOAuthDevice(ctypes.Structure): ++class PGPromptOAuthDevice(ctypes.Structure): + _fields_ = [ + ("verification_uri", ctypes.c_char_p), + ("user_code", ctypes.c_char_p), + ] + + -+class PQOAuthBearerRequest(ctypes.Structure): ++class PGOAuthBearerRequest(ctypes.Structure): + pass + + -+PQOAuthBearerRequest._fields_ = [ ++PGOAuthBearerRequest._fields_ = [ + ("openid_configuration", ctypes.c_char_p), + ("scope", ctypes.c_char_p), + ( @@ src/test/python/client/test_oauth.py (new) + ctypes.CFUNCTYPE( + ctypes.c_int, + ctypes.c_void_p, -+ ctypes.POINTER(PQOAuthBearerRequest), ++ ctypes.POINTER(PGOAuthBearerRequest), + ctypes.POINTER(ctypes.c_int), + ), + ), + ( + "cleanup", -+ ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PQOAuthBearerRequest)), ++ ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PGOAuthBearerRequest)), + ), + ("token", ctypes.c_char_p), + ("user", ctypes.c_void_p), @@ src/test/python/client/test_oauth.py (new) + handle_by_default = 0 # does an implementation have to be provided? + + if typ == PQAUTHDATA_PROMPT_OAUTH_DEVICE: -+ cls = PQPromptOAuthDevice ++ cls = PGPromptOAuthDevice + handle_by_default = 1 + elif typ == PQAUTHDATA_OAUTH_BEARER_TOKEN: -+ cls = PQOAuthBearerRequest ++ cls = PGOAuthBearerRequest + else: + return 0 + @@ src/test/python/client/test_oauth.py (new) + @ctypes.CFUNCTYPE( + ctypes.c_int, + ctypes.c_void_p, -+ ctypes.POINTER(PQOAuthBearerRequest), ++ ctypes.POINTER(PGOAuthBearerRequest), + ctypes.POINTER(ctypes.c_int), + ) + def get_token_wrapper(pgconn, p_request, p_altsock): @@ src/test/python/client/test_oauth.py (new) + logging.error("Exception during async callback:\n" + traceback.format_exc()) + return PGRES_POLLING_FAILED + -+ @ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PQOAuthBearerRequest)) ++ @ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PGOAuthBearerRequest)) + def cleanup(pgconn, p_request): + """ + Should be called exactly once per connection. @@ src/test/python/server/test_oauth.py (new) + ctx = Context() + hba_lines = [ + f'host {ctx.dbname} {ctx.map_user} samehost oauth issuer="{ctx.issuer}" scope="{ctx.scope}" map=oauth\n', -+ f'host {ctx.dbname} {ctx.authz_user} samehost oauth issuer="{ctx.issuer}" scope="{ctx.scope}" trust_validator_authz=1\n', ++ f'host {ctx.dbname} {ctx.authz_user} samehost oauth issuer="{ctx.issuer}" scope="{ctx.scope}" delegate_ident_mapping=1\n', + f'host {ctx.dbname} all samehost oauth issuer="{ctx.issuer}" scope="{ctx.scope}"\n', + ] + ident_lines = [r"oauth /^(.*)@example\.com$ \1"]