Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * fe-auth-oauth.c
4 : : * The front-end (client) implementation of OAuth/OIDC authentication
5 : : * using the SASL OAUTHBEARER mechanism.
6 : : *
7 : : * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
8 : : * Portions Copyright (c) 1994, Regents of the University of California
9 : : *
10 : : * IDENTIFICATION
11 : : * src/interfaces/libpq/fe-auth-oauth.c
12 : : *
13 : : *-------------------------------------------------------------------------
14 : : */
15 : :
16 : : #include "postgres_fe.h"
17 : :
18 : : #ifdef USE_DYNAMIC_OAUTH
19 : : #include <dlfcn.h>
20 : : #endif
21 : :
22 : : #include "common/base64.h"
23 : : #include "common/hmac.h"
24 : : #include "common/jsonapi.h"
25 : : #include "common/oauth-common.h"
26 : : #include "fe-auth.h"
27 : : #include "fe-auth-oauth.h"
28 : : #include "mb/pg_wchar.h"
29 : : #include "oauth-debug.h"
30 : : #include "pg_config_paths.h"
31 : : #include "utils/memdebug.h"
32 : :
33 : : static PostgresPollingStatusType do_async(fe_oauth_state *state,
34 : : PGoauthBearerRequestV2 *request);
35 : : static void do_cleanup(fe_oauth_state *state, PGoauthBearerRequestV2 *request);
36 : : static void poison_req_v2(PGoauthBearerRequestV2 *request, bool poison);
37 : :
38 : : /* The exported OAuth callback mechanism. */
39 : : static void *oauth_init(PGconn *conn, const char *password,
40 : : const char *sasl_mechanism);
41 : : static SASLStatus oauth_exchange(void *opaq, bool final,
42 : : char *input, int inputlen,
43 : : char **output, int *outputlen);
44 : : static bool oauth_channel_bound(void *opaq);
45 : : static void oauth_free(void *opaq);
46 : :
47 : : const pg_fe_sasl_mech pg_oauth_mech = {
48 : : oauth_init,
49 : : oauth_exchange,
50 : : oauth_channel_bound,
51 : : oauth_free,
52 : : };
53 : :
54 : : /*
55 : : * Initializes mechanism state for OAUTHBEARER.
56 : : *
57 : : * For a full description of the API, see libpq/fe-auth-sasl.h.
58 : : */
59 : : static void *
439 dgustafsson@postgres 60 :CBC 116 : oauth_init(PGconn *conn, const char *password,
61 : : const char *sasl_mechanism)
62 : : {
63 : : fe_oauth_state *state;
64 : :
65 : : /*
66 : : * We only support one SASL mechanism here; anything else is programmer
67 : : * error.
68 : : */
69 [ - + ]: 116 : Assert(sasl_mechanism != NULL);
70 [ - + ]: 116 : Assert(strcmp(sasl_mechanism, OAUTHBEARER_NAME) == 0);
71 : :
72 : 116 : state = calloc(1, sizeof(*state));
73 [ - + ]: 116 : if (!state)
439 dgustafsson@postgres 74 :UBC 0 : return NULL;
75 : :
439 dgustafsson@postgres 76 :CBC 116 : state->step = FE_OAUTH_INIT;
77 : 116 : state->conn = conn;
78 : :
79 : 116 : return state;
80 : : }
81 : :
82 : : /*
83 : : * Frees the state allocated by oauth_init().
84 : : *
85 : : * This handles only mechanism state tied to the connection lifetime; state
86 : : * stored in state->async_ctx is freed up either immediately after the
87 : : * authentication handshake succeeds, or before the mechanism is cleaned up on
88 : : * failure. See pg_fe_cleanup_oauth_flow() and cleanup_oauth_flow().
89 : : */
90 : : static void
91 : 116 : oauth_free(void *opaq)
92 : : {
93 : 116 : fe_oauth_state *state = opaq;
94 : :
95 : : /* Any async authentication state should have been cleaned up already. */
96 [ - + ]: 116 : Assert(!state->async_ctx);
97 : :
98 : 116 : free(state);
99 : 116 : }
100 : :
101 : : #define kvsep "\x01"
102 : :
103 : : /*
104 : : * Constructs an OAUTHBEARER client initial response (RFC 7628, Sec. 3.1).
105 : : *
106 : : * If discover is true, the initial response will contain a request for the
107 : : * server's required OAuth parameters (Sec. 4.3). Otherwise, conn->token must
108 : : * be set; it will be sent as the connection's bearer token.
109 : : *
110 : : * Returns the response as a null-terminated string, or NULL on error.
111 : : */
112 : : static char *
113 : 116 : client_initial_response(PGconn *conn, bool discover)
114 : : {
115 : : static const char *const resp_format = "n,," kvsep "auth=%s%s" kvsep kvsep;
116 : :
117 : : PQExpBufferData buf;
118 : : const char *authn_scheme;
119 : 116 : char *response = NULL;
120 : 116 : const char *token = conn->oauth_token;
121 : :
122 [ + + ]: 116 : if (discover)
123 : : {
124 : : /* Parameter discovery uses a completely empty auth value. */
125 : 73 : authn_scheme = token = "";
126 : : }
127 : : else
128 : : {
129 : : /*
130 : : * Use a Bearer authentication scheme (RFC 6750, Sec. 2.1). A trailing
131 : : * space is used as a separator.
132 : : */
133 : 43 : authn_scheme = "Bearer ";
134 : :
135 : : /* conn->token must have been set in this case. */
136 [ - + ]: 43 : if (!token)
137 : : {
439 dgustafsson@postgres 138 :UBC 0 : Assert(false);
139 : : libpq_append_conn_error(conn,
140 : : "internal error: no OAuth token was set for the connection");
141 : : return NULL;
142 : : }
143 : : }
144 : :
439 dgustafsson@postgres 145 :CBC 116 : initPQExpBuffer(&buf);
146 : 116 : appendPQExpBuffer(&buf, resp_format, authn_scheme, token);
147 : :
148 [ + - ]: 116 : if (!PQExpBufferDataBroken(buf))
149 : 116 : response = strdup(buf.data);
150 : 116 : termPQExpBuffer(&buf);
151 : :
152 [ - + ]: 116 : if (!response)
439 dgustafsson@postgres 153 :UBC 0 : libpq_append_conn_error(conn, "out of memory");
154 : :
439 dgustafsson@postgres 155 :CBC 116 : return response;
156 : : }
157 : :
158 : : /*
159 : : * JSON Parser (for the OAUTHBEARER error result)
160 : : */
161 : :
162 : : /* Relevant JSON fields in the error result object. */
163 : : #define ERROR_STATUS_FIELD "status"
164 : : #define ERROR_SCOPE_FIELD "scope"
165 : : #define ERROR_OPENID_CONFIGURATION_FIELD "openid-configuration"
166 : :
167 : : /*
168 : : * Limit the maximum number of nested objects/arrays. Because OAUTHBEARER
169 : : * doesn't have any defined extensions for its JSON yet, we can be much more
170 : : * conservative here than with libpq-oauth's MAX_OAUTH_NESTING_LEVEL; we expect
171 : : * a nesting level of 1 in practice.
172 : : */
173 : : #define MAX_SASL_NESTING_LEVEL 8
174 : :
175 : : struct json_ctx
176 : : {
177 : : char *errmsg; /* any non-NULL value stops all processing */
178 : : PQExpBufferData errbuf; /* backing memory for errmsg */
179 : : int nested; /* nesting level (zero is the top) */
180 : :
181 : : const char *target_field_name; /* points to a static allocation */
182 : : char **target_field; /* see below */
183 : :
184 : : /* target_field, if set, points to one of the following: */
185 : : char *status;
186 : : char *scope;
187 : : char *discovery_uri;
188 : : };
189 : :
190 : : #define oauth_json_has_error(ctx) \
191 : : (PQExpBufferDataBroken((ctx)->errbuf) || (ctx)->errmsg)
192 : :
193 : : #define oauth_json_set_error(ctx, fmt, ...) \
194 : : do { \
195 : : appendPQExpBuffer(&(ctx)->errbuf, libpq_gettext(fmt), ##__VA_ARGS__); \
196 : : (ctx)->errmsg = (ctx)->errbuf.data; \
197 : : } while (0)
198 : :
199 : : /* An untranslated version of oauth_json_set_error(). */
200 : : #define oauth_json_set_error_internal(ctx, ...) \
201 : : do { \
202 : : appendPQExpBuffer(&(ctx)->errbuf, __VA_ARGS__); \
203 : : (ctx)->errmsg = (ctx)->errbuf.data; \
204 : : } while (0)
205 : :
206 : : static JsonParseErrorType
207 : 80 : oauth_json_object_start(void *state)
208 : : {
209 : 80 : struct json_ctx *ctx = state;
210 : :
211 [ - + ]: 80 : if (ctx->target_field)
212 : : {
439 dgustafsson@postgres 213 [ # # ]:UBC 0 : Assert(ctx->nested == 1);
214 : :
215 : 0 : oauth_json_set_error(ctx,
216 : : "field \"%s\" must be a string",
217 : : ctx->target_field_name);
218 : : }
219 : :
439 dgustafsson@postgres 220 :CBC 80 : ++ctx->nested;
347 jchampion@postgresql 221 [ - + ]: 80 : if (ctx->nested > MAX_SASL_NESTING_LEVEL)
141 jchampion@postgresql 222 :UBC 0 : oauth_json_set_error(ctx, "JSON is too deeply nested");
223 : :
439 dgustafsson@postgres 224 [ + - - + ]:CBC 80 : return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
225 : : }
226 : :
227 : : static JsonParseErrorType
228 : 80 : oauth_json_object_end(void *state)
229 : : {
230 : 80 : struct json_ctx *ctx = state;
231 : :
232 : 80 : --ctx->nested;
233 : 80 : return JSON_SUCCESS;
234 : : }
235 : :
236 : : static JsonParseErrorType
237 : 240 : oauth_json_object_field_start(void *state, char *name, bool isnull)
238 : : {
239 : 240 : struct json_ctx *ctx = state;
240 : :
241 : : /* Only top-level keys are considered. */
242 [ + - ]: 240 : if (ctx->nested == 1)
243 : : {
244 [ + + ]: 240 : if (strcmp(name, ERROR_STATUS_FIELD) == 0)
245 : : {
246 : 80 : ctx->target_field_name = ERROR_STATUS_FIELD;
247 : 80 : ctx->target_field = &ctx->status;
248 : : }
249 [ + + ]: 160 : else if (strcmp(name, ERROR_SCOPE_FIELD) == 0)
250 : : {
251 : 80 : ctx->target_field_name = ERROR_SCOPE_FIELD;
252 : 80 : ctx->target_field = &ctx->scope;
253 : : }
254 [ + - ]: 80 : else if (strcmp(name, ERROR_OPENID_CONFIGURATION_FIELD) == 0)
255 : : {
256 : 80 : ctx->target_field_name = ERROR_OPENID_CONFIGURATION_FIELD;
257 : 80 : ctx->target_field = &ctx->discovery_uri;
258 : : }
259 : : }
260 : :
261 : 240 : return JSON_SUCCESS;
262 : : }
263 : :
264 : : static JsonParseErrorType
439 dgustafsson@postgres 265 :UBC 0 : oauth_json_array_start(void *state)
266 : : {
267 : 0 : struct json_ctx *ctx = state;
268 : :
269 [ # # ]: 0 : if (!ctx->nested)
270 : : {
141 jchampion@postgresql 271 : 0 : oauth_json_set_error(ctx, "top-level element must be an object");
272 : : }
439 dgustafsson@postgres 273 [ # # ]: 0 : else if (ctx->target_field)
274 : : {
275 [ # # ]: 0 : Assert(ctx->nested == 1);
276 : :
277 : 0 : oauth_json_set_error(ctx,
278 : : "field \"%s\" must be a string",
279 : : ctx->target_field_name);
280 : : }
281 : :
347 jchampion@postgresql 282 : 0 : ++ctx->nested;
283 [ # # ]: 0 : if (ctx->nested > MAX_SASL_NESTING_LEVEL)
141 284 : 0 : oauth_json_set_error(ctx, "JSON is too deeply nested");
285 : :
439 dgustafsson@postgres 286 [ # # # # ]: 0 : return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
287 : : }
288 : :
289 : : static JsonParseErrorType
347 jchampion@postgresql 290 : 0 : oauth_json_array_end(void *state)
291 : : {
292 : 0 : struct json_ctx *ctx = state;
293 : :
294 : 0 : --ctx->nested;
295 : 0 : return JSON_SUCCESS;
296 : : }
297 : :
298 : : static JsonParseErrorType
439 dgustafsson@postgres 299 :CBC 240 : oauth_json_scalar(void *state, char *token, JsonTokenType type)
300 : : {
301 : 240 : struct json_ctx *ctx = state;
302 : :
303 [ - + ]: 240 : if (!ctx->nested)
304 : : {
141 jchampion@postgresql 305 :UBC 0 : oauth_json_set_error(ctx, "top-level element must be an object");
439 dgustafsson@postgres 306 : 0 : return JSON_SEM_ACTION_FAILED;
307 : : }
308 : :
439 dgustafsson@postgres 309 [ + - ]:CBC 240 : if (ctx->target_field)
310 : : {
311 [ - + ]: 240 : if (ctx->nested != 1)
312 : : {
313 : : /*
314 : : * ctx->target_field should not have been set for nested keys.
315 : : * Assert and don't continue any further for production builds.
316 : : */
439 dgustafsson@postgres 317 :UBC 0 : Assert(false);
318 : : oauth_json_set_error_internal(ctx,
319 : : "internal error: target scalar found at nesting level %d during OAUTHBEARER parsing",
320 : : ctx->nested);
321 : : return JSON_SEM_ACTION_FAILED;
322 : : }
323 : :
324 : : /*
325 : : * We don't allow duplicate field names; error out if the target has
326 : : * already been set.
327 : : */
439 dgustafsson@postgres 328 [ - + ]:CBC 240 : if (*ctx->target_field)
329 : : {
439 dgustafsson@postgres 330 :UBC 0 : oauth_json_set_error(ctx,
331 : : "field \"%s\" is duplicated",
332 : : ctx->target_field_name);
333 : 0 : return JSON_SEM_ACTION_FAILED;
334 : : }
335 : :
336 : : /* The only fields we support are strings. */
439 dgustafsson@postgres 337 [ - + ]:CBC 240 : if (type != JSON_TOKEN_STRING)
338 : : {
439 dgustafsson@postgres 339 :UBC 0 : oauth_json_set_error(ctx,
340 : : "field \"%s\" must be a string",
341 : : ctx->target_field_name);
342 : 0 : return JSON_SEM_ACTION_FAILED;
343 : : }
344 : :
439 dgustafsson@postgres 345 :CBC 240 : *ctx->target_field = strdup(token);
346 [ - + ]: 240 : if (!*ctx->target_field)
439 dgustafsson@postgres 347 :UBC 0 : return JSON_OUT_OF_MEMORY;
348 : :
439 dgustafsson@postgres 349 :CBC 240 : ctx->target_field = NULL;
350 : 240 : ctx->target_field_name = NULL;
351 : : }
352 : : else
353 : : {
354 : : /* otherwise we just ignore it */
355 : : }
356 : :
357 : 240 : return JSON_SUCCESS;
358 : : }
359 : :
360 : : #define HTTPS_SCHEME "https://"
361 : : #define HTTP_SCHEME "http://"
362 : :
363 : : /* We support both well-known suffixes defined by RFC 8414. */
364 : : #define WK_PREFIX "/.well-known/"
365 : : #define OPENID_WK_SUFFIX "openid-configuration"
366 : : #define OAUTH_WK_SUFFIX "oauth-authorization-server"
367 : :
368 : : /*
369 : : * Derives an issuer identifier from one of our recognized .well-known URIs,
370 : : * using the rules in RFC 8414.
371 : : */
372 : : static char *
373 : 96 : issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
374 : : {
375 : 96 : const char *authority_start = NULL;
376 : : const char *wk_start;
377 : : const char *wk_end;
378 : : char *issuer;
379 : : ptrdiff_t start_offset,
380 : : end_offset;
381 : : size_t end_len;
382 : :
383 : : /*
384 : : * https:// is required for issuer identifiers (RFC 8414, Sec. 2; OIDC
385 : : * Discovery 1.0, Sec. 3). This is a case-insensitive comparison at this
386 : : * level (but issuer identifier comparison at the level above this is
387 : : * case-sensitive, so in practice it's probably moot).
388 : : */
389 [ + + ]: 96 : if (pg_strncasecmp(wkuri, HTTPS_SCHEME, strlen(HTTPS_SCHEME)) == 0)
390 : 94 : authority_start = wkuri + strlen(HTTPS_SCHEME);
391 : :
392 [ + + ]: 96 : if (!authority_start
28 jchampion@postgresql 393 [ - + ]:GNC 2 : && (oauth_parse_debug_flags() & OAUTHDEBUG_UNSAFE_HTTP)
439 dgustafsson@postgres 394 [ # # ]:LBC (65) : && pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
395 : : {
396 : : /* Allow http:// for testing only. */
397 : (65) : authority_start = wkuri + strlen(HTTP_SCHEME);
398 : : }
399 : :
439 dgustafsson@postgres 400 [ + + ]:CBC 96 : if (!authority_start)
401 : : {
402 : 2 : libpq_append_conn_error(conn,
403 : : "OAuth discovery URI \"%s\" must use HTTPS",
404 : : wkuri);
405 : 2 : return NULL;
406 : : }
407 : :
408 : : /*
409 : : * Well-known URIs in general may support queries and fragments, but the
410 : : * two types we support here do not. (They must be constructed from the
411 : : * components of issuer identifiers, which themselves may not contain any
412 : : * queries or fragments.)
413 : : *
414 : : * It's important to check this first, to avoid getting tricked later by a
415 : : * prefix buried inside a query or fragment.
416 : : */
417 [ - + ]: 94 : if (strpbrk(authority_start, "?#") != NULL)
418 : : {
439 dgustafsson@postgres 419 :UBC 0 : libpq_append_conn_error(conn,
420 : : "OAuth discovery URI \"%s\" must not contain query or fragment components",
421 : : wkuri);
422 : 0 : return NULL;
423 : : }
424 : :
425 : : /*
426 : : * Find the start of the .well-known prefix. IETF rules (RFC 8615) state
427 : : * this must be at the beginning of the path component, but OIDC defined
428 : : * it at the end instead (OIDC Discovery 1.0, Sec. 4), so we have to
429 : : * search for it anywhere.
430 : : */
439 dgustafsson@postgres 431 :CBC 94 : wk_start = strstr(authority_start, WK_PREFIX);
432 [ - + ]: 94 : if (!wk_start)
433 : : {
439 dgustafsson@postgres 434 :UBC 0 : libpq_append_conn_error(conn,
435 : : "OAuth discovery URI \"%s\" is not a .well-known URI",
436 : : wkuri);
437 : 0 : return NULL;
438 : : }
439 : :
440 : : /*
441 : : * Now find the suffix type. We only support the two defined in OIDC
442 : : * Discovery 1.0 and RFC 8414.
443 : : */
439 dgustafsson@postgres 444 :CBC 94 : wk_end = wk_start + strlen(WK_PREFIX);
445 : :
446 [ + + ]: 94 : if (strncmp(wk_end, OPENID_WK_SUFFIX, strlen(OPENID_WK_SUFFIX)) == 0)
447 : 89 : wk_end += strlen(OPENID_WK_SUFFIX);
448 [ + - ]: 5 : else if (strncmp(wk_end, OAUTH_WK_SUFFIX, strlen(OAUTH_WK_SUFFIX)) == 0)
449 : 5 : wk_end += strlen(OAUTH_WK_SUFFIX);
450 : : else
439 dgustafsson@postgres 451 :UBC 0 : wk_end = NULL;
452 : :
453 : : /*
454 : : * Even if there's a match, we still need to check to make sure the suffix
455 : : * takes up the entire path segment, to weed out constructions like
456 : : * "/.well-known/openid-configuration-bad".
457 : : */
439 dgustafsson@postgres 458 [ + - + + :CBC 94 : if (!wk_end || (*wk_end != '/' && *wk_end != '\0'))
- + ]
459 : : {
439 dgustafsson@postgres 460 :UBC 0 : libpq_append_conn_error(conn,
461 : : "OAuth discovery URI \"%s\" uses an unsupported .well-known suffix",
462 : : wkuri);
463 : 0 : return NULL;
464 : : }
465 : :
466 : : /*
467 : : * Finally, make sure the .well-known components are provided either as a
468 : : * prefix (IETF style) or as a postfix (OIDC style). In other words,
469 : : * "https://localhost/a/.well-known/openid-configuration/b" is not allowed
470 : : * to claim association with "https://localhost/a/b".
471 : : */
439 dgustafsson@postgres 472 [ + + ]:CBC 94 : if (*wk_end != '\0')
473 : : {
474 : : /*
475 : : * It's not at the end, so it's required to be at the beginning at the
476 : : * path. Find the starting slash.
477 : : */
478 : : const char *path_start;
479 : :
480 : 5 : path_start = strchr(authority_start, '/');
481 [ - + ]: 5 : Assert(path_start); /* otherwise we wouldn't have found WK_PREFIX */
482 : :
483 [ - + ]: 5 : if (wk_start != path_start)
484 : : {
439 dgustafsson@postgres 485 :UBC 0 : libpq_append_conn_error(conn,
486 : : "OAuth discovery URI \"%s\" uses an invalid format",
487 : : wkuri);
488 : 0 : return NULL;
489 : : }
490 : : }
491 : :
492 : : /* Checks passed! Now build the issuer. */
439 dgustafsson@postgres 493 :CBC 94 : issuer = strdup(wkuri);
494 [ - + ]: 94 : if (!issuer)
495 : : {
439 dgustafsson@postgres 496 :UBC 0 : libpq_append_conn_error(conn, "out of memory");
497 : 0 : return NULL;
498 : : }
499 : :
500 : : /*
501 : : * The .well-known components are from [wk_start, wk_end). Remove those to
502 : : * form the issuer ID, by shifting the path suffix (which may be empty)
503 : : * leftwards.
504 : : */
439 dgustafsson@postgres 505 :CBC 94 : start_offset = wk_start - wkuri;
506 : 94 : end_offset = wk_end - wkuri;
507 : 94 : end_len = strlen(wk_end) + 1; /* move the NULL terminator too */
508 : :
509 : 94 : memmove(issuer + start_offset, issuer + end_offset, end_len);
510 : :
511 : 94 : return issuer;
512 : : }
513 : :
514 : : /*
515 : : * Parses the server error result (RFC 7628, Sec. 3.2.2) contained in msg and
516 : : * stores any discovered openid_configuration and scope settings for the
517 : : * connection.
518 : : */
519 : : static bool
520 : 80 : handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen)
521 : : {
522 : : JsonLexContext *lex;
523 : 80 : JsonSemAction sem = {0};
524 : : JsonParseErrorType err;
525 : 80 : struct json_ctx ctx = {0};
526 : 80 : char *errmsg = NULL;
527 : 80 : bool success = false;
528 : :
529 [ - + ]: 80 : Assert(conn->oauth_issuer_id); /* ensured by setup_oauth_parameters() */
530 : :
531 : : /* Sanity check. */
532 [ - + ]: 80 : if (strlen(msg) != msglen)
533 : : {
439 dgustafsson@postgres 534 :UBC 0 : libpq_append_conn_error(conn,
535 : : "server's error message contained an embedded NULL, and was discarded");
536 : 0 : return false;
537 : : }
538 : :
539 : : /*
540 : : * pg_parse_json doesn't validate the incoming UTF-8, so we have to check
541 : : * that up front.
542 : : */
439 dgustafsson@postgres 543 [ - + ]:CBC 80 : if (pg_encoding_verifymbstr(PG_UTF8, msg, msglen) != msglen)
544 : : {
439 dgustafsson@postgres 545 :UBC 0 : libpq_append_conn_error(conn,
546 : : "server's error response is not valid UTF-8");
547 : 0 : return false;
548 : : }
549 : :
377 dgustafsson@postgres 550 :CBC 80 : lex = makeJsonLexContextCstringLen(NULL, msg, msglen, PG_UTF8, true);
551 : 80 : setJsonLexContextOwnsTokens(lex, true); /* must not leak on error */
552 : :
439 553 : 80 : initPQExpBuffer(&ctx.errbuf);
554 : 80 : sem.semstate = &ctx;
555 : :
556 : 80 : sem.object_start = oauth_json_object_start;
557 : 80 : sem.object_end = oauth_json_object_end;
558 : 80 : sem.object_field_start = oauth_json_object_field_start;
559 : 80 : sem.array_start = oauth_json_array_start;
347 jchampion@postgresql 560 : 80 : sem.array_end = oauth_json_array_end;
439 dgustafsson@postgres 561 : 80 : sem.scalar = oauth_json_scalar;
562 : :
377 563 : 80 : err = pg_parse_json(lex, &sem);
564 : :
439 565 [ - + ]: 80 : if (err == JSON_SEM_ACTION_FAILED)
566 : : {
439 dgustafsson@postgres 567 [ # # ]:UBC 0 : if (PQExpBufferDataBroken(ctx.errbuf))
568 : 0 : errmsg = libpq_gettext("out of memory");
569 [ # # ]: 0 : else if (ctx.errmsg)
570 : 0 : errmsg = ctx.errmsg;
571 : : else
572 : : {
573 : : /*
574 : : * Developer error: one of the action callbacks didn't call
575 : : * oauth_json_set_error() before erroring out.
576 : : */
577 [ # # # # ]: 0 : Assert(oauth_json_has_error(&ctx));
578 : 0 : errmsg = "<unexpected empty error>";
579 : : }
580 : : }
439 dgustafsson@postgres 581 [ - + ]:CBC 80 : else if (err != JSON_SUCCESS)
377 dgustafsson@postgres 582 :UBC 0 : errmsg = json_errdetail(err, lex);
583 : :
439 dgustafsson@postgres 584 [ - + ]:CBC 80 : if (errmsg)
439 dgustafsson@postgres 585 :UBC 0 : libpq_append_conn_error(conn,
586 : : "failed to parse server's error response: %s",
587 : : errmsg);
588 : :
589 : : /* Don't need the error buffer or the JSON lexer anymore. */
439 dgustafsson@postgres 590 :CBC 80 : termPQExpBuffer(&ctx.errbuf);
377 591 : 80 : freeJsonLexContext(lex);
592 : :
439 593 [ - + ]: 80 : if (errmsg)
439 dgustafsson@postgres 594 :UBC 0 : goto cleanup;
595 : :
439 dgustafsson@postgres 596 [ + - ]:CBC 80 : if (ctx.discovery_uri)
597 : : {
598 : : char *discovery_issuer;
599 : :
600 : : /*
601 : : * The URI MUST correspond to our existing issuer, to avoid mix-ups.
602 : : *
603 : : * Issuer comparison is done byte-wise, rather than performing any URL
604 : : * normalization; this follows the suggestions for issuer comparison
605 : : * in RFC 9207 Sec. 2.4 (which requires simple string comparison) and
606 : : * vastly simplifies things. Since this is the key protection against
607 : : * a rogue server sending the client to an untrustworthy location,
608 : : * simpler is better.
609 : : */
610 : 80 : discovery_issuer = issuer_from_well_known_uri(conn, ctx.discovery_uri);
611 [ + + ]: 80 : if (!discovery_issuer)
612 : 2 : goto cleanup; /* error message already set */
613 : :
614 [ + + ]: 78 : if (strcmp(conn->oauth_issuer_id, discovery_issuer) != 0)
615 : : {
616 : 1 : libpq_append_conn_error(conn,
617 : : "server's discovery document at %s (issuer \"%s\") is incompatible with oauth_issuer (%s)",
618 : : ctx.discovery_uri, discovery_issuer,
619 : : conn->oauth_issuer_id);
620 : :
621 : 1 : free(discovery_issuer);
622 : 1 : goto cleanup;
623 : : }
624 : :
625 : 77 : free(discovery_issuer);
626 : :
627 [ + + ]: 77 : if (!conn->oauth_discovery_uri)
628 : : {
629 : 55 : conn->oauth_discovery_uri = ctx.discovery_uri;
630 : 55 : ctx.discovery_uri = NULL;
631 : : }
632 : : else
633 : : {
634 : : /* This must match the URI we'd previously determined. */
635 [ - + ]: 22 : if (strcmp(conn->oauth_discovery_uri, ctx.discovery_uri) != 0)
636 : : {
439 dgustafsson@postgres 637 :UBC 0 : libpq_append_conn_error(conn,
638 : : "server's discovery document has moved to %s (previous location was %s)",
639 : : ctx.discovery_uri,
640 : : conn->oauth_discovery_uri);
641 : 0 : goto cleanup;
642 : : }
643 : : }
644 : : }
645 : :
439 dgustafsson@postgres 646 [ + - ]:CBC 77 : if (ctx.scope)
647 : : {
648 : : /* Servers may not override a previously set oauth_scope. */
649 [ + + ]: 77 : if (!conn->oauth_scope)
650 : : {
651 : 56 : conn->oauth_scope = ctx.scope;
652 : 56 : ctx.scope = NULL;
653 : : }
654 : : }
655 : :
656 [ - + ]: 77 : if (!ctx.status)
657 : : {
439 dgustafsson@postgres 658 :UBC 0 : libpq_append_conn_error(conn,
659 : : "server sent error response without a status");
660 : 0 : goto cleanup;
661 : : }
662 : :
439 dgustafsson@postgres 663 [ - + ]:CBC 77 : if (strcmp(ctx.status, "invalid_token") != 0)
664 : : {
665 : : /*
666 : : * invalid_token is the only error code we'll automatically retry for;
667 : : * otherwise, just bail out now.
668 : : */
439 dgustafsson@postgres 669 :UBC 0 : libpq_append_conn_error(conn,
670 : : "server rejected OAuth bearer token: %s",
671 : : ctx.status);
672 : 0 : goto cleanup;
673 : : }
674 : :
439 dgustafsson@postgres 675 :CBC 77 : success = true;
676 : :
677 : 80 : cleanup:
678 : 80 : free(ctx.status);
679 : 80 : free(ctx.scope);
680 : 80 : free(ctx.discovery_uri);
681 : :
682 : 80 : return success;
683 : : }
684 : :
685 : : /*
686 : : * Helper for handling flow failures. If anything was put into request->error,
687 : : * it's added to conn->errorMessage here.
688 : : */
689 : : static void
53 jchampion@postgresql 690 :GNC 19 : report_flow_error(PGconn *conn, const PGoauthBearerRequestV2 *request)
691 : : {
692 : 19 : fe_oauth_state *state = conn->sasl_state;
693 : 19 : const char *errmsg = request->error;
694 : :
695 : : /*
696 : : * User-defined flows are called out explicitly so that the user knows who
697 : : * to blame. Builtin flows don't need that extra message length; we expect
698 : : * them to always fill in request->error on failure anyway.
699 : : */
700 [ + + ]: 19 : if (state->builtin)
701 : : {
702 [ - + ]: 15 : if (!errmsg)
703 : : {
704 : : /*
705 : : * Don't turn a bug here into a crash in production, but don't
706 : : * bother translating either.
707 : : */
53 jchampion@postgresql 708 :UNC 0 : Assert(false);
709 : : errmsg = "builtin flow failed but did not provide an error message";
710 : : }
711 : :
53 jchampion@postgresql 712 :GNC 15 : appendPQExpBufferStr(&conn->errorMessage, errmsg);
713 : : }
714 : : else
715 : : {
716 : 4 : appendPQExpBufferStr(&conn->errorMessage,
717 : 4 : libpq_gettext("user-defined OAuth flow failed"));
718 [ + + ]: 4 : if (errmsg)
719 : : {
720 : 2 : appendPQExpBufferStr(&conn->errorMessage, ": ");
721 : 2 : appendPQExpBufferStr(&conn->errorMessage, errmsg);
722 : : }
723 : : }
724 : :
60 725 : 19 : appendPQExpBufferChar(&conn->errorMessage, '\n');
726 : 19 : }
727 : :
728 : : /*
729 : : * Callback implementation of conn->async_auth() for OAuth flows. Delegates the
730 : : * retrieval of the token to the PGoauthBearerRequestV2.async() callback.
731 : : *
732 : : * This will be called multiple times as needed; the callback is responsible for
733 : : * setting an altsock to signal and returning the correct PGRES_POLLING_*
734 : : * statuses for use by PQconnectPoll().
735 : : */
736 : : static PostgresPollingStatusType
53 737 : 340372 : run_oauth_flow(PGconn *conn)
738 : : {
439 dgustafsson@postgres 739 :CBC 340372 : fe_oauth_state *state = conn->sasl_state;
60 jchampion@postgresql 740 :GNC 340372 : PGoauthBearerRequestV2 *request = state->async_ctx;
741 : : PostgresPollingStatusType status;
742 : :
743 [ + + ]: 340372 : if (!request->v1.async)
744 : : {
53 745 [ - + ]: 2 : Assert(!state->builtin); /* be very noisy if our code does this */
439 dgustafsson@postgres 746 :CBC 2 : libpq_append_conn_error(conn,
747 : : "user-defined OAuth flow provided neither a token nor an async callback");
748 : 2 : return PGRES_POLLING_FAILED;
749 : : }
750 : :
35 jchampion@postgresql 751 :GNC 340370 : status = do_async(state, request);
752 : :
439 dgustafsson@postgres 753 [ + + ]:CBC 340370 : if (status == PGRES_POLLING_FAILED)
754 : : {
53 jchampion@postgresql 755 :GNC 18 : report_flow_error(conn, request);
439 dgustafsson@postgres 756 :CBC 18 : return status;
757 : : }
758 [ + + ]: 340352 : else if (status == PGRES_POLLING_OK)
759 : : {
760 : : /*
761 : : * We already have a token, so copy it into the conn. (We can't hold
762 : : * onto the original string, since it may not be safe for us to free()
763 : : * it.)
764 : : */
60 jchampion@postgresql 765 [ + + ]:GNC 42 : if (!request->v1.token)
766 : : {
53 767 [ - + ]: 2 : Assert(!state->builtin);
439 dgustafsson@postgres 768 :CBC 2 : libpq_append_conn_error(conn,
769 : : "user-defined OAuth flow did not provide a token");
770 : 2 : return PGRES_POLLING_FAILED;
771 : : }
772 : :
60 jchampion@postgresql 773 :GNC 40 : conn->oauth_token = strdup(request->v1.token);
439 dgustafsson@postgres 774 [ - + ]:GBC 40 : if (!conn->oauth_token)
775 : : {
439 dgustafsson@postgres 776 :UBC 0 : libpq_append_conn_error(conn, "out of memory");
777 : 0 : return PGRES_POLLING_FAILED;
778 : : }
779 : :
439 dgustafsson@postgres 780 :GBC 40 : return PGRES_POLLING_OK;
781 : : }
782 : :
783 : : /* The hook wants the client to poll the altsock. Make sure it set one. */
439 dgustafsson@postgres 784 [ + + ]:CBC 340310 : if (conn->altsock == PGINVALID_SOCKET)
785 : : {
53 jchampion@postgresql 786 [ - + ]:GNC 2 : Assert(!state->builtin);
439 dgustafsson@postgres 787 :CBC 2 : libpq_append_conn_error(conn,
788 : : "user-defined OAuth flow did not provide a socket for polling");
789 : 2 : return PGRES_POLLING_FAILED;
790 : : }
791 : :
792 : 340308 : return status;
793 : : }
794 : :
795 : : /*
796 : : * Cleanup callback for the async flow. Delegates most of its job to
797 : : * PGoauthBearerRequest.cleanup(), then disconnects the altsock and frees the
798 : : * request itself.
799 : : *
800 : : * This is called either at the end of a successful authentication, or during
801 : : * pqDropConnection(), so we won't leak resources even if PQconnectPoll() never
802 : : * calls us back.
803 : : */
804 : : static void
53 jchampion@postgresql 805 :GNC 67 : cleanup_oauth_flow(PGconn *conn)
806 : : {
439 dgustafsson@postgres 807 :CBC 67 : fe_oauth_state *state = conn->sasl_state;
60 jchampion@postgresql 808 :GNC 67 : PGoauthBearerRequestV2 *request = state->async_ctx;
809 : :
439 dgustafsson@postgres 810 [ - + ]:CBC 67 : Assert(request);
811 : :
35 jchampion@postgresql 812 :GNC 67 : do_cleanup(state, request);
439 dgustafsson@postgres 813 :CBC 67 : conn->altsock = PGINVALID_SOCKET;
814 : :
815 : 67 : free(request);
816 : 67 : state->async_ctx = NULL;
817 : 67 : }
818 : :
819 : : /*-------------
820 : : * Builtin Flow
821 : : *
822 : : * There are three potential implementations of use_builtin_flow:
823 : : *
824 : : * 1) If the OAuth client is disabled at configuration time, return zero.
825 : : * Dependent clients must provide their own flow.
826 : : * 2) If the OAuth client is enabled and USE_DYNAMIC_OAUTH is defined, dlopen()
827 : : * the libpq-oauth plugin and use its implementation.
828 : : * 3) Otherwise, use flow callbacks that are statically linked into the
829 : : * executable.
830 : : *
831 : : * For caller convenience, the return value follows the convention of
832 : : * PQauthDataHook: zero means no implementation is provided, negative indicates
833 : : * failure, and positive indicates success.
834 : : */
835 : :
836 : : #if !defined(USE_LIBCURL)
837 : :
838 : : /*
839 : : * This configuration doesn't support the builtin flow.
840 : : */
841 : :
842 : : static int
843 : : use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request)
844 : : {
845 : : return 0;
846 : : }
847 : :
848 : : #elif defined(USE_DYNAMIC_OAUTH)
849 : :
850 : : /*
851 : : * Use the builtin flow in the libpq-oauth plugin, which is loaded at runtime.
852 : : */
853 : :
854 : : typedef char *(*libpq_gettext_func) (const char *msgid);
855 : :
856 : : /*
857 : : * Loads the libpq-oauth plugin via dlopen(), initializes it, and plugs its
858 : : * callbacks into the connection's async auth handlers.
859 : : *
860 : : * Failure to load here results in a relatively quiet connection error, to
861 : : * handle the use case where the build supports loading a flow but a user does
862 : : * not want to install it. Troubleshooting of linker/loader failures can be done
863 : : * via PGOAUTHDEBUG.
864 : : *
865 : : * The lifetime of *request ends shortly after this call, so it must be copied
866 : : * to longer-lived storage.
867 : : */
868 : : static int
53 jchampion@postgresql 869 :GNC 57 : use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request)
870 : : {
871 : : static bool initialized = false;
872 : : static pthread_mutex_t init_mutex = PTHREAD_MUTEX_INITIALIZER;
873 : : int lockerr;
874 : :
875 : : void (*init) (libpq_gettext_func gettext_impl);
876 : : int (*start_flow) (PGconn *conn, PGoauthBearerRequestV2 *request);
877 : :
878 : : /*
879 : : * On macOS only, load the module using its absolute install path; the
880 : : * standard search behavior is not very helpful for this use case. Unlike
881 : : * on other platforms, DYLD_LIBRARY_PATH is used as a fallback even with
882 : : * absolute paths (modulo SIP effects), so tests can continue to work.
883 : : *
884 : : * On the other platforms, load the module using only the basename, to
885 : : * rely on the runtime linker's standard search behavior.
886 : : */
369 jchampion@postgresql 887 :CBC 57 : const char *const module_name =
888 : : #if defined(__darwin__)
889 : : LIBDIR "/libpq-oauth" DLSUFFIX;
890 : : #else
891 : : "libpq-oauth" DLSUFFIX;
892 : : #endif
893 : :
35 jchampion@postgresql 894 :GNC 57 : state->flow_module = dlopen(module_name, RTLD_NOW | RTLD_LOCAL);
895 [ - + ]: 57 : if (!state->flow_module)
896 : : {
897 : : /*
898 : : * For end users, this probably isn't an error condition, it just
899 : : * means the flow isn't installed. Developers and package maintainers
900 : : * may want to debug this via the PGOAUTHDEBUG envvar, though.
901 : : *
902 : : * Note that POSIX dlerror() isn't guaranteed to be threadsafe.
903 : : */
28 jchampion@postgresql 904 [ # # ]:UNC 0 : if (oauth_parse_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
369 jchampion@postgresql 905 :UBC 0 : fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror());
906 : :
53 jchampion@postgresql 907 :UNC 0 : return 0;
908 : : }
909 : :
910 : : /*
911 : : * Our libpq-oauth.so provides a special initialization function for libpq
912 : : * integration. If we don't find this, assume that a custom module is in
913 : : * use instead.
914 : : */
35 jchampion@postgresql 915 :GNC 57 : init = dlsym(state->flow_module, "libpq_oauth_init");
916 [ - + ]: 57 : if (!init)
35 jchampion@postgresql 917 :UNC 0 : state->builtin = false; /* adjust our error messages */
918 : :
35 jchampion@postgresql 919 [ - + ]:GNC 57 : if ((start_flow = dlsym(state->flow_module, "pg_start_oauthbearer")) == NULL)
920 : : {
921 : : /*
922 : : * This is more of an error condition than the one above, but the
923 : : * cause is still locked behind PGOAUTHDEBUG due to the dlerror()
924 : : * threadsafety issue.
925 : : */
28 jchampion@postgresql 926 [ # # ]:UNC 0 : if (oauth_parse_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS)
369 jchampion@postgresql 927 :UBC 0 : fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror());
928 : :
35 jchampion@postgresql 929 :UNC 0 : dlclose(state->flow_module);
930 : 0 : state->flow_module = NULL;
931 : :
53 932 : 0 : request->error = libpq_gettext("could not find entry point for libpq-oauth");
933 : 0 : return -1;
934 : : }
935 : :
936 : : /*
937 : : * Past this point, we do not unload the module. It stays in the process
938 : : * permanently.
939 : : */
940 : :
35 jchampion@postgresql 941 [ + - ]:GNC 57 : if (init)
942 : : {
943 : : /*
944 : : * We need to inject necessary function pointers into the module. This
945 : : * only needs to be done once -- even if the pointers are constant,
946 : : * assigning them while another thread is executing the flows feels
947 : : * like tempting fate.
948 : : */
949 [ - + ]: 57 : if ((lockerr = pthread_mutex_lock(&init_mutex)) != 0)
950 : : {
951 : : /* Should not happen... but don't continue if it does. */
35 jchampion@postgresql 952 :UNC 0 : Assert(false);
953 : :
954 : : appendPQExpBuffer(&conn->errorMessage,
955 : : "use_builtin_flow: failed to lock mutex (%d)\n",
956 : : lockerr);
957 : :
958 : : request->error = ""; /* satisfy report_flow_error() */
959 : : return -1;
960 : : }
961 : :
35 jchampion@postgresql 962 [ + - ]:GNC 57 : if (!initialized)
963 : : {
964 : 57 : init(
965 : : #ifdef ENABLE_NLS
966 : : libpq_gettext
967 : : #else
968 : : NULL
969 : : #endif
970 : : );
971 : :
972 : 57 : initialized = true;
973 : : }
974 : :
975 : 57 : pthread_mutex_unlock(&init_mutex);
976 : : }
977 : :
53 978 [ + - ]: 57 : return (start_flow(conn, request) == 0) ? 1 : -1;
979 : : }
980 : :
981 : : #else
982 : :
983 : : /*
984 : : * For static builds, we can just call pg_start_oauthbearer() directly. It's
985 : : * provided by libpq-oauth.a.
986 : : */
987 : :
988 : : extern int pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request);
989 : :
990 : : static int
53 jchampion@postgresql 991 :UNC 0 : use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request)
992 : : {
993 [ # # ]: 0 : return (pg_start_oauthbearer(conn, request) == 0) ? 1 : -1;
994 : : }
995 : :
996 : : #endif /* USE_LIBCURL */
997 : :
998 : :
999 : : /*
1000 : : * Chooses an OAuth client flow for the connection, which will retrieve a Bearer
1001 : : * token for presentation to the server.
1002 : : *
1003 : : * If the application has registered a custom flow handler using
1004 : : * PQAUTHDATA_OAUTH_BEARER_TOKEN[_V2], it may either return a token immediately
1005 : : * (e.g. if it has one cached for immediate use), or set up for a series of
1006 : : * asynchronous callbacks which will be managed by run_oauth_flow().
1007 : : *
1008 : : * If the default handler is used instead, a Device Authorization flow is used
1009 : : * for the connection if support has been compiled in. (See oauth-curl.c for
1010 : : * implementation details.)
1011 : : *
1012 : : * If neither a custom handler nor the builtin flow is available, the connection
1013 : : * fails here.
1014 : : */
1015 : : static bool
439 dgustafsson@postgres 1016 :CBC 71 : setup_token_request(PGconn *conn, fe_oauth_state *state)
1017 : : {
1018 : : int res;
60 jchampion@postgresql 1019 :GNC 71 : PGoauthBearerRequestV2 request = {
1020 : : .v1 = {
1021 : 71 : .openid_configuration = conn->oauth_discovery_uri,
1022 : 71 : .scope = conn->oauth_scope,
1023 : : },
1024 : 71 : .issuer = conn->oauth_issuer_id,
1025 : : };
1026 : :
1027 [ - + ]: 71 : Assert(request.v1.openid_configuration);
1028 [ - + ]: 71 : Assert(request.issuer);
1029 : :
1030 : : /*
1031 : : * The client may have overridden the OAuth flow. Try the v2 hook first,
1032 : : * then fall back to the v1 implementation. If neither is available, try
1033 : : * the builtin flow.
1034 : : */
1035 : 71 : res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN_V2, conn, &request);
1036 [ + + ]: 71 : if (res == 0)
1037 : : {
35 1038 : 62 : poison_req_v2(&request, true);
1039 : :
60 1040 : 62 : res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request);
35 1041 : 62 : state->v1 = (res != 0);
1042 : :
1043 : 62 : poison_req_v2(&request, false);
1044 : : }
53 1045 [ + + ]: 71 : if (res == 0)
1046 : : {
1047 : 57 : state->builtin = true;
1048 : 57 : res = use_builtin_flow(conn, state, &request);
1049 : : }
1050 : :
439 dgustafsson@postgres 1051 [ + + ]:CBC 71 : if (res > 0)
1052 : : {
1053 : : PGoauthBearerRequestV2 *request_copy;
1054 : :
60 jchampion@postgresql 1055 [ + + ]:GNC 70 : if (request.v1.token)
1056 : : {
1057 : : /*
1058 : : * We already have a token, so copy it into the conn. (We can't
1059 : : * hold onto the original string, since it may not be safe for us
1060 : : * to free() it.)
1061 : : */
1062 : 3 : conn->oauth_token = strdup(request.v1.token);
439 dgustafsson@postgres 1063 [ - + ]:CBC 3 : if (!conn->oauth_token)
1064 : : {
439 dgustafsson@postgres 1065 :UBC 0 : libpq_append_conn_error(conn, "out of memory");
1066 : 0 : goto fail;
1067 : : }
1068 : :
1069 : : /* short-circuit */
35 jchampion@postgresql 1070 :GNC 3 : do_cleanup(state, &request);
439 dgustafsson@postgres 1071 :CBC 3 : return true;
1072 : : }
1073 : :
1074 : 67 : request_copy = malloc(sizeof(*request_copy));
1075 [ - + ]: 67 : if (!request_copy)
1076 : : {
439 dgustafsson@postgres 1077 :UBC 0 : libpq_append_conn_error(conn, "out of memory");
1078 : 0 : goto fail;
1079 : : }
1080 : :
412 tmunro@postgresql.or 1081 :CBC 67 : *request_copy = request;
1082 : :
53 jchampion@postgresql 1083 :GNC 67 : conn->async_auth = run_oauth_flow;
1084 : 67 : conn->cleanup_async_auth = cleanup_oauth_flow;
439 dgustafsson@postgres 1085 :CBC 67 : state->async_ctx = request_copy;
1086 : :
53 jchampion@postgresql 1087 :GNC 67 : return true;
1088 : : }
1089 : :
1090 : : /*
1091 : : * Failure cases: either we tried to set up a flow and failed, or there
1092 : : * was no flow to try.
1093 : : */
1094 [ + - ]: 1 : if (res < 0)
1095 : 1 : report_flow_error(conn, &request);
1096 : : else
53 jchampion@postgresql 1097 :UNC 0 : libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)");
1098 : :
439 dgustafsson@postgres 1099 :GBC 1 : fail:
35 jchampion@postgresql 1100 :GNC 1 : do_cleanup(state, &request);
439 dgustafsson@postgres 1101 :GBC 1 : return false;
1102 : : }
1103 : :
1104 : : /*
1105 : : * Fill in our issuer identifier (and discovery URI, if possible) using the
1106 : : * connection parameters. If conn->oauth_discovery_uri can't be populated in
1107 : : * this function, it will be requested from the server.
1108 : : */
1109 : : static bool
439 dgustafsson@postgres 1110 :CBC 116 : setup_oauth_parameters(PGconn *conn)
1111 : : {
1112 : : /*
1113 : : * This is the only function that sets conn->oauth_issuer_id. If a
1114 : : * previous connection attempt has already computed it, don't overwrite it
1115 : : * or the discovery URI. (There's no reason for them to change once
1116 : : * they're set, and handle_oauth_sasl_error() will fail the connection if
1117 : : * the server attempts to switch them on us later.)
1118 : : */
1119 [ + + ]: 116 : if (conn->oauth_issuer_id)
1120 : 42 : return true;
1121 : :
1122 : : /*---
1123 : : * To talk to a server, we require the user to provide issuer and client
1124 : : * identifiers.
1125 : : *
1126 : : * While it's possible for an OAuth client to support multiple issuers, it
1127 : : * requires additional effort to make sure the flows in use are safe -- to
1128 : : * quote RFC 9207,
1129 : : *
1130 : : * OAuth clients that interact with only one authorization server are
1131 : : * not vulnerable to mix-up attacks. However, when such clients decide
1132 : : * to add support for a second authorization server in the future, they
1133 : : * become vulnerable and need to apply countermeasures to mix-up
1134 : : * attacks.
1135 : : *
1136 : : * For now, we allow only one.
1137 : : */
1138 [ + - - + ]: 74 : if (!conn->oauth_issuer || !conn->oauth_client_id)
1139 : : {
439 dgustafsson@postgres 1140 :UBC 0 : libpq_append_conn_error(conn,
1141 : : "server requires OAuth authentication, but oauth_issuer and oauth_client_id are not both set");
1142 : 0 : return false;
1143 : : }
1144 : :
1145 : : /*
1146 : : * oauth_issuer is interpreted differently if it's a well-known discovery
1147 : : * URI rather than just an issuer identifier.
1148 : : */
439 dgustafsson@postgres 1149 [ + + ]:CBC 74 : if (strstr(conn->oauth_issuer, WK_PREFIX) != NULL)
1150 : : {
1151 : : /*
1152 : : * Convert the URI back to an issuer identifier. (This also performs
1153 : : * validation of the URI format.)
1154 : : */
1155 : 32 : conn->oauth_issuer_id = issuer_from_well_known_uri(conn,
1156 : 16 : conn->oauth_issuer);
1157 [ - + ]: 16 : if (!conn->oauth_issuer_id)
439 dgustafsson@postgres 1158 :UBC 0 : return false; /* error message already set */
1159 : :
439 dgustafsson@postgres 1160 :CBC 16 : conn->oauth_discovery_uri = strdup(conn->oauth_issuer);
1161 [ - + ]: 16 : if (!conn->oauth_discovery_uri)
1162 : : {
439 dgustafsson@postgres 1163 :UBC 0 : libpq_append_conn_error(conn, "out of memory");
1164 : 0 : return false;
1165 : : }
1166 : : }
1167 : : else
1168 : : {
1169 : : /*
1170 : : * Treat oauth_issuer as an issuer identifier. We'll ask the server
1171 : : * for the discovery URI.
1172 : : */
439 dgustafsson@postgres 1173 :CBC 58 : conn->oauth_issuer_id = strdup(conn->oauth_issuer);
1174 [ - + ]: 58 : if (!conn->oauth_issuer_id)
1175 : : {
439 dgustafsson@postgres 1176 :UBC 0 : libpq_append_conn_error(conn, "out of memory");
1177 : 0 : return false;
1178 : : }
1179 : : }
1180 : :
439 dgustafsson@postgres 1181 :CBC 74 : return true;
1182 : : }
1183 : :
1184 : : /*
1185 : : * Implements the OAUTHBEARER SASL exchange (RFC 7628, Sec. 3.2).
1186 : : *
1187 : : * If the necessary OAuth parameters are set up on the connection, this will run
1188 : : * the client flow asynchronously and present the resulting token to the server.
1189 : : * Otherwise, an empty discovery response will be sent and any parameters sent
1190 : : * back by the server will be stored for a second attempt.
1191 : : *
1192 : : * For a full description of the API, see libpq/sasl.h.
1193 : : */
1194 : : static SASLStatus
1195 : 236 : oauth_exchange(void *opaq, bool final,
1196 : : char *input, int inputlen,
1197 : : char **output, int *outputlen)
1198 : : {
1199 : 236 : fe_oauth_state *state = opaq;
1200 : 236 : PGconn *conn = state->conn;
1201 : 236 : bool discover = false;
1202 : :
1203 : 236 : *output = NULL;
1204 : 236 : *outputlen = 0;
1205 : :
1206 [ + + + - : 236 : switch (state->step)
- ]
1207 : : {
1208 : 116 : case FE_OAUTH_INIT:
1209 : : /* We begin in the initial response phase. */
1210 [ - + ]: 116 : Assert(inputlen == -1);
1211 : :
1212 [ - + ]: 116 : if (!setup_oauth_parameters(conn))
439 dgustafsson@postgres 1213 :UBC 0 : return SASL_FAILED;
1214 : :
439 dgustafsson@postgres 1215 [ + + ]:CBC 116 : if (conn->oauth_token)
1216 : : {
1217 : : /*
1218 : : * A previous connection already fetched the token; we'll use
1219 : : * it below.
1220 : : */
1221 : : }
1222 [ + + ]: 74 : else if (conn->oauth_discovery_uri)
1223 : : {
1224 : : /*
1225 : : * We don't have a token, but we have a discovery URI already
1226 : : * stored. Decide whether we're using a user-provided OAuth
1227 : : * flow or the one we have built in.
1228 : : */
1229 [ - + ]: 16 : if (!setup_token_request(conn, state))
439 dgustafsson@postgres 1230 :UBC 0 : return SASL_FAILED;
1231 : :
439 dgustafsson@postgres 1232 [ + + ]:CBC 16 : if (conn->oauth_token)
1233 : : {
1234 : : /*
1235 : : * A really smart user implementation may have already
1236 : : * given us the token (e.g. if there was an unexpired copy
1237 : : * already cached), and we can use it immediately.
1238 : : */
1239 : : }
1240 : : else
1241 : : {
1242 : : /*
1243 : : * Otherwise, we'll have to hand the connection over to
1244 : : * our OAuth implementation.
1245 : : *
1246 : : * This could take a while, since it generally involves a
1247 : : * user in the loop. To avoid consuming the server's
1248 : : * authentication timeout, we'll continue this handshake
1249 : : * to the end, so that the server can close its side of
1250 : : * the connection. We'll open a second connection later
1251 : : * once we've retrieved a token.
1252 : : */
1253 : 15 : discover = true;
1254 : : }
1255 : : }
1256 : : else
1257 : : {
1258 : : /*
1259 : : * If we don't have a token, and we don't have a discovery URI
1260 : : * to be able to request a token, we ask the server for one
1261 : : * explicitly.
1262 : : */
1263 : 58 : discover = true;
1264 : : }
1265 : :
1266 : : /*
1267 : : * Generate an initial response. This either contains a token, if
1268 : : * we have one, or an empty discovery response which is doomed to
1269 : : * fail.
1270 : : */
1271 : 116 : *output = client_initial_response(conn, discover);
1272 [ - + ]: 116 : if (!*output)
439 dgustafsson@postgres 1273 :UBC 0 : return SASL_FAILED;
1274 : :
439 dgustafsson@postgres 1275 :CBC 116 : *outputlen = strlen(*output);
1276 : 116 : state->step = FE_OAUTH_BEARER_SENT;
1277 : :
1278 [ + + ]: 116 : if (conn->oauth_token)
1279 : : {
1280 : : /*
1281 : : * For the purposes of require_auth, our side of
1282 : : * authentication is done at this point; the server will
1283 : : * either accept the connection or send an error. Unlike
1284 : : * SCRAM, there is no additional server data to check upon
1285 : : * success.
1286 : : */
1287 : 43 : conn->client_finished_auth = true;
1288 : : }
1289 : :
1290 : 116 : return SASL_CONTINUE;
1291 : :
1292 : 80 : case FE_OAUTH_BEARER_SENT:
1293 [ - + ]: 80 : if (final)
1294 : : {
1295 : : /*
1296 : : * OAUTHBEARER does not make use of additional data with a
1297 : : * successful SASL exchange, so we shouldn't get an
1298 : : * AuthenticationSASLFinal message.
1299 : : */
439 dgustafsson@postgres 1300 :UBC 0 : libpq_append_conn_error(conn,
1301 : : "server sent unexpected additional OAuth data");
1302 : 0 : return SASL_FAILED;
1303 : : }
1304 : :
1305 : : /*
1306 : : * An error message was sent by the server. Respond with the
1307 : : * required dummy message (RFC 7628, sec. 3.2.3).
1308 : : */
439 dgustafsson@postgres 1309 :CBC 80 : *output = strdup(kvsep);
1310 [ - + ]: 80 : if (unlikely(!*output))
1311 : : {
439 dgustafsson@postgres 1312 :UBC 0 : libpq_append_conn_error(conn, "out of memory");
1313 : 0 : return SASL_FAILED;
1314 : : }
439 dgustafsson@postgres 1315 :CBC 80 : *outputlen = strlen(*output); /* == 1 */
1316 : :
1317 : : /* Grab the settings from discovery. */
1318 [ + + ]: 80 : if (!handle_oauth_sasl_error(conn, input, inputlen))
1319 : 3 : return SASL_FAILED;
1320 : :
1321 [ + + ]: 77 : if (conn->oauth_token)
1322 : : {
1323 : : /*
1324 : : * The server rejected our token. Continue onwards towards the
1325 : : * expected FATAL message, but mark our state to catch any
1326 : : * unexpected "success" from the server.
1327 : : */
1328 : 9 : state->step = FE_OAUTH_SERVER_ERROR;
1329 : 9 : return SASL_CONTINUE;
1330 : : }
1331 : :
1332 [ + + ]: 68 : if (!conn->async_auth)
1333 : : {
1334 : : /*
1335 : : * No OAuth flow is set up yet. Did we get enough information
1336 : : * from the server to create one?
1337 : : */
1338 [ - + ]: 55 : if (!conn->oauth_discovery_uri)
1339 : : {
439 dgustafsson@postgres 1340 :UBC 0 : libpq_append_conn_error(conn,
1341 : : "server requires OAuth authentication, but no discovery metadata was provided");
1342 : 0 : return SASL_FAILED;
1343 : : }
1344 : :
1345 : : /* Yes. Set up the flow now. */
439 dgustafsson@postgres 1346 [ + + ]:CBC 55 : if (!setup_token_request(conn, state))
439 dgustafsson@postgres 1347 :GBC 1 : return SASL_FAILED;
1348 : :
439 dgustafsson@postgres 1349 [ + + ]:CBC 54 : if (conn->oauth_token)
1350 : : {
1351 : : /*
1352 : : * A token was available in a custom flow's cache. Skip
1353 : : * the asynchronous processing.
1354 : : */
1355 : 2 : goto reconnect;
1356 : : }
1357 : : }
1358 : :
1359 : : /*
1360 : : * Time to retrieve a token. This involves a number of HTTP
1361 : : * connections and timed waits, so we escape the synchronous auth
1362 : : * processing and tell PQconnectPoll to transfer control to our
1363 : : * async implementation.
1364 : : */
1365 [ - + ]: 65 : Assert(conn->async_auth); /* should have been set already */
1366 : 65 : state->step = FE_OAUTH_REQUESTING_TOKEN;
1367 : 65 : return SASL_ASYNC;
1368 : :
1369 : 40 : case FE_OAUTH_REQUESTING_TOKEN:
1370 : :
1371 : : /*
1372 : : * We've returned successfully from token retrieval. Double-check
1373 : : * that we have what we need for the next connection.
1374 : : */
1375 [ - + ]: 40 : if (!conn->oauth_token)
1376 : : {
439 dgustafsson@postgres 1377 :UBC 0 : Assert(false); /* should have failed before this point! */
1378 : : libpq_append_conn_error(conn,
1379 : : "internal error: OAuth flow did not set a token");
1380 : : return SASL_FAILED;
1381 : : }
1382 : :
439 dgustafsson@postgres 1383 :CBC 40 : goto reconnect;
1384 : :
439 dgustafsson@postgres 1385 :UBC 0 : case FE_OAUTH_SERVER_ERROR:
1386 : :
1387 : : /*
1388 : : * After an error, the server should send an error response to
1389 : : * fail the SASL handshake, which is handled in higher layers.
1390 : : *
1391 : : * If we get here, the server either sent *another* challenge
1392 : : * which isn't defined in the RFC, or completed the handshake
1393 : : * successfully after telling us it was going to fail. Neither is
1394 : : * acceptable.
1395 : : */
1396 : 0 : libpq_append_conn_error(conn,
1397 : : "server sent additional OAuth data after error");
1398 : 0 : return SASL_FAILED;
1399 : :
1400 : 0 : default:
1401 : 0 : libpq_append_conn_error(conn, "invalid OAuth exchange state");
1402 : 0 : break;
1403 : : }
1404 : :
1405 : 0 : Assert(false); /* should never get here */
1406 : : return SASL_FAILED;
1407 : :
439 dgustafsson@postgres 1408 :CBC 42 : reconnect:
1409 : :
1410 : : /*
1411 : : * Despite being a failure from the point of view of SASL, we have enough
1412 : : * information to restart with a new connection.
1413 : : */
1414 : 42 : libpq_append_conn_error(conn, "retrying connection with new bearer token");
1415 : 42 : conn->oauth_want_retry = true;
1416 : 42 : return SASL_FAILED;
1417 : : }
1418 : :
1419 : : static bool
439 dgustafsson@postgres 1420 :UBC 0 : oauth_channel_bound(void *opaq)
1421 : : {
1422 : : /* This mechanism does not support channel binding. */
1423 : 0 : return false;
1424 : : }
1425 : :
1426 : : /*
1427 : : * Fully clears out any stored OAuth token. This is done proactively upon
1428 : : * successful connection as well as during pqClosePGconn().
1429 : : */
1430 : : void
439 dgustafsson@postgres 1431 :CBC 30732 : pqClearOAuthToken(PGconn *conn)
1432 : : {
1433 [ + + ]: 30732 : if (!conn->oauth_token)
1434 : 30689 : return;
1435 : :
1436 : 43 : explicit_bzero(conn->oauth_token, strlen(conn->oauth_token));
1437 : 43 : free(conn->oauth_token);
1438 : 43 : conn->oauth_token = NULL;
1439 : : }
1440 : :
1441 : : /*
1442 : : * Hook v1 Poisoning
1443 : : *
1444 : : * Try to catch misuses of the v1 PQAUTHDATA_OAUTH_BEARER_TOKEN hook and its
1445 : : * callbacks, which are not allowed to downcast their request argument to
1446 : : * PGoauthBearerRequestV2. (Such clients may crash or worse when speaking to
1447 : : * libpq 18.)
1448 : : *
1449 : : * This attempts to use Valgrind hooks, if present, to mark the extra members as
1450 : : * inaccessible. For uninstrumented builds, it also munges request->issuer to
1451 : : * try to crash clients that perform string operations, and it aborts if
1452 : : * request->error is set.
1453 : : */
1454 : :
1455 : : #define MASK_BITS ((uintptr_t) 0x55aa55aa55aa55aa)
1456 : : #define POISON_MASK(ptr) ((void *) (((uintptr_t) ptr) ^ MASK_BITS))
1457 : :
1458 : : /*
1459 : : * Workhorse for v2 request poisoning. This must be called exactly twice: once
1460 : : * to poison, once to unpoison.
1461 : : *
1462 : : * NB: Unpoisoning must restore the request to its original state, because we
1463 : : * might still switch back to a v2 implementation internally. Don't do anything
1464 : : * destructive during the poison operation.
1465 : : */
1466 : : static void
35 jchampion@postgresql 1467 :GNC 130 : poison_req_v2(PGoauthBearerRequestV2 *request, bool poison)
1468 : : {
1469 : : #ifdef USE_VALGRIND
1470 : : void *const base = (char *) request + sizeof(request->v1);
1471 : : const size_t len = sizeof(*request) - sizeof(request->v1);
1472 : : #endif
1473 : :
1474 [ + + ]: 130 : if (poison)
1475 : : {
1476 : : /* Poison request->issuer with a mask to help uninstrumented builds. */
1477 : 65 : request->issuer = POISON_MASK(request->issuer);
1478 : :
1479 : : /*
1480 : : * We'll check to make sure request->error wasn't assigned when
1481 : : * unpoisoning, so it had better not be assigned now.
1482 : : */
1483 [ - + ]: 65 : Assert(!request->error);
1484 : :
1485 : : VALGRIND_MAKE_MEM_NOACCESS(base, len);
1486 : : }
1487 : : else
1488 : : {
1489 : : /*
1490 : : * XXX Using DEFINED here is technically too lax; we might catch
1491 : : * struct padding in the blast radius. But since this API has to
1492 : : * poison stack addresses, and Valgrind can't track/manage undefined
1493 : : * stack regions, we can't be any stricter without tracking the
1494 : : * original state of the memory.
1495 : : */
1496 : : VALGRIND_MAKE_MEM_DEFINED(base, len);
1497 : :
1498 : : /* Undo our mask. */
1499 : 65 : request->issuer = POISON_MASK(request->issuer);
1500 : :
1501 : : /*
1502 : : * For uninstrumented builds, make sure request->error wasn't touched.
1503 : : */
1504 [ - + ]: 65 : if (request->error)
1505 : : {
35 jchampion@postgresql 1506 :UNC 0 : fprintf(stderr,
1507 : : "abort! out-of-bounds write to PGoauthBearerRequest by PQAUTHDATA_OAUTH_BEARER_TOKEN hook\n");
1508 : 0 : abort();
1509 : : }
1510 : : }
35 jchampion@postgresql 1511 :GNC 130 : }
1512 : :
1513 : : /*
1514 : : * Wrapper around PGoauthBearerRequest.async() which applies poison during the
1515 : : * callback when necessary.
1516 : : */
1517 : : static PostgresPollingStatusType
1518 : 340370 : do_async(fe_oauth_state *state, PGoauthBearerRequestV2 *request)
1519 : : {
1520 : : PostgresPollingStatusType ret;
1521 : 340370 : PGconn *conn = state->conn;
1522 : :
1523 [ - + ]: 340370 : Assert(request->v1.async);
1524 : :
1525 [ + + ]: 340370 : if (state->v1)
1526 : 3 : poison_req_v2(request, true);
1527 : :
1528 : 340370 : ret = request->v1.async(conn,
1529 : : (PGoauthBearerRequest *) request,
1530 : 340370 : &conn->altsock);
1531 : :
1532 [ + + ]: 340370 : if (state->v1)
1533 : 3 : poison_req_v2(request, false);
1534 : :
1535 : 340370 : return ret;
1536 : : }
1537 : :
1538 : : /*
1539 : : * Similar wrapper for the optional PGoauthBearerRequest.cleanup() callback.
1540 : : * Does nothing if one is not defined.
1541 : : */
1542 : : static void
1543 : 71 : do_cleanup(fe_oauth_state *state, PGoauthBearerRequestV2 *request)
1544 : : {
1545 [ + + ]: 71 : if (!request->v1.cleanup)
1546 : 14 : return;
1547 : :
1548 [ - + ]: 57 : if (state->v1)
35 jchampion@postgresql 1549 :UNC 0 : poison_req_v2(request, true);
1550 : :
35 jchampion@postgresql 1551 :GNC 57 : request->v1.cleanup(state->conn, (PGoauthBearerRequest *) request);
1552 : :
1553 [ - + ]: 57 : if (state->v1)
35 jchampion@postgresql 1554 :UNC 0 : poison_req_v2(request, false);
1555 : : }
|