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