/*------------------------------------------------------------------------- * * 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 #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" 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; 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.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 = ""; } } 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); }