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-2025, 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 : #include "common/base64.h"
19 : #include "common/hmac.h"
20 : #include "common/jsonapi.h"
21 : #include "common/oauth-common.h"
22 : #include "fe-auth.h"
23 : #include "fe-auth-oauth.h"
24 : #include "mb/pg_wchar.h"
25 :
26 : /* The exported OAuth callback mechanism. */
27 : static void *oauth_init(PGconn *conn, const char *password,
28 : const char *sasl_mechanism);
29 : static SASLStatus oauth_exchange(void *opaq, bool final,
30 : char *input, int inputlen,
31 : char **output, int *outputlen);
32 : static bool oauth_channel_bound(void *opaq);
33 : static void oauth_free(void *opaq);
34 :
35 : const pg_fe_sasl_mech pg_oauth_mech = {
36 : oauth_init,
37 : oauth_exchange,
38 : oauth_channel_bound,
39 : oauth_free,
40 : };
41 :
42 : /*
43 : * Initializes mechanism state for OAUTHBEARER.
44 : *
45 : * For a full description of the API, see libpq/fe-auth-sasl.h.
46 : */
47 : static void *
48 0 : oauth_init(PGconn *conn, const char *password,
49 : const char *sasl_mechanism)
50 : {
51 : fe_oauth_state *state;
52 :
53 : /*
54 : * We only support one SASL mechanism here; anything else is programmer
55 : * error.
56 : */
57 : Assert(sasl_mechanism != NULL);
58 : Assert(strcmp(sasl_mechanism, OAUTHBEARER_NAME) == 0);
59 :
60 0 : state = calloc(1, sizeof(*state));
61 0 : if (!state)
62 0 : return NULL;
63 :
64 0 : state->step = FE_OAUTH_INIT;
65 0 : state->conn = conn;
66 :
67 0 : return state;
68 : }
69 :
70 : /*
71 : * Frees the state allocated by oauth_init().
72 : *
73 : * This handles only mechanism state tied to the connection lifetime; state
74 : * stored in state->async_ctx is freed up either immediately after the
75 : * authentication handshake succeeds, or before the mechanism is cleaned up on
76 : * failure. See pg_fe_cleanup_oauth_flow() and cleanup_user_oauth_flow().
77 : */
78 : static void
79 0 : oauth_free(void *opaq)
80 : {
81 0 : fe_oauth_state *state = opaq;
82 :
83 : /* Any async authentication state should have been cleaned up already. */
84 : Assert(!state->async_ctx);
85 :
86 0 : free(state);
87 0 : }
88 :
89 : #define kvsep "\x01"
90 :
91 : /*
92 : * Constructs an OAUTHBEARER client initial response (RFC 7628, Sec. 3.1).
93 : *
94 : * If discover is true, the initial response will contain a request for the
95 : * server's required OAuth parameters (Sec. 4.3). Otherwise, conn->token must
96 : * be set; it will be sent as the connection's bearer token.
97 : *
98 : * Returns the response as a null-terminated string, or NULL on error.
99 : */
100 : static char *
101 0 : client_initial_response(PGconn *conn, bool discover)
102 : {
103 : static const char *const resp_format = "n,," kvsep "auth=%s%s" kvsep kvsep;
104 :
105 : PQExpBufferData buf;
106 : const char *authn_scheme;
107 0 : char *response = NULL;
108 0 : const char *token = conn->oauth_token;
109 :
110 0 : if (discover)
111 : {
112 : /* Parameter discovery uses a completely empty auth value. */
113 0 : authn_scheme = token = "";
114 : }
115 : else
116 : {
117 : /*
118 : * Use a Bearer authentication scheme (RFC 6750, Sec. 2.1). A trailing
119 : * space is used as a separator.
120 : */
121 0 : authn_scheme = "Bearer ";
122 :
123 : /* conn->token must have been set in this case. */
124 0 : if (!token)
125 : {
126 : Assert(false);
127 0 : libpq_append_conn_error(conn,
128 : "internal error: no OAuth token was set for the connection");
129 0 : return NULL;
130 : }
131 : }
132 :
133 0 : initPQExpBuffer(&buf);
134 0 : appendPQExpBuffer(&buf, resp_format, authn_scheme, token);
135 :
136 0 : if (!PQExpBufferDataBroken(buf))
137 0 : response = strdup(buf.data);
138 0 : termPQExpBuffer(&buf);
139 :
140 0 : if (!response)
141 0 : libpq_append_conn_error(conn, "out of memory");
142 :
143 0 : return response;
144 : }
145 :
146 : /*
147 : * JSON Parser (for the OAUTHBEARER error result)
148 : */
149 :
150 : /* Relevant JSON fields in the error result object. */
151 : #define ERROR_STATUS_FIELD "status"
152 : #define ERROR_SCOPE_FIELD "scope"
153 : #define ERROR_OPENID_CONFIGURATION_FIELD "openid-configuration"
154 :
155 : struct json_ctx
156 : {
157 : char *errmsg; /* any non-NULL value stops all processing */
158 : PQExpBufferData errbuf; /* backing memory for errmsg */
159 : int nested; /* nesting level (zero is the top) */
160 :
161 : const char *target_field_name; /* points to a static allocation */
162 : char **target_field; /* see below */
163 :
164 : /* target_field, if set, points to one of the following: */
165 : char *status;
166 : char *scope;
167 : char *discovery_uri;
168 : };
169 :
170 : #define oauth_json_has_error(ctx) \
171 : (PQExpBufferDataBroken((ctx)->errbuf) || (ctx)->errmsg)
172 :
173 : #define oauth_json_set_error(ctx, ...) \
174 : do { \
175 : appendPQExpBuffer(&(ctx)->errbuf, __VA_ARGS__); \
176 : (ctx)->errmsg = (ctx)->errbuf.data; \
177 : } while (0)
178 :
179 : static JsonParseErrorType
180 0 : oauth_json_object_start(void *state)
181 : {
182 0 : struct json_ctx *ctx = state;
183 :
184 0 : if (ctx->target_field)
185 : {
186 : Assert(ctx->nested == 1);
187 :
188 0 : oauth_json_set_error(ctx,
189 : libpq_gettext("field \"%s\" must be a string"),
190 : ctx->target_field_name);
191 : }
192 :
193 0 : ++ctx->nested;
194 0 : return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
195 : }
196 :
197 : static JsonParseErrorType
198 0 : oauth_json_object_end(void *state)
199 : {
200 0 : struct json_ctx *ctx = state;
201 :
202 0 : --ctx->nested;
203 0 : return JSON_SUCCESS;
204 : }
205 :
206 : static JsonParseErrorType
207 0 : oauth_json_object_field_start(void *state, char *name, bool isnull)
208 : {
209 0 : struct json_ctx *ctx = state;
210 :
211 : /* Only top-level keys are considered. */
212 0 : if (ctx->nested == 1)
213 : {
214 0 : if (strcmp(name, ERROR_STATUS_FIELD) == 0)
215 : {
216 0 : ctx->target_field_name = ERROR_STATUS_FIELD;
217 0 : ctx->target_field = &ctx->status;
218 : }
219 0 : else if (strcmp(name, ERROR_SCOPE_FIELD) == 0)
220 : {
221 0 : ctx->target_field_name = ERROR_SCOPE_FIELD;
222 0 : ctx->target_field = &ctx->scope;
223 : }
224 0 : else if (strcmp(name, ERROR_OPENID_CONFIGURATION_FIELD) == 0)
225 : {
226 0 : ctx->target_field_name = ERROR_OPENID_CONFIGURATION_FIELD;
227 0 : ctx->target_field = &ctx->discovery_uri;
228 : }
229 : }
230 :
231 0 : return JSON_SUCCESS;
232 : }
233 :
234 : static JsonParseErrorType
235 0 : oauth_json_array_start(void *state)
236 : {
237 0 : struct json_ctx *ctx = state;
238 :
239 0 : if (!ctx->nested)
240 : {
241 0 : ctx->errmsg = libpq_gettext("top-level element must be an object");
242 : }
243 0 : else if (ctx->target_field)
244 : {
245 : Assert(ctx->nested == 1);
246 :
247 0 : oauth_json_set_error(ctx,
248 : libpq_gettext("field \"%s\" must be a string"),
249 : ctx->target_field_name);
250 : }
251 :
252 0 : return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
253 : }
254 :
255 : static JsonParseErrorType
256 0 : oauth_json_scalar(void *state, char *token, JsonTokenType type)
257 : {
258 0 : struct json_ctx *ctx = state;
259 :
260 0 : if (!ctx->nested)
261 : {
262 0 : ctx->errmsg = libpq_gettext("top-level element must be an object");
263 0 : return JSON_SEM_ACTION_FAILED;
264 : }
265 :
266 0 : if (ctx->target_field)
267 : {
268 0 : if (ctx->nested != 1)
269 : {
270 : /*
271 : * ctx->target_field should not have been set for nested keys.
272 : * Assert and don't continue any further for production builds.
273 : */
274 : Assert(false);
275 0 : oauth_json_set_error(ctx,
276 : "internal error: target scalar found at nesting level %d during OAUTHBEARER parsing",
277 : ctx->nested);
278 0 : return JSON_SEM_ACTION_FAILED;
279 : }
280 :
281 : /*
282 : * We don't allow duplicate field names; error out if the target has
283 : * already been set.
284 : */
285 0 : if (*ctx->target_field)
286 : {
287 0 : oauth_json_set_error(ctx,
288 : libpq_gettext("field \"%s\" is duplicated"),
289 : ctx->target_field_name);
290 0 : return JSON_SEM_ACTION_FAILED;
291 : }
292 :
293 : /* The only fields we support are strings. */
294 0 : if (type != JSON_TOKEN_STRING)
295 : {
296 0 : oauth_json_set_error(ctx,
297 : libpq_gettext("field \"%s\" must be a string"),
298 : ctx->target_field_name);
299 0 : return JSON_SEM_ACTION_FAILED;
300 : }
301 :
302 0 : *ctx->target_field = strdup(token);
303 0 : if (!*ctx->target_field)
304 0 : return JSON_OUT_OF_MEMORY;
305 :
306 0 : ctx->target_field = NULL;
307 0 : ctx->target_field_name = NULL;
308 : }
309 : else
310 : {
311 : /* otherwise we just ignore it */
312 : }
313 :
314 0 : return JSON_SUCCESS;
315 : }
316 :
317 : #define HTTPS_SCHEME "https://"
318 : #define HTTP_SCHEME "http://"
319 :
320 : /* We support both well-known suffixes defined by RFC 8414. */
321 : #define WK_PREFIX "/.well-known/"
322 : #define OPENID_WK_SUFFIX "openid-configuration"
323 : #define OAUTH_WK_SUFFIX "oauth-authorization-server"
324 :
325 : /*
326 : * Derives an issuer identifier from one of our recognized .well-known URIs,
327 : * using the rules in RFC 8414.
328 : */
329 : static char *
330 0 : issuer_from_well_known_uri(PGconn *conn, const char *wkuri)
331 : {
332 0 : const char *authority_start = NULL;
333 : const char *wk_start;
334 : const char *wk_end;
335 : char *issuer;
336 : ptrdiff_t start_offset,
337 : end_offset;
338 : size_t end_len;
339 :
340 : /*
341 : * https:// is required for issuer identifiers (RFC 8414, Sec. 2; OIDC
342 : * Discovery 1.0, Sec. 3). This is a case-insensitive comparison at this
343 : * level (but issuer identifier comparison at the level above this is
344 : * case-sensitive, so in practice it's probably moot).
345 : */
346 0 : if (pg_strncasecmp(wkuri, HTTPS_SCHEME, strlen(HTTPS_SCHEME)) == 0)
347 0 : authority_start = wkuri + strlen(HTTPS_SCHEME);
348 :
349 0 : if (!authority_start
350 0 : && oauth_unsafe_debugging_enabled()
351 0 : && pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0)
352 : {
353 : /* Allow http:// for testing only. */
354 0 : authority_start = wkuri + strlen(HTTP_SCHEME);
355 : }
356 :
357 0 : if (!authority_start)
358 : {
359 0 : libpq_append_conn_error(conn,
360 : "OAuth discovery URI \"%s\" must use HTTPS",
361 : wkuri);
362 0 : return NULL;
363 : }
364 :
365 : /*
366 : * Well-known URIs in general may support queries and fragments, but the
367 : * two types we support here do not. (They must be constructed from the
368 : * components of issuer identifiers, which themselves may not contain any
369 : * queries or fragments.)
370 : *
371 : * It's important to check this first, to avoid getting tricked later by a
372 : * prefix buried inside a query or fragment.
373 : */
374 0 : if (strpbrk(authority_start, "?#") != NULL)
375 : {
376 0 : libpq_append_conn_error(conn,
377 : "OAuth discovery URI \"%s\" must not contain query or fragment components",
378 : wkuri);
379 0 : return NULL;
380 : }
381 :
382 : /*
383 : * Find the start of the .well-known prefix. IETF rules (RFC 8615) state
384 : * this must be at the beginning of the path component, but OIDC defined
385 : * it at the end instead (OIDC Discovery 1.0, Sec. 4), so we have to
386 : * search for it anywhere.
387 : */
388 0 : wk_start = strstr(authority_start, WK_PREFIX);
389 0 : if (!wk_start)
390 : {
391 0 : libpq_append_conn_error(conn,
392 : "OAuth discovery URI \"%s\" is not a .well-known URI",
393 : wkuri);
394 0 : return NULL;
395 : }
396 :
397 : /*
398 : * Now find the suffix type. We only support the two defined in OIDC
399 : * Discovery 1.0 and RFC 8414.
400 : */
401 0 : wk_end = wk_start + strlen(WK_PREFIX);
402 :
403 0 : if (strncmp(wk_end, OPENID_WK_SUFFIX, strlen(OPENID_WK_SUFFIX)) == 0)
404 0 : wk_end += strlen(OPENID_WK_SUFFIX);
405 0 : else if (strncmp(wk_end, OAUTH_WK_SUFFIX, strlen(OAUTH_WK_SUFFIX)) == 0)
406 0 : wk_end += strlen(OAUTH_WK_SUFFIX);
407 : else
408 0 : wk_end = NULL;
409 :
410 : /*
411 : * Even if there's a match, we still need to check to make sure the suffix
412 : * takes up the entire path segment, to weed out constructions like
413 : * "/.well-known/openid-configuration-bad".
414 : */
415 0 : if (!wk_end || (*wk_end != '/' && *wk_end != '\0'))
416 : {
417 0 : libpq_append_conn_error(conn,
418 : "OAuth discovery URI \"%s\" uses an unsupported .well-known suffix",
419 : wkuri);
420 0 : return NULL;
421 : }
422 :
423 : /*
424 : * Finally, make sure the .well-known components are provided either as a
425 : * prefix (IETF style) or as a postfix (OIDC style). In other words,
426 : * "https://localhost/a/.well-known/openid-configuration/b" is not allowed
427 : * to claim association with "https://localhost/a/b".
428 : */
429 0 : if (*wk_end != '\0')
430 : {
431 : /*
432 : * It's not at the end, so it's required to be at the beginning at the
433 : * path. Find the starting slash.
434 : */
435 : const char *path_start;
436 :
437 0 : path_start = strchr(authority_start, '/');
438 : Assert(path_start); /* otherwise we wouldn't have found WK_PREFIX */
439 :
440 0 : if (wk_start != path_start)
441 : {
442 0 : libpq_append_conn_error(conn,
443 : "OAuth discovery URI \"%s\" uses an invalid format",
444 : wkuri);
445 0 : return NULL;
446 : }
447 : }
448 :
449 : /* Checks passed! Now build the issuer. */
450 0 : issuer = strdup(wkuri);
451 0 : if (!issuer)
452 : {
453 0 : libpq_append_conn_error(conn, "out of memory");
454 0 : return NULL;
455 : }
456 :
457 : /*
458 : * The .well-known components are from [wk_start, wk_end). Remove those to
459 : * form the issuer ID, by shifting the path suffix (which may be empty)
460 : * leftwards.
461 : */
462 0 : start_offset = wk_start - wkuri;
463 0 : end_offset = wk_end - wkuri;
464 0 : end_len = strlen(wk_end) + 1; /* move the NULL terminator too */
465 :
466 0 : memmove(issuer + start_offset, issuer + end_offset, end_len);
467 :
468 0 : return issuer;
469 : }
470 :
471 : /*
472 : * Parses the server error result (RFC 7628, Sec. 3.2.2) contained in msg and
473 : * stores any discovered openid_configuration and scope settings for the
474 : * connection.
475 : */
476 : static bool
477 0 : handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen)
478 : {
479 0 : JsonLexContext lex = {0};
480 0 : JsonSemAction sem = {0};
481 : JsonParseErrorType err;
482 0 : struct json_ctx ctx = {0};
483 0 : char *errmsg = NULL;
484 0 : bool success = false;
485 :
486 : Assert(conn->oauth_issuer_id); /* ensured by setup_oauth_parameters() */
487 :
488 : /* Sanity check. */
489 0 : if (strlen(msg) != msglen)
490 : {
491 0 : libpq_append_conn_error(conn,
492 : "server's error message contained an embedded NULL, and was discarded");
493 0 : return false;
494 : }
495 :
496 : /*
497 : * pg_parse_json doesn't validate the incoming UTF-8, so we have to check
498 : * that up front.
499 : */
500 0 : if (pg_encoding_verifymbstr(PG_UTF8, msg, msglen) != msglen)
501 : {
502 0 : libpq_append_conn_error(conn,
503 : "server's error response is not valid UTF-8");
504 0 : return false;
505 : }
506 :
507 0 : makeJsonLexContextCstringLen(&lex, msg, msglen, PG_UTF8, true);
508 0 : setJsonLexContextOwnsTokens(&lex, true); /* must not leak on error */
509 :
510 0 : initPQExpBuffer(&ctx.errbuf);
511 0 : sem.semstate = &ctx;
512 :
513 0 : sem.object_start = oauth_json_object_start;
514 0 : sem.object_end = oauth_json_object_end;
515 0 : sem.object_field_start = oauth_json_object_field_start;
516 0 : sem.array_start = oauth_json_array_start;
517 0 : sem.scalar = oauth_json_scalar;
518 :
519 0 : err = pg_parse_json(&lex, &sem);
520 :
521 0 : if (err == JSON_SEM_ACTION_FAILED)
522 : {
523 0 : if (PQExpBufferDataBroken(ctx.errbuf))
524 0 : errmsg = libpq_gettext("out of memory");
525 0 : else if (ctx.errmsg)
526 0 : errmsg = ctx.errmsg;
527 : else
528 : {
529 : /*
530 : * Developer error: one of the action callbacks didn't call
531 : * oauth_json_set_error() before erroring out.
532 : */
533 : Assert(oauth_json_has_error(&ctx));
534 0 : errmsg = "<unexpected empty error>";
535 : }
536 : }
537 0 : else if (err != JSON_SUCCESS)
538 0 : errmsg = json_errdetail(err, &lex);
539 :
540 0 : if (errmsg)
541 0 : libpq_append_conn_error(conn,
542 : "failed to parse server's error response: %s",
543 : errmsg);
544 :
545 : /* Don't need the error buffer or the JSON lexer anymore. */
546 0 : termPQExpBuffer(&ctx.errbuf);
547 0 : freeJsonLexContext(&lex);
548 :
549 0 : if (errmsg)
550 0 : goto cleanup;
551 :
552 0 : if (ctx.discovery_uri)
553 : {
554 : char *discovery_issuer;
555 :
556 : /*
557 : * The URI MUST correspond to our existing issuer, to avoid mix-ups.
558 : *
559 : * Issuer comparison is done byte-wise, rather than performing any URL
560 : * normalization; this follows the suggestions for issuer comparison
561 : * in RFC 9207 Sec. 2.4 (which requires simple string comparison) and
562 : * vastly simplifies things. Since this is the key protection against
563 : * a rogue server sending the client to an untrustworthy location,
564 : * simpler is better.
565 : */
566 0 : discovery_issuer = issuer_from_well_known_uri(conn, ctx.discovery_uri);
567 0 : if (!discovery_issuer)
568 0 : goto cleanup; /* error message already set */
569 :
570 0 : if (strcmp(conn->oauth_issuer_id, discovery_issuer) != 0)
571 : {
572 0 : libpq_append_conn_error(conn,
573 : "server's discovery document at %s (issuer \"%s\") is incompatible with oauth_issuer (%s)",
574 : ctx.discovery_uri, discovery_issuer,
575 : conn->oauth_issuer_id);
576 :
577 0 : free(discovery_issuer);
578 0 : goto cleanup;
579 : }
580 :
581 0 : free(discovery_issuer);
582 :
583 0 : if (!conn->oauth_discovery_uri)
584 : {
585 0 : conn->oauth_discovery_uri = ctx.discovery_uri;
586 0 : ctx.discovery_uri = NULL;
587 : }
588 : else
589 : {
590 : /* This must match the URI we'd previously determined. */
591 0 : if (strcmp(conn->oauth_discovery_uri, ctx.discovery_uri) != 0)
592 : {
593 0 : libpq_append_conn_error(conn,
594 : "server's discovery document has moved to %s (previous location was %s)",
595 : ctx.discovery_uri,
596 : conn->oauth_discovery_uri);
597 0 : goto cleanup;
598 : }
599 : }
600 : }
601 :
602 0 : if (ctx.scope)
603 : {
604 : /* Servers may not override a previously set oauth_scope. */
605 0 : if (!conn->oauth_scope)
606 : {
607 0 : conn->oauth_scope = ctx.scope;
608 0 : ctx.scope = NULL;
609 : }
610 : }
611 :
612 0 : if (!ctx.status)
613 : {
614 0 : libpq_append_conn_error(conn,
615 : "server sent error response without a status");
616 0 : goto cleanup;
617 : }
618 :
619 0 : if (strcmp(ctx.status, "invalid_token") != 0)
620 : {
621 : /*
622 : * invalid_token is the only error code we'll automatically retry for;
623 : * otherwise, just bail out now.
624 : */
625 0 : libpq_append_conn_error(conn,
626 : "server rejected OAuth bearer token: %s",
627 : ctx.status);
628 0 : goto cleanup;
629 : }
630 :
631 0 : success = true;
632 :
633 0 : cleanup:
634 0 : free(ctx.status);
635 0 : free(ctx.scope);
636 0 : free(ctx.discovery_uri);
637 :
638 0 : return success;
639 : }
640 :
641 : /*
642 : * Callback implementation of conn->async_auth() for a user-defined OAuth flow.
643 : * Delegates the retrieval of the token to the application's async callback.
644 : *
645 : * This will be called multiple times as needed; the application is responsible
646 : * for setting an altsock to signal and returning the correct PGRES_POLLING_*
647 : * statuses for use by PQconnectPoll().
648 : */
649 : static PostgresPollingStatusType
650 0 : run_user_oauth_flow(PGconn *conn)
651 : {
652 0 : fe_oauth_state *state = conn->sasl_state;
653 0 : PGoauthBearerRequest *request = state->async_ctx;
654 : PostgresPollingStatusType status;
655 :
656 0 : if (!request->async)
657 : {
658 0 : libpq_append_conn_error(conn,
659 : "user-defined OAuth flow provided neither a token nor an async callback");
660 0 : return PGRES_POLLING_FAILED;
661 : }
662 :
663 0 : status = request->async(conn, request, &conn->altsock);
664 0 : if (status == PGRES_POLLING_FAILED)
665 : {
666 0 : libpq_append_conn_error(conn, "user-defined OAuth flow failed");
667 0 : return status;
668 : }
669 0 : else if (status == PGRES_POLLING_OK)
670 : {
671 : /*
672 : * We already have a token, so copy it into the conn. (We can't hold
673 : * onto the original string, since it may not be safe for us to free()
674 : * it.)
675 : */
676 0 : if (!request->token)
677 : {
678 0 : libpq_append_conn_error(conn,
679 : "user-defined OAuth flow did not provide a token");
680 0 : return PGRES_POLLING_FAILED;
681 : }
682 :
683 0 : conn->oauth_token = strdup(request->token);
684 0 : if (!conn->oauth_token)
685 : {
686 0 : libpq_append_conn_error(conn, "out of memory");
687 0 : return PGRES_POLLING_FAILED;
688 : }
689 :
690 0 : return PGRES_POLLING_OK;
691 : }
692 :
693 : /* The hook wants the client to poll the altsock. Make sure it set one. */
694 0 : if (conn->altsock == PGINVALID_SOCKET)
695 : {
696 0 : libpq_append_conn_error(conn,
697 : "user-defined OAuth flow did not provide a socket for polling");
698 0 : return PGRES_POLLING_FAILED;
699 : }
700 :
701 0 : return status;
702 : }
703 :
704 : /*
705 : * Cleanup callback for the async user flow. Delegates most of its job to the
706 : * user-provided cleanup implementation, then disconnects the altsock.
707 : */
708 : static void
709 0 : cleanup_user_oauth_flow(PGconn *conn)
710 : {
711 0 : fe_oauth_state *state = conn->sasl_state;
712 0 : PGoauthBearerRequest *request = state->async_ctx;
713 :
714 : Assert(request);
715 :
716 0 : if (request->cleanup)
717 0 : request->cleanup(conn, request);
718 0 : conn->altsock = PGINVALID_SOCKET;
719 :
720 0 : free(request);
721 0 : state->async_ctx = NULL;
722 0 : }
723 :
724 : /*
725 : * Chooses an OAuth client flow for the connection, which will retrieve a Bearer
726 : * token for presentation to the server.
727 : *
728 : * If the application has registered a custom flow handler using
729 : * PQAUTHDATA_OAUTH_BEARER_TOKEN, it may either return a token immediately (e.g.
730 : * if it has one cached for immediate use), or set up for a series of
731 : * asynchronous callbacks which will be managed by run_user_oauth_flow().
732 : *
733 : * If the default handler is used instead, a Device Authorization flow is used
734 : * for the connection if support has been compiled in. (See
735 : * fe-auth-oauth-curl.c for implementation details.)
736 : *
737 : * If neither a custom handler nor the builtin flow is available, the connection
738 : * fails here.
739 : */
740 : static bool
741 0 : setup_token_request(PGconn *conn, fe_oauth_state *state)
742 : {
743 : int res;
744 0 : PGoauthBearerRequest request = {
745 0 : .openid_configuration = conn->oauth_discovery_uri,
746 0 : .scope = conn->oauth_scope,
747 : };
748 :
749 : Assert(request.openid_configuration);
750 :
751 : /* The client may have overridden the OAuth flow. */
752 0 : res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request);
753 0 : if (res > 0)
754 : {
755 : PGoauthBearerRequest *request_copy;
756 :
757 0 : if (request.token)
758 : {
759 : /*
760 : * We already have a token, so copy it into the conn. (We can't
761 : * hold onto the original string, since it may not be safe for us
762 : * to free() it.)
763 : */
764 0 : conn->oauth_token = strdup(request.token);
765 0 : if (!conn->oauth_token)
766 : {
767 0 : libpq_append_conn_error(conn, "out of memory");
768 0 : goto fail;
769 : }
770 :
771 : /* short-circuit */
772 0 : if (request.cleanup)
773 0 : request.cleanup(conn, &request);
774 0 : return true;
775 : }
776 :
777 0 : request_copy = malloc(sizeof(*request_copy));
778 0 : if (!request_copy)
779 : {
780 0 : libpq_append_conn_error(conn, "out of memory");
781 0 : goto fail;
782 : }
783 :
784 0 : memcpy(request_copy, &request, sizeof(request));
785 :
786 0 : conn->async_auth = run_user_oauth_flow;
787 0 : conn->cleanup_async_auth = cleanup_user_oauth_flow;
788 0 : state->async_ctx = request_copy;
789 : }
790 0 : else if (res < 0)
791 : {
792 0 : libpq_append_conn_error(conn, "user-defined OAuth flow failed");
793 0 : goto fail;
794 : }
795 : else
796 : {
797 : #if USE_LIBCURL
798 : /* Hand off to our built-in OAuth flow. */
799 : conn->async_auth = pg_fe_run_oauth_flow;
800 : conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow;
801 :
802 : #else
803 0 : libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built with libcurl support");
804 0 : goto fail;
805 :
806 : #endif
807 : }
808 :
809 0 : return true;
810 :
811 0 : fail:
812 0 : if (request.cleanup)
813 0 : request.cleanup(conn, &request);
814 0 : return false;
815 : }
816 :
817 : /*
818 : * Fill in our issuer identifier (and discovery URI, if possible) using the
819 : * connection parameters. If conn->oauth_discovery_uri can't be populated in
820 : * this function, it will be requested from the server.
821 : */
822 : static bool
823 0 : setup_oauth_parameters(PGconn *conn)
824 : {
825 : /*
826 : * This is the only function that sets conn->oauth_issuer_id. If a
827 : * previous connection attempt has already computed it, don't overwrite it
828 : * or the discovery URI. (There's no reason for them to change once
829 : * they're set, and handle_oauth_sasl_error() will fail the connection if
830 : * the server attempts to switch them on us later.)
831 : */
832 0 : if (conn->oauth_issuer_id)
833 0 : return true;
834 :
835 : /*---
836 : * To talk to a server, we require the user to provide issuer and client
837 : * identifiers.
838 : *
839 : * While it's possible for an OAuth client to support multiple issuers, it
840 : * requires additional effort to make sure the flows in use are safe -- to
841 : * quote RFC 9207,
842 : *
843 : * OAuth clients that interact with only one authorization server are
844 : * not vulnerable to mix-up attacks. However, when such clients decide
845 : * to add support for a second authorization server in the future, they
846 : * become vulnerable and need to apply countermeasures to mix-up
847 : * attacks.
848 : *
849 : * For now, we allow only one.
850 : */
851 0 : if (!conn->oauth_issuer || !conn->oauth_client_id)
852 : {
853 0 : libpq_append_conn_error(conn,
854 : "server requires OAuth authentication, but oauth_issuer and oauth_client_id are not both set");
855 0 : return false;
856 : }
857 :
858 : /*
859 : * oauth_issuer is interpreted differently if it's a well-known discovery
860 : * URI rather than just an issuer identifier.
861 : */
862 0 : if (strstr(conn->oauth_issuer, WK_PREFIX) != NULL)
863 : {
864 : /*
865 : * Convert the URI back to an issuer identifier. (This also performs
866 : * validation of the URI format.)
867 : */
868 0 : conn->oauth_issuer_id = issuer_from_well_known_uri(conn,
869 0 : conn->oauth_issuer);
870 0 : if (!conn->oauth_issuer_id)
871 0 : return false; /* error message already set */
872 :
873 0 : conn->oauth_discovery_uri = strdup(conn->oauth_issuer);
874 0 : if (!conn->oauth_discovery_uri)
875 : {
876 0 : libpq_append_conn_error(conn, "out of memory");
877 0 : return false;
878 : }
879 : }
880 : else
881 : {
882 : /*
883 : * Treat oauth_issuer as an issuer identifier. We'll ask the server
884 : * for the discovery URI.
885 : */
886 0 : conn->oauth_issuer_id = strdup(conn->oauth_issuer);
887 0 : if (!conn->oauth_issuer_id)
888 : {
889 0 : libpq_append_conn_error(conn, "out of memory");
890 0 : return false;
891 : }
892 : }
893 :
894 0 : return true;
895 : }
896 :
897 : /*
898 : * Implements the OAUTHBEARER SASL exchange (RFC 7628, Sec. 3.2).
899 : *
900 : * If the necessary OAuth parameters are set up on the connection, this will run
901 : * the client flow asynchronously and present the resulting token to the server.
902 : * Otherwise, an empty discovery response will be sent and any parameters sent
903 : * back by the server will be stored for a second attempt.
904 : *
905 : * For a full description of the API, see libpq/sasl.h.
906 : */
907 : static SASLStatus
908 0 : oauth_exchange(void *opaq, bool final,
909 : char *input, int inputlen,
910 : char **output, int *outputlen)
911 : {
912 0 : fe_oauth_state *state = opaq;
913 0 : PGconn *conn = state->conn;
914 0 : bool discover = false;
915 :
916 0 : *output = NULL;
917 0 : *outputlen = 0;
918 :
919 0 : switch (state->step)
920 : {
921 0 : case FE_OAUTH_INIT:
922 : /* We begin in the initial response phase. */
923 : Assert(inputlen == -1);
924 :
925 0 : if (!setup_oauth_parameters(conn))
926 0 : return SASL_FAILED;
927 :
928 0 : if (conn->oauth_token)
929 : {
930 : /*
931 : * A previous connection already fetched the token; we'll use
932 : * it below.
933 : */
934 : }
935 0 : else if (conn->oauth_discovery_uri)
936 : {
937 : /*
938 : * We don't have a token, but we have a discovery URI already
939 : * stored. Decide whether we're using a user-provided OAuth
940 : * flow or the one we have built in.
941 : */
942 0 : if (!setup_token_request(conn, state))
943 0 : return SASL_FAILED;
944 :
945 0 : if (conn->oauth_token)
946 : {
947 : /*
948 : * A really smart user implementation may have already
949 : * given us the token (e.g. if there was an unexpired copy
950 : * already cached), and we can use it immediately.
951 : */
952 : }
953 : else
954 : {
955 : /*
956 : * Otherwise, we'll have to hand the connection over to
957 : * our OAuth implementation.
958 : *
959 : * This could take a while, since it generally involves a
960 : * user in the loop. To avoid consuming the server's
961 : * authentication timeout, we'll continue this handshake
962 : * to the end, so that the server can close its side of
963 : * the connection. We'll open a second connection later
964 : * once we've retrieved a token.
965 : */
966 0 : discover = true;
967 : }
968 : }
969 : else
970 : {
971 : /*
972 : * If we don't have a token, and we don't have a discovery URI
973 : * to be able to request a token, we ask the server for one
974 : * explicitly.
975 : */
976 0 : discover = true;
977 : }
978 :
979 : /*
980 : * Generate an initial response. This either contains a token, if
981 : * we have one, or an empty discovery response which is doomed to
982 : * fail.
983 : */
984 0 : *output = client_initial_response(conn, discover);
985 0 : if (!*output)
986 0 : return SASL_FAILED;
987 :
988 0 : *outputlen = strlen(*output);
989 0 : state->step = FE_OAUTH_BEARER_SENT;
990 :
991 0 : if (conn->oauth_token)
992 : {
993 : /*
994 : * For the purposes of require_auth, our side of
995 : * authentication is done at this point; the server will
996 : * either accept the connection or send an error. Unlike
997 : * SCRAM, there is no additional server data to check upon
998 : * success.
999 : */
1000 0 : conn->client_finished_auth = true;
1001 : }
1002 :
1003 0 : return SASL_CONTINUE;
1004 :
1005 0 : case FE_OAUTH_BEARER_SENT:
1006 0 : if (final)
1007 : {
1008 : /*
1009 : * OAUTHBEARER does not make use of additional data with a
1010 : * successful SASL exchange, so we shouldn't get an
1011 : * AuthenticationSASLFinal message.
1012 : */
1013 0 : libpq_append_conn_error(conn,
1014 : "server sent unexpected additional OAuth data");
1015 0 : return SASL_FAILED;
1016 : }
1017 :
1018 : /*
1019 : * An error message was sent by the server. Respond with the
1020 : * required dummy message (RFC 7628, sec. 3.2.3).
1021 : */
1022 0 : *output = strdup(kvsep);
1023 0 : if (unlikely(!*output))
1024 : {
1025 0 : libpq_append_conn_error(conn, "out of memory");
1026 0 : return SASL_FAILED;
1027 : }
1028 0 : *outputlen = strlen(*output); /* == 1 */
1029 :
1030 : /* Grab the settings from discovery. */
1031 0 : if (!handle_oauth_sasl_error(conn, input, inputlen))
1032 0 : return SASL_FAILED;
1033 :
1034 0 : if (conn->oauth_token)
1035 : {
1036 : /*
1037 : * The server rejected our token. Continue onwards towards the
1038 : * expected FATAL message, but mark our state to catch any
1039 : * unexpected "success" from the server.
1040 : */
1041 0 : state->step = FE_OAUTH_SERVER_ERROR;
1042 0 : return SASL_CONTINUE;
1043 : }
1044 :
1045 0 : if (!conn->async_auth)
1046 : {
1047 : /*
1048 : * No OAuth flow is set up yet. Did we get enough information
1049 : * from the server to create one?
1050 : */
1051 0 : if (!conn->oauth_discovery_uri)
1052 : {
1053 0 : libpq_append_conn_error(conn,
1054 : "server requires OAuth authentication, but no discovery metadata was provided");
1055 0 : return SASL_FAILED;
1056 : }
1057 :
1058 : /* Yes. Set up the flow now. */
1059 0 : if (!setup_token_request(conn, state))
1060 0 : return SASL_FAILED;
1061 :
1062 0 : if (conn->oauth_token)
1063 : {
1064 : /*
1065 : * A token was available in a custom flow's cache. Skip
1066 : * the asynchronous processing.
1067 : */
1068 0 : goto reconnect;
1069 : }
1070 : }
1071 :
1072 : /*
1073 : * Time to retrieve a token. This involves a number of HTTP
1074 : * connections and timed waits, so we escape the synchronous auth
1075 : * processing and tell PQconnectPoll to transfer control to our
1076 : * async implementation.
1077 : */
1078 : Assert(conn->async_auth); /* should have been set already */
1079 0 : state->step = FE_OAUTH_REQUESTING_TOKEN;
1080 0 : return SASL_ASYNC;
1081 :
1082 0 : case FE_OAUTH_REQUESTING_TOKEN:
1083 :
1084 : /*
1085 : * We've returned successfully from token retrieval. Double-check
1086 : * that we have what we need for the next connection.
1087 : */
1088 0 : if (!conn->oauth_token)
1089 : {
1090 : Assert(false); /* should have failed before this point! */
1091 0 : libpq_append_conn_error(conn,
1092 : "internal error: OAuth flow did not set a token");
1093 0 : return SASL_FAILED;
1094 : }
1095 :
1096 0 : goto reconnect;
1097 :
1098 0 : case FE_OAUTH_SERVER_ERROR:
1099 :
1100 : /*
1101 : * After an error, the server should send an error response to
1102 : * fail the SASL handshake, which is handled in higher layers.
1103 : *
1104 : * If we get here, the server either sent *another* challenge
1105 : * which isn't defined in the RFC, or completed the handshake
1106 : * successfully after telling us it was going to fail. Neither is
1107 : * acceptable.
1108 : */
1109 0 : libpq_append_conn_error(conn,
1110 : "server sent additional OAuth data after error");
1111 0 : return SASL_FAILED;
1112 :
1113 0 : default:
1114 0 : libpq_append_conn_error(conn, "invalid OAuth exchange state");
1115 0 : break;
1116 : }
1117 :
1118 : Assert(false); /* should never get here */
1119 0 : return SASL_FAILED;
1120 :
1121 0 : reconnect:
1122 :
1123 : /*
1124 : * Despite being a failure from the point of view of SASL, we have enough
1125 : * information to restart with a new connection.
1126 : */
1127 0 : libpq_append_conn_error(conn, "retrying connection with new bearer token");
1128 0 : conn->oauth_want_retry = true;
1129 0 : return SASL_FAILED;
1130 : }
1131 :
1132 : static bool
1133 0 : oauth_channel_bound(void *opaq)
1134 : {
1135 : /* This mechanism does not support channel binding. */
1136 0 : return false;
1137 : }
1138 :
1139 : /*
1140 : * Fully clears out any stored OAuth token. This is done proactively upon
1141 : * successful connection as well as during pqClosePGconn().
1142 : */
1143 : void
1144 53026 : pqClearOAuthToken(PGconn *conn)
1145 : {
1146 53026 : if (!conn->oauth_token)
1147 53026 : return;
1148 :
1149 0 : explicit_bzero(conn->oauth_token, strlen(conn->oauth_token));
1150 0 : free(conn->oauth_token);
1151 0 : conn->oauth_token = NULL;
1152 : }
1153 :
1154 : /*
1155 : * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment.
1156 : */
1157 : bool
1158 0 : oauth_unsafe_debugging_enabled(void)
1159 : {
1160 0 : const char *env = getenv("PGOAUTHDEBUG");
1161 :
1162 0 : return (env && strcmp(env, "UNSAFE") == 0);
1163 : }
|