1
0
mirror of https://github.com/postgres/postgres.git synced 2025-12-19 17:02:53 +03:00
Files
postgres/src/interfaces/libpq/fe-auth-oauth.c
Jacob Champion 7a15cff1f1 libpq: Align oauth_json_set_error() with other NLS patterns
Now that the prior commits have fixed missing OAuth translations, pull
the bespoke usage of libpq_gettext() for OAUTHBEARER parsing into
oauth_json_set_error() itself, and make that a gettext trigger as well,
to better match what the other sites are doing. Add an _internal()
variant to handle the existing untranslated case.

Suggested-by: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Álvaro Herrera <alvherre@kurilemu.de>
Discussion: https://postgr.es/m/0EEBCAA8-A5AC-4E3B-BABA-0BA7A08C361B%40yesql.se
Backpatch-through: 18
2025-12-15 13:30:48 -08:00

1405 lines
37 KiB
C

/*-------------------------------------------------------------------------
*
* 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"
#ifdef USE_DYNAMIC_OAUTH
#include <dlfcn.h>
#endif
#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"
#include "pg_config_paths.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"
/*
* Limit the maximum number of nested objects/arrays. Because OAUTHBEARER
* doesn't have any defined extensions for its JSON yet, we can be much more
* conservative here than with libpq-oauth's MAX_OAUTH_NESTING_LEVEL; we expect
* a nesting level of 1 in practice.
*/
#define MAX_SASL_NESTING_LEVEL 8
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, fmt, ...) \
do { \
appendPQExpBuffer(&(ctx)->errbuf, libpq_gettext(fmt), ##__VA_ARGS__); \
(ctx)->errmsg = (ctx)->errbuf.data; \
} while (0)
/* An untranslated version of oauth_json_set_error(). */
#define oauth_json_set_error_internal(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,
"field \"%s\" must be a string",
ctx->target_field_name);
}
++ctx->nested;
if (ctx->nested > MAX_SASL_NESTING_LEVEL)
oauth_json_set_error(ctx, "JSON is too deeply 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)
{
oauth_json_set_error(ctx, "top-level element must be an object");
}
else if (ctx->target_field)
{
Assert(ctx->nested == 1);
oauth_json_set_error(ctx,
"field \"%s\" must be a string",
ctx->target_field_name);
}
++ctx->nested;
if (ctx->nested > MAX_SASL_NESTING_LEVEL)
oauth_json_set_error(ctx, "JSON is too deeply nested");
return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
}
static JsonParseErrorType
oauth_json_array_end(void *state)
{
struct json_ctx *ctx = state;
--ctx->nested;
return JSON_SUCCESS;
}
static JsonParseErrorType
oauth_json_scalar(void *state, char *token, JsonTokenType type)
{
struct json_ctx *ctx = state;
if (!ctx->nested)
{
oauth_json_set_error(ctx, "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_internal(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,
"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,
"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;
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;
}
lex = makeJsonLexContextCstringLen(NULL, 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.array_end = oauth_json_array_end;
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;
}
/*-------------
* Builtin Flow
*
* There are three potential implementations of use_builtin_flow:
*
* 1) If the OAuth client is disabled at configuration time, return false.
* Dependent clients must provide their own flow.
* 2) If the OAuth client is enabled and USE_DYNAMIC_OAUTH is defined, dlopen()
* the libpq-oauth plugin and use its implementation.
* 3) Otherwise, use flow callbacks that are statically linked into the
* executable.
*/
#if !defined(USE_LIBCURL)
/*
* This configuration doesn't support the builtin flow.
*/
bool
use_builtin_flow(PGconn *conn, fe_oauth_state *state)
{
return false;
}
#elif defined(USE_DYNAMIC_OAUTH)
/*
* Use the builtin flow in the libpq-oauth plugin, which is loaded at runtime.
*/
typedef char *(*libpq_gettext_func) (const char *msgid);
/*
* Define accessor/mutator shims to inject into libpq-oauth, so that it doesn't
* depend on the offsets within PGconn. (These have changed during minor version
* updates in the past.)
*/
#define DEFINE_GETTER(TYPE, MEMBER) \
typedef TYPE (*conn_ ## MEMBER ## _func) (PGconn *conn); \
static TYPE conn_ ## MEMBER(PGconn *conn) { return conn->MEMBER; }
/* Like DEFINE_GETTER, but returns a pointer to the member. */
#define DEFINE_GETTER_P(TYPE, MEMBER) \
typedef TYPE (*conn_ ## MEMBER ## _func) (PGconn *conn); \
static TYPE conn_ ## MEMBER(PGconn *conn) { return &conn->MEMBER; }
#define DEFINE_SETTER(TYPE, MEMBER) \
typedef void (*set_conn_ ## MEMBER ## _func) (PGconn *conn, TYPE val); \
static void set_conn_ ## MEMBER(PGconn *conn, TYPE val) { conn->MEMBER = val; }
DEFINE_GETTER_P(PQExpBuffer, errorMessage);
DEFINE_GETTER(char *, oauth_client_id);
DEFINE_GETTER(char *, oauth_client_secret);
DEFINE_GETTER(char *, oauth_discovery_uri);
DEFINE_GETTER(char *, oauth_issuer_id);
DEFINE_GETTER(char *, oauth_scope);
DEFINE_GETTER(fe_oauth_state *, sasl_state);
DEFINE_SETTER(pgsocket, altsock);
DEFINE_SETTER(char *, oauth_token);
/*
* Loads the libpq-oauth plugin via dlopen(), initializes it, and plugs its
* callbacks into the connection's async auth handlers.
*
* Failure to load here results in a relatively quiet connection error, to
* handle the use case where the build supports loading a flow but a user does
* not want to install it. Troubleshooting of linker/loader failures can be done
* via PGOAUTHDEBUG.
*/
bool
use_builtin_flow(PGconn *conn, fe_oauth_state *state)
{
static bool initialized = false;
static pthread_mutex_t init_mutex = PTHREAD_MUTEX_INITIALIZER;
int lockerr;
void (*init) (pgthreadlock_t threadlock,
libpq_gettext_func gettext_impl,
conn_errorMessage_func errmsg_impl,
conn_oauth_client_id_func clientid_impl,
conn_oauth_client_secret_func clientsecret_impl,
conn_oauth_discovery_uri_func discoveryuri_impl,
conn_oauth_issuer_id_func issuerid_impl,
conn_oauth_scope_func scope_impl,
conn_sasl_state_func saslstate_impl,
set_conn_altsock_func setaltsock_impl,
set_conn_oauth_token_func settoken_impl);
PostgresPollingStatusType (*flow) (PGconn *conn);
void (*cleanup) (PGconn *conn);
/*
* On macOS only, load the module using its absolute install path; the
* standard search behavior is not very helpful for this use case. Unlike
* on other platforms, DYLD_LIBRARY_PATH is used as a fallback even with
* absolute paths (modulo SIP effects), so tests can continue to work.
*
* On the other platforms, load the module using only the basename, to
* rely on the runtime linker's standard search behavior.
*/
const char *const module_name =
#if defined(__darwin__)
LIBDIR "/libpq-oauth-" PG_MAJORVERSION DLSUFFIX;
#else
"libpq-oauth-" PG_MAJORVERSION DLSUFFIX;
#endif
state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
if (!state->builtin_flow)
{
/*
* For end users, this probably isn't an error condition, it just
* means the flow isn't installed. Developers and package maintainers
* may want to debug this via the PGOAUTHDEBUG envvar, though.
*
* Note that POSIX dlerror() isn't guaranteed to be threadsafe.
*/
if (oauth_unsafe_debugging_enabled())
fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
return false;
}
if ((init = dlsym(state->builtin_flow, "libpq_oauth_init")) == NULL
|| (flow = dlsym(state->builtin_flow, "pg_fe_run_oauth_flow")) == NULL
|| (cleanup = dlsym(state->builtin_flow, "pg_fe_cleanup_oauth_flow")) == NULL)
{
/*
* This is more of an error condition than the one above, but due to
* the dlerror() threadsafety issue, lock it behind PGOAUTHDEBUG too.
*/
if (oauth_unsafe_debugging_enabled())
fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
dlclose(state->builtin_flow);
return false;
}
/*
* Past this point, we do not unload the module. It stays in the process
* permanently.
*/
/*
* We need to inject necessary function pointers into the module. This
* only needs to be done once -- even if the pointers are constant,
* assigning them while another thread is executing the flows feels like
* tempting fate.
*/
if ((lockerr = pthread_mutex_lock(&init_mutex)) != 0)
{
/* Should not happen... but don't continue if it does. */
Assert(false);
libpq_append_conn_error(conn, "failed to lock mutex (%d)", lockerr);
return false;
}
if (!initialized)
{
init(pg_g_threadlock,
#ifdef ENABLE_NLS
libpq_gettext,
#else
NULL,
#endif
conn_errorMessage,
conn_oauth_client_id,
conn_oauth_client_secret,
conn_oauth_discovery_uri,
conn_oauth_issuer_id,
conn_oauth_scope,
conn_sasl_state,
set_conn_altsock,
set_conn_oauth_token);
initialized = true;
}
pthread_mutex_unlock(&init_mutex);
/* Set our asynchronous callbacks. */
conn->async_auth = flow;
conn->cleanup_async_auth = cleanup;
return true;
}
#else
/*
* Use the builtin flow in libpq-oauth.a (see libpq-oauth/oauth-curl.h).
*/
extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn);
extern void pg_fe_cleanup_oauth_flow(PGconn *conn);
bool
use_builtin_flow(PGconn *conn, fe_oauth_state *state)
{
/* Set our asynchronous callbacks. */
conn->async_auth = pg_fe_run_oauth_flow;
conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow;
return true;
}
#endif /* USE_LIBCURL */
/*
* 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;
}
*request_copy = 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_builtin_flow(conn, state))
{
libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
goto fail;
}
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);
}