aboutsummaryrefslogtreecommitdiff
path: root/src/interfaces/libpq/fe-auth-oauth.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/interfaces/libpq/fe-auth-oauth.c')
-rw-r--r--src/interfaces/libpq/fe-auth-oauth.c1163
1 files changed, 1163 insertions, 0 deletions
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
new file mode 100644
index 00000000000..fb1e9a1a8aa
--- /dev/null
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -0,0 +1,1163 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-auth-oauth.c
+ * The front-end (client) implementation of OAuth/OIDC authentication
+ * using the SASL OAUTHBEARER mechanism.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/fe-auth-oauth.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "common/base64.h"
+#include "common/hmac.h"
+#include "common/jsonapi.h"
+#include "common/oauth-common.h"
+#include "fe-auth.h"
+#include "fe-auth-oauth.h"
+#include "mb/pg_wchar.h"
+
+/* The exported OAuth callback mechanism. */
+static void *oauth_init(PGconn *conn, const char *password,
+ const char *sasl_mechanism);
+static SASLStatus oauth_exchange(void *opaq, bool final,
+ char *input, int inputlen,
+ char **output, int *outputlen);
+static bool oauth_channel_bound(void *opaq);
+static void oauth_free(void *opaq);
+
+const pg_fe_sasl_mech pg_oauth_mech = {
+ oauth_init,
+ oauth_exchange,
+ oauth_channel_bound,
+ 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)
+{
+ fe_oauth_state *state;
+
+ /*
+ * We only support one SASL mechanism here; anything else is programmer
+ * error.
+ */
+ Assert(sasl_mechanism != NULL);
+ Assert(strcmp(sasl_mechanism, OAUTHBEARER_NAME) == 0);
+
+ state = calloc(1, sizeof(*state));
+ if (!state)
+ return NULL;
+
+ state->step = FE_OAUTH_INIT;
+ state->conn = conn;
+
+ return state;
+}
+
+/*
+ * Frees the state allocated by oauth_init().
+ *
+ * This handles only mechanism state tied to the connection lifetime; state
+ * stored in state->async_ctx is freed up either immediately after the
+ * authentication handshake succeeds, or before the mechanism is cleaned up on
+ * failure. See pg_fe_cleanup_oauth_flow() and cleanup_user_oauth_flow().
+ */
+static void
+oauth_free(void *opaq)
+{
+ fe_oauth_state *state = opaq;
+
+ /* Any async authentication state should have been cleaned up already. */
+ Assert(!state->async_ctx);
+
+ free(state);
+}
+
+#define kvsep "\x01"
+
+/*
+ * Constructs an OAUTHBEARER client initial response (RFC 7628, Sec. 3.1).
+ *
+ * If discover is true, the initial response will contain a request for the
+ * server's required OAuth parameters (Sec. 4.3). Otherwise, conn->token must
+ * be set; it will be sent as the connection's bearer token.
+ *
+ * Returns the response as a null-terminated string, or NULL on error.
+ */
+static char *
+client_initial_response(PGconn *conn, bool discover)
+{
+ static const char *const resp_format = "n,," kvsep "auth=%s%s" kvsep kvsep;
+
+ PQExpBufferData buf;
+ const char *authn_scheme;
+ char *response = NULL;
+ const char *token = conn->oauth_token;
+
+ if (discover)
+ {
+ /* Parameter discovery uses a completely empty auth value. */
+ authn_scheme = token = "";
+ }
+ else
+ {
+ /*
+ * Use a Bearer authentication scheme (RFC 6750, Sec. 2.1). A trailing
+ * space is used as a separator.
+ */
+ authn_scheme = "Bearer ";
+
+ /* conn->token must have been set in this case. */
+ if (!token)
+ {
+ Assert(false);
+ libpq_append_conn_error(conn,
+ "internal error: no OAuth token was set for the connection");
+ return NULL;
+ }
+ }
+
+ initPQExpBuffer(&buf);
+ appendPQExpBuffer(&buf, resp_format, authn_scheme, token);
+
+ if (!PQExpBufferDataBroken(buf))
+ response = strdup(buf.data);
+ termPQExpBuffer(&buf);
+
+ if (!response)
+ libpq_append_conn_error(conn, "out of memory");
+
+ 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"
+
+struct json_ctx
+{
+ char *errmsg; /* any non-NULL value stops all processing */
+ PQExpBufferData errbuf; /* backing memory for errmsg */
+ int nested; /* nesting level (zero is the top) */
+
+ const char *target_field_name; /* points to a static allocation */
+ char **target_field; /* see below */
+
+ /* target_field, if set, points to one of the following: */
+ char *status;
+ char *scope;
+ char *discovery_uri;
+};
+
+#define oauth_json_has_error(ctx) \
+ (PQExpBufferDataBroken((ctx)->errbuf) || (ctx)->errmsg)
+
+#define oauth_json_set_error(ctx, ...) \
+ do { \
+ appendPQExpBuffer(&(ctx)->errbuf, __VA_ARGS__); \
+ (ctx)->errmsg = (ctx)->errbuf.data; \
+ } while (0)
+
+static JsonParseErrorType
+oauth_json_object_start(void *state)
+{
+ struct json_ctx *ctx = state;
+
+ if (ctx->target_field)
+ {
+ Assert(ctx->nested == 1);
+
+ oauth_json_set_error(ctx,
+ libpq_gettext("field \"%s\" must be a string"),
+ ctx->target_field_name);
+ }
+
+ ++ctx->nested;
+ return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
+}
+
+static JsonParseErrorType
+oauth_json_object_end(void *state)
+{
+ struct json_ctx *ctx = state;
+
+ --ctx->nested;
+ return JSON_SUCCESS;
+}
+
+static JsonParseErrorType
+oauth_json_object_field_start(void *state, char *name, bool isnull)
+{
+ struct json_ctx *ctx = state;
+
+ /* Only top-level keys are considered. */
+ if (ctx->nested == 1)
+ {
+ if (strcmp(name, ERROR_STATUS_FIELD) == 0)
+ {
+ ctx->target_field_name = ERROR_STATUS_FIELD;
+ ctx->target_field = &ctx->status;
+ }
+ else if (strcmp(name, ERROR_SCOPE_FIELD) == 0)
+ {
+ ctx->target_field_name = ERROR_SCOPE_FIELD;
+ ctx->target_field = &ctx->scope;
+ }
+ else if (strcmp(name, ERROR_OPENID_CONFIGURATION_FIELD) == 0)
+ {
+ ctx->target_field_name = ERROR_OPENID_CONFIGURATION_FIELD;
+ ctx->target_field = &ctx->discovery_uri;
+ }
+ }
+
+ return JSON_SUCCESS;
+}
+
+static JsonParseErrorType
+oauth_json_array_start(void *state)
+{
+ struct json_ctx *ctx = state;
+
+ if (!ctx->nested)
+ {
+ ctx->errmsg = libpq_gettext("top-level element must be an object");
+ }
+ else if (ctx->target_field)
+ {
+ Assert(ctx->nested == 1);
+
+ oauth_json_set_error(ctx,
+ libpq_gettext("field \"%s\" must be a string"),
+ ctx->target_field_name);
+ }
+
+ return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
+}
+
+static JsonParseErrorType
+oauth_json_scalar(void *state, char *token, JsonTokenType type)
+{
+ struct json_ctx *ctx = state;
+
+ if (!ctx->nested)
+ {
+ ctx->errmsg = libpq_gettext("top-level element must be an object");
+ return JSON_SEM_ACTION_FAILED;
+ }
+
+ if (ctx->target_field)
+ {
+ if (ctx->nested != 1)
+ {
+ /*
+ * ctx->target_field should not have been set for nested keys.
+ * Assert and don't continue any further for production builds.
+ */
+ Assert(false);
+ oauth_json_set_error(ctx,
+ "internal error: target scalar found at nesting level %d during OAUTHBEARER parsing",
+ ctx->nested);
+ return JSON_SEM_ACTION_FAILED;
+ }
+
+ /*
+ * We don't allow duplicate field names; error out if the target has
+ * already been set.
+ */
+ if (*ctx->target_field)
+ {
+ oauth_json_set_error(ctx,
+ libpq_gettext("field \"%s\" is duplicated"),
+ ctx->target_field_name);
+ return JSON_SEM_ACTION_FAILED;
+ }
+
+ /* The only fields we support are strings. */
+ if (type != JSON_TOKEN_STRING)
+ {
+ oauth_json_set_error(ctx,
+ libpq_gettext("field \"%s\" must be a string"),
+ ctx->target_field_name);
+ return JSON_SEM_ACTION_FAILED;
+ }
+
+ *ctx->target_field = strdup(token);
+ if (!*ctx->target_field)
+ return JSON_OUT_OF_MEMORY;
+
+ ctx->target_field = NULL;
+ ctx->target_field_name = NULL;
+ }
+ else
+ {
+ /* otherwise we just ignore it */
+ }
+
+ return JSON_SUCCESS;
+}
+
+#define HTTPS_SCHEME "https://"
+#define HTTP_SCHEME "http://"
+
+/* We support both well-known suffixes defined by RFC 8414. */
+#define WK_PREFIX "/.well-known/"
+#define OPENID_WK_SUFFIX "openid-configuration"
+#define OAUTH_WK_SUFFIX "oauth-authorization-server"
+
+/*
+ * Derives an issuer identifier from one of our recognized .well-known URIs,
+ * using the rules in RFC 8414.
+ */
+static char *
+issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
+{
+ const char *authority_start = NULL;
+ const char *wk_start;
+ const char *wk_end;
+ char *issuer;
+ ptrdiff_t start_offset,
+ end_offset;
+ size_t end_len;
+
+ /*
+ * https:// is required for issuer identifiers (RFC 8414, Sec. 2; OIDC
+ * Discovery 1.0, Sec. 3). This is a case-insensitive comparison at this
+ * level (but issuer identifier comparison at the level above this is
+ * case-sensitive, so in practice it's probably moot).
+ */
+ if (pg_strncasecmp(wkuri, HTTPS_SCHEME, strlen(HTTPS_SCHEME)) == 0)
+ authority_start = wkuri + strlen(HTTPS_SCHEME);
+
+ if (!authority_start
+ && oauth_unsafe_debugging_enabled()
+ && pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
+ {
+ /* Allow http:// for testing only. */
+ authority_start = wkuri + strlen(HTTP_SCHEME);
+ }
+
+ if (!authority_start)
+ {
+ libpq_append_conn_error(conn,
+ "OAuth discovery URI \"%s\" must use HTTPS",
+ wkuri);
+ return NULL;
+ }
+
+ /*
+ * Well-known URIs in general may support queries and fragments, but the
+ * two types we support here do not. (They must be constructed from the
+ * components of issuer identifiers, which themselves may not contain any
+ * queries or fragments.)
+ *
+ * It's important to check this first, to avoid getting tricked later by a
+ * prefix buried inside a query or fragment.
+ */
+ if (strpbrk(authority_start, "?#") != NULL)
+ {
+ libpq_append_conn_error(conn,
+ "OAuth discovery URI \"%s\" must not contain query or fragment components",
+ wkuri);
+ return NULL;
+ }
+
+ /*
+ * Find the start of the .well-known prefix. IETF rules (RFC 8615) state
+ * this must be at the beginning of the path component, but OIDC defined
+ * it at the end instead (OIDC Discovery 1.0, Sec. 4), so we have to
+ * search for it anywhere.
+ */
+ wk_start = strstr(authority_start, WK_PREFIX);
+ if (!wk_start)
+ {
+ libpq_append_conn_error(conn,
+ "OAuth discovery URI \"%s\" is not a .well-known URI",
+ wkuri);
+ return NULL;
+ }
+
+ /*
+ * Now find the suffix type. We only support the two defined in OIDC
+ * Discovery 1.0 and RFC 8414.
+ */
+ wk_end = wk_start + strlen(WK_PREFIX);
+
+ if (strncmp(wk_end, OPENID_WK_SUFFIX, strlen(OPENID_WK_SUFFIX)) == 0)
+ wk_end += strlen(OPENID_WK_SUFFIX);
+ else if (strncmp(wk_end, OAUTH_WK_SUFFIX, strlen(OAUTH_WK_SUFFIX)) == 0)
+ wk_end += strlen(OAUTH_WK_SUFFIX);
+ else
+ wk_end = NULL;
+
+ /*
+ * Even if there's a match, we still need to check to make sure the suffix
+ * takes up the entire path segment, to weed out constructions like
+ * "/.well-known/openid-configuration-bad".
+ */
+ if (!wk_end || (*wk_end != '/' && *wk_end != '\0'))
+ {
+ libpq_append_conn_error(conn,
+ "OAuth discovery URI \"%s\" uses an unsupported .well-known suffix",
+ wkuri);
+ return NULL;
+ }
+
+ /*
+ * Finally, make sure the .well-known components are provided either as a
+ * prefix (IETF style) or as a postfix (OIDC style). In other words,
+ * "https://localhost/a/.well-known/openid-configuration/b" is not allowed
+ * to claim association with "https://localhost/a/b".
+ */
+ if (*wk_end != '\0')
+ {
+ /*
+ * It's not at the end, so it's required to be at the beginning at the
+ * path. Find the starting slash.
+ */
+ const char *path_start;
+
+ path_start = strchr(authority_start, '/');
+ Assert(path_start); /* otherwise we wouldn't have found WK_PREFIX */
+
+ if (wk_start != path_start)
+ {
+ libpq_append_conn_error(conn,
+ "OAuth discovery URI \"%s\" uses an invalid format",
+ wkuri);
+ return NULL;
+ }
+ }
+
+ /* Checks passed! Now build the issuer. */
+ issuer = strdup(wkuri);
+ if (!issuer)
+ {
+ libpq_append_conn_error(conn, "out of memory");
+ return NULL;
+ }
+
+ /*
+ * The .well-known components are from [wk_start, wk_end). Remove those to
+ * form the issuer ID, by shifting the path suffix (which may be empty)
+ * leftwards.
+ */
+ start_offset = wk_start - wkuri;
+ end_offset = wk_end - wkuri;
+ end_len = strlen(wk_end) + 1; /* move the NULL terminator too */
+
+ memmove(issuer + start_offset, issuer + end_offset, end_len);
+
+ 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.
+ */
+static bool
+handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen)
+{
+ JsonLexContext lex = {0};
+ JsonSemAction sem = {0};
+ JsonParseErrorType err;
+ struct json_ctx ctx = {0};
+ char *errmsg = NULL;
+ bool success = false;
+
+ Assert(conn->oauth_issuer_id); /* ensured by setup_oauth_parameters() */
+
+ /* Sanity check. */
+ if (strlen(msg) != msglen)
+ {
+ libpq_append_conn_error(conn,
+ "server's error message contained an embedded NULL, and was discarded");
+ return false;
+ }
+
+ /*
+ * pg_parse_json doesn't validate the incoming UTF-8, so we have to check
+ * that up front.
+ */
+ if (pg_encoding_verifymbstr(PG_UTF8, msg, msglen) != msglen)
+ {
+ libpq_append_conn_error(conn,
+ "server's error response is not valid UTF-8");
+ return false;
+ }
+
+ makeJsonLexContextCstringLen(&lex, msg, msglen, PG_UTF8, true);
+ setJsonLexContextOwnsTokens(&lex, true); /* must not leak on error */
+
+ initPQExpBuffer(&ctx.errbuf);
+ sem.semstate = &ctx;
+
+ sem.object_start = oauth_json_object_start;
+ sem.object_end = oauth_json_object_end;
+ sem.object_field_start = oauth_json_object_field_start;
+ sem.array_start = oauth_json_array_start;
+ sem.scalar = oauth_json_scalar;
+
+ err = pg_parse_json(&lex, &sem);
+
+ if (err == JSON_SEM_ACTION_FAILED)
+ {
+ if (PQExpBufferDataBroken(ctx.errbuf))
+ errmsg = libpq_gettext("out of memory");
+ else if (ctx.errmsg)
+ errmsg = ctx.errmsg;
+ else
+ {
+ /*
+ * Developer error: one of the action callbacks didn't call
+ * oauth_json_set_error() before erroring out.
+ */
+ Assert(oauth_json_has_error(&ctx));
+ errmsg = "<unexpected empty error>";
+ }
+ }
+ else if (err != JSON_SUCCESS)
+ errmsg = json_errdetail(err, &lex);
+
+ if (errmsg)
+ libpq_append_conn_error(conn,
+ "failed to parse server's error response: %s",
+ errmsg);
+
+ /* Don't need the error buffer or the JSON lexer anymore. */
+ termPQExpBuffer(&ctx.errbuf);
+ freeJsonLexContext(&lex);
+
+ if (errmsg)
+ goto cleanup;
+
+ if (ctx.discovery_uri)
+ {
+ char *discovery_issuer;
+
+ /*
+ * The URI MUST correspond to our existing issuer, to avoid mix-ups.
+ *
+ * Issuer comparison is done byte-wise, rather than performing any URL
+ * normalization; this follows the suggestions for issuer comparison
+ * in RFC 9207 Sec. 2.4 (which requires simple string comparison) and
+ * vastly simplifies things. Since this is the key protection against
+ * a rogue server sending the client to an untrustworthy location,
+ * simpler is better.
+ */
+ discovery_issuer = issuer_from_well_known_uri(conn, ctx.discovery_uri);
+ if (!discovery_issuer)
+ goto cleanup; /* error message already set */
+
+ if (strcmp(conn->oauth_issuer_id, discovery_issuer) != 0)
+ {
+ libpq_append_conn_error(conn,
+ "server's discovery document at %s (issuer \"%s\") is incompatible with oauth_issuer (%s)",
+ ctx.discovery_uri, discovery_issuer,
+ conn->oauth_issuer_id);
+
+ free(discovery_issuer);
+ goto cleanup;
+ }
+
+ free(discovery_issuer);
+
+ if (!conn->oauth_discovery_uri)
+ {
+ conn->oauth_discovery_uri = ctx.discovery_uri;
+ ctx.discovery_uri = NULL;
+ }
+ else
+ {
+ /* This must match the URI we'd previously determined. */
+ if (strcmp(conn->oauth_discovery_uri, ctx.discovery_uri) != 0)
+ {
+ libpq_append_conn_error(conn,
+ "server's discovery document has moved to %s (previous location was %s)",
+ ctx.discovery_uri,
+ conn->oauth_discovery_uri);
+ goto cleanup;
+ }
+ }
+ }
+
+ if (ctx.scope)
+ {
+ /* Servers may not override a previously set oauth_scope. */
+ if (!conn->oauth_scope)
+ {
+ conn->oauth_scope = ctx.scope;
+ ctx.scope = NULL;
+ }
+ }
+
+ if (!ctx.status)
+ {
+ libpq_append_conn_error(conn,
+ "server sent error response without a status");
+ goto cleanup;
+ }
+
+ if (strcmp(ctx.status, "invalid_token") != 0)
+ {
+ /*
+ * invalid_token is the only error code we'll automatically retry for;
+ * otherwise, just bail out now.
+ */
+ libpq_append_conn_error(conn,
+ "server rejected OAuth bearer token: %s",
+ ctx.status);
+ goto cleanup;
+ }
+
+ success = true;
+
+cleanup:
+ free(ctx.status);
+ free(ctx.scope);
+ free(ctx.discovery_uri);
+
+ return success;
+}
+
+/*
+ * 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)
+{
+ fe_oauth_state *state = conn->sasl_state;
+ PGoauthBearerRequest *request = state->async_ctx;
+ PostgresPollingStatusType status;
+
+ if (!request->async)
+ {
+ libpq_append_conn_error(conn,
+ "user-defined OAuth flow provided neither a token nor an async callback");
+ return PGRES_POLLING_FAILED;
+ }
+
+ status = request->async(conn, request, &conn->altsock);
+ if (status == PGRES_POLLING_FAILED)
+ {
+ libpq_append_conn_error(conn, "user-defined OAuth flow failed");
+ return status;
+ }
+ else if (status == PGRES_POLLING_OK)
+ {
+ /*
+ * We already have a token, so copy it into the conn. (We can't hold
+ * onto the original string, since it may not be safe for us to free()
+ * it.)
+ */
+ if (!request->token)
+ {
+ libpq_append_conn_error(conn,
+ "user-defined OAuth flow did not provide a token");
+ return PGRES_POLLING_FAILED;
+ }
+
+ conn->oauth_token = strdup(request->token);
+ if (!conn->oauth_token)
+ {
+ libpq_append_conn_error(conn, "out of memory");
+ return PGRES_POLLING_FAILED;
+ }
+
+ return PGRES_POLLING_OK;
+ }
+
+ /* The hook wants the client to poll the altsock. Make sure it set one. */
+ if (conn->altsock == PGINVALID_SOCKET)
+ {
+ libpq_append_conn_error(conn,
+ "user-defined OAuth flow did not provide a socket for polling");
+ return PGRES_POLLING_FAILED;
+ }
+
+ return status;
+}
+
+/*
+ * Cleanup callback for the async user flow. Delegates most of its job to the
+ * user-provided cleanup implementation, then disconnects the altsock.
+ */
+static void
+cleanup_user_oauth_flow(PGconn *conn)
+{
+ fe_oauth_state *state = conn->sasl_state;
+ PGoauthBearerRequest *request = state->async_ctx;
+
+ Assert(request);
+
+ if (request->cleanup)
+ request->cleanup(conn, request);
+ conn->altsock = PGINVALID_SOCKET;
+
+ free(request);
+ state->async_ctx = NULL;
+}
+
+/*
+ * 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;
+ PGoauthBearerRequest request = {
+ .openid_configuration = conn->oauth_discovery_uri,
+ .scope = conn->oauth_scope,
+ };
+
+ Assert(request.openid_configuration);
+
+ /* The client may have overridden the OAuth flow. */
+ res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request);
+ if (res > 0)
+ {
+ PGoauthBearerRequest *request_copy;
+
+ if (request.token)
+ {
+ /*
+ * We already have a token, so copy it into the conn. (We can't
+ * hold onto the original string, since it may not be safe for us
+ * to free() it.)
+ */
+ conn->oauth_token = strdup(request.token);
+ if (!conn->oauth_token)
+ {
+ libpq_append_conn_error(conn, "out of memory");
+ goto fail;
+ }
+
+ /* short-circuit */
+ if (request.cleanup)
+ request.cleanup(conn, &request);
+ return true;
+ }
+
+ request_copy = malloc(sizeof(*request_copy));
+ if (!request_copy)
+ {
+ libpq_append_conn_error(conn, "out of memory");
+ goto fail;
+ }
+
+ memcpy(request_copy, &request, sizeof(request));
+
+ conn->async_auth = run_user_oauth_flow;
+ conn->cleanup_async_auth = cleanup_user_oauth_flow;
+ state->async_ctx = request_copy;
+ }
+ else if (res < 0)
+ {
+ libpq_append_conn_error(conn, "user-defined OAuth flow failed");
+ goto fail;
+ }
+ else
+ {
+#if USE_LIBCURL
+ /* Hand off to our built-in OAuth flow. */
+ conn->async_auth = pg_fe_run_oauth_flow;
+ conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow;
+
+#else
+ libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built with libcurl support");
+ goto fail;
+
+#endif
+ }
+
+ return true;
+
+fail:
+ if (request.cleanup)
+ request.cleanup(conn, &request);
+ return false;
+}
+
+/*
+ * Fill in our issuer identifier (and discovery URI, if possible) using the
+ * connection parameters. If conn->oauth_discovery_uri can't be populated in
+ * this function, it will be requested from the server.
+ */
+static bool
+setup_oauth_parameters(PGconn *conn)
+{
+ /*
+ * This is the only function that sets conn->oauth_issuer_id. If a
+ * previous connection attempt has already computed it, don't overwrite it
+ * or the discovery URI. (There's no reason for them to change once
+ * they're set, and handle_oauth_sasl_error() will fail the connection if
+ * the server attempts to switch them on us later.)
+ */
+ if (conn->oauth_issuer_id)
+ return true;
+
+ /*---
+ * To talk to a server, we require the user to provide issuer and client
+ * identifiers.
+ *
+ * While it's possible for an OAuth client to support multiple issuers, it
+ * requires additional effort to make sure the flows in use are safe -- to
+ * quote RFC 9207,
+ *
+ * OAuth clients that interact with only one authorization server are
+ * not vulnerable to mix-up attacks. However, when such clients decide
+ * to add support for a second authorization server in the future, they
+ * become vulnerable and need to apply countermeasures to mix-up
+ * attacks.
+ *
+ * For now, we allow only one.
+ */
+ if (!conn->oauth_issuer || !conn->oauth_client_id)
+ {
+ libpq_append_conn_error(conn,
+ "server requires OAuth authentication, but oauth_issuer and oauth_client_id are not both set");
+ return false;
+ }
+
+ /*
+ * oauth_issuer is interpreted differently if it's a well-known discovery
+ * URI rather than just an issuer identifier.
+ */
+ if (strstr(conn->oauth_issuer, WK_PREFIX) != NULL)
+ {
+ /*
+ * Convert the URI back to an issuer identifier. (This also performs
+ * validation of the URI format.)
+ */
+ conn->oauth_issuer_id = issuer_from_well_known_uri(conn,
+ conn->oauth_issuer);
+ if (!conn->oauth_issuer_id)
+ return false; /* error message already set */
+
+ conn->oauth_discovery_uri = strdup(conn->oauth_issuer);
+ if (!conn->oauth_discovery_uri)
+ {
+ libpq_append_conn_error(conn, "out of memory");
+ return false;
+ }
+ }
+ else
+ {
+ /*
+ * Treat oauth_issuer as an issuer identifier. We'll ask the server
+ * for the discovery URI.
+ */
+ conn->oauth_issuer_id = strdup(conn->oauth_issuer);
+ if (!conn->oauth_issuer_id)
+ {
+ libpq_append_conn_error(conn, "out of memory");
+ return false;
+ }
+ }
+
+ 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,
+ char **output, int *outputlen)
+{
+ fe_oauth_state *state = opaq;
+ PGconn *conn = state->conn;
+ bool discover = false;
+
+ *output = NULL;
+ *outputlen = 0;
+
+ switch (state->step)
+ {
+ case FE_OAUTH_INIT:
+ /* We begin in the initial response phase. */
+ Assert(inputlen == -1);
+
+ if (!setup_oauth_parameters(conn))
+ return SASL_FAILED;
+
+ if (conn->oauth_token)
+ {
+ /*
+ * A previous connection already fetched the token; we'll use
+ * it below.
+ */
+ }
+ else if (conn->oauth_discovery_uri)
+ {
+ /*
+ * We don't have a token, but we have a discovery URI already
+ * stored. Decide whether we're using a user-provided OAuth
+ * flow or the one we have built in.
+ */
+ if (!setup_token_request(conn, state))
+ return SASL_FAILED;
+
+ if (conn->oauth_token)
+ {
+ /*
+ * A really smart user implementation may have already
+ * given us the token (e.g. if there was an unexpired copy
+ * already cached), and we can use it immediately.
+ */
+ }
+ else
+ {
+ /*
+ * Otherwise, we'll have to hand the connection over to
+ * our OAuth implementation.
+ *
+ * This could take a while, since it generally involves a
+ * user in the loop. To avoid consuming the server's
+ * authentication timeout, we'll continue this handshake
+ * to the end, so that the server can close its side of
+ * the connection. We'll open a second connection later
+ * once we've retrieved a token.
+ */
+ discover = true;
+ }
+ }
+ else
+ {
+ /*
+ * If we don't have a token, and we don't have a discovery URI
+ * to be able to request a token, we ask the server for one
+ * explicitly.
+ */
+ discover = true;
+ }
+
+ /*
+ * Generate an initial response. This either contains a token, if
+ * we have one, or an empty discovery response which is doomed to
+ * fail.
+ */
+ *output = client_initial_response(conn, discover);
+ if (!*output)
+ return SASL_FAILED;
+
+ *outputlen = strlen(*output);
+ state->step = FE_OAUTH_BEARER_SENT;
+
+ if (conn->oauth_token)
+ {
+ /*
+ * For the purposes of require_auth, our side of
+ * authentication is done at this point; the server will
+ * either accept the connection or send an error. Unlike
+ * SCRAM, there is no additional server data to check upon
+ * success.
+ */
+ conn->client_finished_auth = true;
+ }
+
+ return SASL_CONTINUE;
+
+ case FE_OAUTH_BEARER_SENT:
+ if (final)
+ {
+ /*
+ * OAUTHBEARER does not make use of additional data with a
+ * successful SASL exchange, so we shouldn't get an
+ * AuthenticationSASLFinal message.
+ */
+ libpq_append_conn_error(conn,
+ "server sent unexpected additional OAuth data");
+ return SASL_FAILED;
+ }
+
+ /*
+ * An error message was sent by the server. Respond with the
+ * required dummy message (RFC 7628, sec. 3.2.3).
+ */
+ *output = strdup(kvsep);
+ if (unlikely(!*output))
+ {
+ libpq_append_conn_error(conn, "out of memory");
+ return SASL_FAILED;
+ }
+ *outputlen = strlen(*output); /* == 1 */
+
+ /* Grab the settings from discovery. */
+ if (!handle_oauth_sasl_error(conn, input, inputlen))
+ return SASL_FAILED;
+
+ if (conn->oauth_token)
+ {
+ /*
+ * The server rejected our token. Continue onwards towards the
+ * expected FATAL message, but mark our state to catch any
+ * unexpected "success" from the server.
+ */
+ state->step = FE_OAUTH_SERVER_ERROR;
+ return SASL_CONTINUE;
+ }
+
+ if (!conn->async_auth)
+ {
+ /*
+ * No OAuth flow is set up yet. Did we get enough information
+ * from the server to create one?
+ */
+ if (!conn->oauth_discovery_uri)
+ {
+ libpq_append_conn_error(conn,
+ "server requires OAuth authentication, but no discovery metadata was provided");
+ return SASL_FAILED;
+ }
+
+ /* Yes. Set up the flow now. */
+ if (!setup_token_request(conn, state))
+ return SASL_FAILED;
+
+ if (conn->oauth_token)
+ {
+ /*
+ * A token was available in a custom flow's cache. Skip
+ * the asynchronous processing.
+ */
+ goto reconnect;
+ }
+ }
+
+ /*
+ * Time to retrieve a token. This involves a number of HTTP
+ * connections and timed waits, so we escape the synchronous auth
+ * processing and tell PQconnectPoll to transfer control to our
+ * async implementation.
+ */
+ Assert(conn->async_auth); /* should have been set already */
+ state->step = FE_OAUTH_REQUESTING_TOKEN;
+ return SASL_ASYNC;
+
+ case FE_OAUTH_REQUESTING_TOKEN:
+
+ /*
+ * We've returned successfully from token retrieval. Double-check
+ * that we have what we need for the next connection.
+ */
+ if (!conn->oauth_token)
+ {
+ Assert(false); /* should have failed before this point! */
+ libpq_append_conn_error(conn,
+ "internal error: OAuth flow did not set a token");
+ return SASL_FAILED;
+ }
+
+ goto reconnect;
+
+ case FE_OAUTH_SERVER_ERROR:
+
+ /*
+ * After an error, the server should send an error response to
+ * fail the SASL handshake, which is handled in higher layers.
+ *
+ * If we get here, the server either sent *another* challenge
+ * which isn't defined in the RFC, or completed the handshake
+ * successfully after telling us it was going to fail. Neither is
+ * acceptable.
+ */
+ libpq_append_conn_error(conn,
+ "server sent additional OAuth data after error");
+ return SASL_FAILED;
+
+ default:
+ libpq_append_conn_error(conn, "invalid OAuth exchange state");
+ break;
+ }
+
+ Assert(false); /* should never get here */
+ return SASL_FAILED;
+
+reconnect:
+
+ /*
+ * Despite being a failure from the point of view of SASL, we have enough
+ * information to restart with a new connection.
+ */
+ libpq_append_conn_error(conn, "retrying connection with new bearer token");
+ conn->oauth_want_retry = true;
+ return SASL_FAILED;
+}
+
+static bool
+oauth_channel_bound(void *opaq)
+{
+ /* This mechanism does not support channel binding. */
+ return false;
+}
+
+/*
+ * Fully clears out any stored OAuth token. This is done proactively upon
+ * successful connection as well as during pqClosePGconn().
+ */
+void
+pqClearOAuthToken(PGconn *conn)
+{
+ if (!conn->oauth_token)
+ return;
+
+ explicit_bzero(conn->oauth_token, strlen(conn->oauth_token));
+ free(conn->oauth_token);
+ conn->oauth_token = NULL;
+}
+
+/*
+ * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
+ */
+bool
+oauth_unsafe_debugging_enabled(void)
+{
+ const char *env = getenv("PGOAUTHDEBUG");
+
+ return (env && strcmp(env, "UNSAFE") == 0);
+}