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"]