mirror of
https://github.com/postgres/postgres.git
synced 2025-12-19 17:02:53 +03:00
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
1405 lines
37 KiB
C
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);
|
|
}
|