Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * auth-oauth.c
4 : : * Server-side implementation of the SASL OAUTHBEARER mechanism.
5 : : *
6 : : * See the following RFC for more details:
7 : : * - RFC 7628: https://datatracker.ietf.org/doc/html/rfc7628
8 : : *
9 : : * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
10 : : * Portions Copyright (c) 1994, Regents of the University of California
11 : : *
12 : : * src/backend/libpq/auth-oauth.c
13 : : *
14 : : *-------------------------------------------------------------------------
15 : : */
16 : : #include "postgres.h"
17 : :
18 : : #include <unistd.h>
19 : : #include <fcntl.h>
20 : :
21 : : #include "common/oauth-common.h"
22 : : #include "fmgr.h"
23 : : #include "lib/stringinfo.h"
24 : : #include "libpq/auth.h"
25 : : #include "libpq/hba.h"
26 : : #include "libpq/oauth.h"
27 : : #include "libpq/sasl.h"
28 : : #include "storage/fd.h"
29 : : #include "storage/ipc.h"
30 : : #include "utils/json.h"
31 : : #include "utils/varlena.h"
32 : :
33 : : /* GUC */
34 : : char *oauth_validator_libraries_string = NULL;
35 : :
36 : : static void oauth_get_mechanisms(Port *port, StringInfo buf);
37 : : static void *oauth_init(Port *port, const char *selected_mech, const char *shadow_pass);
38 : : static int oauth_exchange(void *opaq, const char *input, int inputlen,
39 : : char **output, int *outputlen, const char **logdetail);
40 : :
41 : : static void load_validator_library(const char *libname);
42 : : static void shutdown_validator_library(void *arg);
43 : :
44 : : static ValidatorModuleState *validator_module_state;
45 : : static const OAuthValidatorCallbacks *ValidatorCallbacks;
46 : :
47 : : /* Mechanism declaration */
48 : : const pg_be_sasl_mech pg_be_oauth_mech = {
49 : : .get_mechanisms = oauth_get_mechanisms,
50 : : .init = oauth_init,
51 : : .exchange = oauth_exchange,
52 : :
53 : : .max_message_length = PG_MAX_AUTH_TOKEN_LENGTH,
54 : : };
55 : :
56 : : /* Valid states for the oauth_exchange() machine. */
57 : : enum oauth_state
58 : : {
59 : : OAUTH_STATE_INIT = 0,
60 : : OAUTH_STATE_ERROR,
61 : : OAUTH_STATE_FINISHED,
62 : : };
63 : :
64 : : /* Mechanism callback state. */
65 : : struct oauth_ctx
66 : : {
67 : : enum oauth_state state;
68 : : Port *port;
69 : : const char *issuer;
70 : : const char *scope;
71 : : };
72 : :
73 : : static char *sanitize_char(char c);
74 : : static char *parse_kvpairs_for_auth(char **input);
75 : : static void generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen);
76 : : static bool validate(Port *port, const char *auth);
77 : :
78 : : /* Constants seen in an OAUTHBEARER client initial response. */
79 : : #define KVSEP 0x01 /* separator byte for key/value pairs */
80 : : #define AUTH_KEY "auth" /* key containing the Authorization header */
81 : : #define BEARER_SCHEME "Bearer " /* required header scheme (case-insensitive!) */
82 : :
83 : : /*
84 : : * Retrieves the OAUTHBEARER mechanism list (currently a single item).
85 : : *
86 : : * For a full description of the API, see libpq/sasl.h.
87 : : */
88 : : static void
198 dgustafsson@postgres 89 :CBC 94 : oauth_get_mechanisms(Port *port, StringInfo buf)
90 : : {
91 : : /* Only OAUTHBEARER is supported. */
92 : 94 : appendStringInfoString(buf, OAUTHBEARER_NAME);
93 : 94 : appendStringInfoChar(buf, '\0');
94 : 94 : }
95 : :
96 : : /*
97 : : * Initializes mechanism state and loads the configured validator module.
98 : : *
99 : : * For a full description of the API, see libpq/sasl.h.
100 : : */
101 : : static void *
102 : 89 : oauth_init(Port *port, const char *selected_mech, const char *shadow_pass)
103 : : {
104 : : struct oauth_ctx *ctx;
105 : :
106 [ - + ]: 89 : if (strcmp(selected_mech, OAUTHBEARER_NAME) != 0)
198 dgustafsson@postgres 107 [ # # ]:UBC 0 : ereport(ERROR,
108 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
109 : : errmsg("client selected an invalid SASL authentication mechanism"));
110 : :
198 dgustafsson@postgres 111 :CBC 89 : ctx = palloc0(sizeof(*ctx));
112 : :
113 : 89 : ctx->state = OAUTH_STATE_INIT;
114 : 89 : ctx->port = port;
115 : :
116 [ - + ]: 89 : Assert(port->hba);
117 : 89 : ctx->issuer = port->hba->oauth_issuer;
118 : 89 : ctx->scope = port->hba->oauth_scope;
119 : :
120 : 89 : load_validator_library(port->hba->oauth_validator);
121 : :
122 : 88 : return ctx;
123 : : }
124 : :
125 : : /*
126 : : * Implements the OAUTHBEARER SASL exchange (RFC 7628, Sec. 3.2). This pulls
127 : : * apart the client initial response and validates the Bearer token. It also
128 : : * handles the dummy error response for a failed handshake, as described in
129 : : * Sec. 3.2.3.
130 : : *
131 : : * For a full description of the API, see libpq/sasl.h.
132 : : */
133 : : static int
134 : 148 : oauth_exchange(void *opaq, const char *input, int inputlen,
135 : : char **output, int *outputlen, const char **logdetail)
136 : : {
137 : : char *input_copy;
138 : : char *p;
139 : : char cbind_flag;
140 : : char *auth;
141 : : int status;
142 : :
143 : 148 : struct oauth_ctx *ctx = opaq;
144 : :
145 : 148 : *output = NULL;
146 : 148 : *outputlen = -1;
147 : :
148 : : /*
149 : : * If the client didn't include an "Initial Client Response" in the
150 : : * SASLInitialResponse message, send an empty challenge, to which the
151 : : * client will respond with the same data that usually comes in the
152 : : * Initial Client Response.
153 : : */
154 [ - + ]: 148 : if (input == NULL)
155 : : {
198 dgustafsson@postgres 156 [ # # ]:UBC 0 : Assert(ctx->state == OAUTH_STATE_INIT);
157 : :
158 : 0 : *output = pstrdup("");
159 : 0 : *outputlen = 0;
160 : 0 : return PG_SASL_EXCHANGE_CONTINUE;
161 : : }
162 : :
163 : : /*
164 : : * Check that the input length agrees with the string length of the input.
165 : : */
198 dgustafsson@postgres 166 [ - + ]:CBC 148 : if (inputlen == 0)
198 dgustafsson@postgres 167 [ # # ]:UBC 0 : ereport(ERROR,
168 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
169 : : errmsg("malformed OAUTHBEARER message"),
170 : : errdetail("The message is empty."));
198 dgustafsson@postgres 171 [ - + ]:CBC 148 : if (inputlen != strlen(input))
198 dgustafsson@postgres 172 [ # # ]:UBC 0 : ereport(ERROR,
173 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
174 : : errmsg("malformed OAUTHBEARER message"),
175 : : errdetail("Message length does not match input length."));
176 : :
198 dgustafsson@postgres 177 [ + + - ]:CBC 148 : switch (ctx->state)
178 : : {
179 : 88 : case OAUTH_STATE_INIT:
180 : : /* Handle this case below. */
181 : 88 : break;
182 : :
183 : 60 : case OAUTH_STATE_ERROR:
184 : :
185 : : /*
186 : : * Only one response is valid for the client during authentication
187 : : * failure: a single kvsep.
188 : : */
189 [ + - - + ]: 60 : if (inputlen != 1 || *input != KVSEP)
198 dgustafsson@postgres 190 [ # # ]:UBC 0 : ereport(ERROR,
191 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
192 : : errmsg("malformed OAUTHBEARER message"),
193 : : errdetail("Client did not send a kvsep response."));
194 : :
195 : : /* The (failed) handshake is now complete. */
198 dgustafsson@postgres 196 :CBC 60 : ctx->state = OAUTH_STATE_FINISHED;
197 : 60 : return PG_SASL_EXCHANGE_FAILURE;
198 : :
198 dgustafsson@postgres 199 :UBC 0 : default:
200 [ # # ]: 0 : elog(ERROR, "invalid OAUTHBEARER exchange state");
201 : : return PG_SASL_EXCHANGE_FAILURE;
202 : : }
203 : :
204 : : /* Handle the client's initial message. */
198 dgustafsson@postgres 205 :CBC 88 : p = input_copy = pstrdup(input);
206 : :
207 : : /*
208 : : * OAUTHBEARER does not currently define a channel binding (so there is no
209 : : * OAUTHBEARER-PLUS, and we do not accept a 'p' specifier). We accept a
210 : : * 'y' specifier purely for the remote chance that a future specification
211 : : * could define one; then future clients can still interoperate with this
212 : : * server implementation. 'n' is the expected case.
213 : : */
214 : 88 : cbind_flag = *p;
215 [ - + - ]: 88 : switch (cbind_flag)
216 : : {
198 dgustafsson@postgres 217 :UBC 0 : case 'p':
218 [ # # ]: 0 : ereport(ERROR,
219 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
220 : : errmsg("malformed OAUTHBEARER message"),
221 : : errdetail("The server does not support channel binding for OAuth, but the client message includes channel binding data."));
222 : : break;
223 : :
198 dgustafsson@postgres 224 :CBC 88 : case 'y': /* fall through */
225 : : case 'n':
226 : 88 : p++;
227 [ - + ]: 88 : if (*p != ',')
198 dgustafsson@postgres 228 [ # # ]:UBC 0 : ereport(ERROR,
229 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
230 : : errmsg("malformed OAUTHBEARER message"),
231 : : errdetail("Comma expected, but found character \"%s\".",
232 : : sanitize_char(*p)));
198 dgustafsson@postgres 233 :CBC 88 : p++;
234 : 88 : break;
235 : :
198 dgustafsson@postgres 236 :UBC 0 : default:
237 [ # # ]: 0 : ereport(ERROR,
238 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
239 : : errmsg("malformed OAUTHBEARER message"),
240 : : errdetail("Unexpected channel-binding flag \"%s\".",
241 : : sanitize_char(cbind_flag)));
242 : : }
243 : :
244 : : /*
245 : : * Forbid optional authzid (authorization identity). We don't support it.
246 : : */
198 dgustafsson@postgres 247 [ - + ]:CBC 88 : if (*p == 'a')
198 dgustafsson@postgres 248 [ # # ]:UBC 0 : ereport(ERROR,
249 : : errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
250 : : errmsg("client uses authorization identity, but it is not supported"));
198 dgustafsson@postgres 251 [ - + ]:CBC 88 : if (*p != ',')
198 dgustafsson@postgres 252 [ # # ]:UBC 0 : ereport(ERROR,
253 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
254 : : errmsg("malformed OAUTHBEARER message"),
255 : : errdetail("Unexpected attribute \"%s\" in client-first-message.",
256 : : sanitize_char(*p)));
198 dgustafsson@postgres 257 :CBC 88 : p++;
258 : :
259 : : /* All remaining fields are separated by the RFC's kvsep (\x01). */
260 [ - + ]: 88 : if (*p != KVSEP)
198 dgustafsson@postgres 261 [ # # ]:UBC 0 : ereport(ERROR,
262 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
263 : : errmsg("malformed OAUTHBEARER message"),
264 : : errdetail("Key-value separator expected, but found character \"%s\".",
265 : : sanitize_char(*p)));
198 dgustafsson@postgres 266 :CBC 88 : p++;
267 : :
268 : 88 : auth = parse_kvpairs_for_auth(&p);
269 [ - + ]: 88 : if (!auth)
198 dgustafsson@postgres 270 [ # # ]:UBC 0 : ereport(ERROR,
271 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
272 : : errmsg("malformed OAUTHBEARER message"),
273 : : errdetail("Message does not contain an auth value."));
274 : :
275 : : /* We should be at the end of our message. */
198 dgustafsson@postgres 276 [ - + ]:CBC 88 : if (*p)
198 dgustafsson@postgres 277 [ # # ]:UBC 0 : ereport(ERROR,
278 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
279 : : errmsg("malformed OAUTHBEARER message"),
280 : : errdetail("Message contains additional data after the final terminator."));
281 : :
198 dgustafsson@postgres 282 [ + + ]:CBC 88 : if (!validate(ctx->port, auth))
283 : : {
284 : 60 : generate_error_response(ctx, output, outputlen);
285 : :
286 : 60 : ctx->state = OAUTH_STATE_ERROR;
287 : 60 : status = PG_SASL_EXCHANGE_CONTINUE;
288 : : }
289 : : else
290 : : {
291 : 27 : ctx->state = OAUTH_STATE_FINISHED;
292 : 27 : status = PG_SASL_EXCHANGE_SUCCESS;
293 : : }
294 : :
295 : : /* Don't let extra copies of the bearer token hang around. */
296 : 87 : explicit_bzero(input_copy, inputlen);
297 : :
298 : 87 : return status;
299 : : }
300 : :
301 : : /*
302 : : * Convert an arbitrary byte to printable form. For error messages.
303 : : *
304 : : * If it's a printable ASCII character, print it as a single character.
305 : : * otherwise, print it in hex.
306 : : *
307 : : * The returned pointer points to a static buffer.
308 : : */
309 : : static char *
198 dgustafsson@postgres 310 :UBC 0 : sanitize_char(char c)
311 : : {
312 : : static char buf[5];
313 : :
314 [ # # # # ]: 0 : if (c >= 0x21 && c <= 0x7E)
315 : 0 : snprintf(buf, sizeof(buf), "'%c'", c);
316 : : else
317 : 0 : snprintf(buf, sizeof(buf), "0x%02x", (unsigned char) c);
318 : 0 : return buf;
319 : : }
320 : :
321 : : /*
322 : : * Performs syntactic validation of a key and value from the initial client
323 : : * response. (Semantic validation of interesting values must be performed
324 : : * later.)
325 : : */
326 : : static void
198 dgustafsson@postgres 327 :CBC 88 : validate_kvpair(const char *key, const char *val)
328 : : {
329 : : /*-----
330 : : * From Sec 3.1:
331 : : * key = 1*(ALPHA)
332 : : */
333 : : static const char *key_allowed_set =
334 : : "abcdefghijklmnopqrstuvwxyz"
335 : : "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
336 : :
337 : : size_t span;
338 : :
339 [ - + ]: 88 : if (!key[0])
198 dgustafsson@postgres 340 [ # # ]:UBC 0 : ereport(ERROR,
341 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
342 : : errmsg("malformed OAUTHBEARER message"),
343 : : errdetail("Message contains an empty key name."));
344 : :
198 dgustafsson@postgres 345 :CBC 88 : span = strspn(key, key_allowed_set);
346 [ - + ]: 88 : if (key[span] != '\0')
198 dgustafsson@postgres 347 [ # # ]:UBC 0 : ereport(ERROR,
348 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
349 : : errmsg("malformed OAUTHBEARER message"),
350 : : errdetail("Message contains an invalid key name."));
351 : :
352 : : /*-----
353 : : * From Sec 3.1:
354 : : * value = *(VCHAR / SP / HTAB / CR / LF )
355 : : *
356 : : * The VCHAR (visible character) class is large; a loop is more
357 : : * straightforward than strspn().
358 : : */
198 dgustafsson@postgres 359 [ + + ]:CBC 673 : for (; *val; ++val)
360 : : {
361 [ + + + - ]: 585 : if (0x21 <= *val && *val <= 0x7E)
362 : 550 : continue; /* VCHAR */
363 : :
364 [ + - ]: 35 : switch (*val)
365 : : {
366 : 35 : case ' ':
367 : : case '\t':
368 : : case '\r':
369 : : case '\n':
370 : 35 : continue; /* SP, HTAB, CR, LF */
371 : :
198 dgustafsson@postgres 372 :UBC 0 : default:
373 [ # # ]: 0 : ereport(ERROR,
374 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
375 : : errmsg("malformed OAUTHBEARER message"),
376 : : errdetail("Message contains an invalid value."));
377 : : }
378 : : }
198 dgustafsson@postgres 379 :CBC 88 : }
380 : :
381 : : /*
382 : : * Consumes all kvpairs in an OAUTHBEARER exchange message. If the "auth" key is
383 : : * found, its value is returned.
384 : : */
385 : : static char *
386 : 88 : parse_kvpairs_for_auth(char **input)
387 : : {
388 : 88 : char *pos = *input;
389 : 88 : char *auth = NULL;
390 : :
391 : : /*----
392 : : * The relevant ABNF, from Sec. 3.1:
393 : : *
394 : : * kvsep = %x01
395 : : * key = 1*(ALPHA)
396 : : * value = *(VCHAR / SP / HTAB / CR / LF )
397 : : * kvpair = key "=" value kvsep
398 : : * ;;gs2-header = See RFC 5801
399 : : * client-resp = (gs2-header kvsep *kvpair kvsep) / kvsep
400 : : *
401 : : * By the time we reach this code, the gs2-header and initial kvsep have
402 : : * already been validated. We start at the beginning of the first kvpair.
403 : : */
404 : :
405 [ + - ]: 176 : while (*pos)
406 : : {
407 : : char *end;
408 : : char *sep;
409 : : char *key;
410 : : char *value;
411 : :
412 : : /*
413 : : * Find the end of this kvpair. Note that input is null-terminated by
414 : : * the SASL code, so the strchr() is bounded.
415 : : */
416 : 176 : end = strchr(pos, KVSEP);
417 [ - + ]: 176 : if (!end)
198 dgustafsson@postgres 418 [ # # ]:UBC 0 : ereport(ERROR,
419 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
420 : : errmsg("malformed OAUTHBEARER message"),
421 : : errdetail("Message contains an unterminated key/value pair."));
198 dgustafsson@postgres 422 :CBC 176 : *end = '\0';
423 : :
424 [ + + ]: 176 : if (pos == end)
425 : : {
426 : : /* Empty kvpair, signifying the end of the list. */
427 : 88 : *input = pos + 1;
428 : 88 : return auth;
429 : : }
430 : :
431 : : /*
432 : : * Find the end of the key name.
433 : : */
434 : 88 : sep = strchr(pos, '=');
435 [ - + ]: 88 : if (!sep)
198 dgustafsson@postgres 436 [ # # ]:UBC 0 : ereport(ERROR,
437 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
438 : : errmsg("malformed OAUTHBEARER message"),
439 : : errdetail("Message contains a key without a value."));
198 dgustafsson@postgres 440 :CBC 88 : *sep = '\0';
441 : :
442 : : /* Both key and value are now safely terminated. */
443 : 88 : key = pos;
444 : 88 : value = sep + 1;
445 : 88 : validate_kvpair(key, value);
446 : :
447 [ + - ]: 88 : if (strcmp(key, AUTH_KEY) == 0)
448 : : {
449 [ - + ]: 88 : if (auth)
198 dgustafsson@postgres 450 [ # # ]:UBC 0 : ereport(ERROR,
451 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
452 : : errmsg("malformed OAUTHBEARER message"),
453 : : errdetail("Message contains multiple auth values."));
454 : :
198 dgustafsson@postgres 455 :CBC 88 : auth = value;
456 : : }
457 : : else
458 : : {
459 : : /*
460 : : * The RFC also defines the host and port keys, but they are not
461 : : * required for OAUTHBEARER and we do not use them. Also, per Sec.
462 : : * 3.1, any key/value pairs we don't recognize must be ignored.
463 : : */
464 : : }
465 : :
466 : : /* Move to the next pair. */
467 : 88 : pos = end + 1;
468 : : }
469 : :
198 dgustafsson@postgres 470 [ # # ]:UBC 0 : ereport(ERROR,
471 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
472 : : errmsg("malformed OAUTHBEARER message"),
473 : : errdetail("Message did not contain a final terminator."));
474 : :
475 : : pg_unreachable();
476 : : return NULL;
477 : : }
478 : :
479 : : /*
480 : : * Builds the JSON response for failed authentication (RFC 7628, Sec. 3.2.2).
481 : : * This contains the required scopes for entry and a pointer to the OAuth/OpenID
482 : : * discovery document, which the client may use to conduct its OAuth flow.
483 : : */
484 : : static void
198 dgustafsson@postgres 485 :CBC 60 : generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen)
486 : : {
487 : : StringInfoData buf;
488 : : StringInfoData issuer;
489 : :
490 : : /*
491 : : * The admin needs to set an issuer and scope for OAuth to work. There's
492 : : * not really a way to hide this from the user, either, because we can't
493 : : * choose a "default" issuer, so be honest in the failure message. (In
494 : : * practice such configurations are rejected during HBA parsing.)
495 : : */
496 [ + - - + ]: 60 : if (!ctx->issuer || !ctx->scope)
198 dgustafsson@postgres 497 [ # # ]:UBC 0 : ereport(FATAL,
498 : : errcode(ERRCODE_INTERNAL_ERROR),
499 : : errmsg("OAuth is not properly configured for this user"),
500 : : errdetail_log("The issuer and scope parameters must be set in pg_hba.conf."));
501 : :
502 : : /*
503 : : * Build a default .well-known URI based on our issuer, unless the HBA has
504 : : * already provided one.
505 : : */
198 dgustafsson@postgres 506 :CBC 60 : initStringInfo(&issuer);
507 : 60 : appendStringInfoString(&issuer, ctx->issuer);
508 [ + + ]: 60 : if (strstr(ctx->issuer, "/.well-known/") == NULL)
509 : 57 : appendStringInfoString(&issuer, "/.well-known/openid-configuration");
510 : :
511 : 60 : initStringInfo(&buf);
512 : :
513 : : /*
514 : : * Escaping the string here is belt-and-suspenders defensive programming
515 : : * since escapable characters aren't valid in either the issuer URI or the
516 : : * scope list, but the HBA doesn't enforce that yet.
517 : : */
518 : 60 : appendStringInfoString(&buf, "{ \"status\": \"invalid_token\", ");
519 : :
520 : 60 : appendStringInfoString(&buf, "\"openid-configuration\": ");
521 : 60 : escape_json(&buf, issuer.data);
522 : 60 : pfree(issuer.data);
523 : :
524 : 60 : appendStringInfoString(&buf, ", \"scope\": ");
525 : 60 : escape_json(&buf, ctx->scope);
526 : :
527 : 60 : appendStringInfoString(&buf, " }");
528 : :
529 : 60 : *output = buf.data;
530 : 60 : *outputlen = buf.len;
531 : 60 : }
532 : :
533 : : /*-----
534 : : * Validates the provided Authorization header and returns the token from
535 : : * within it. NULL is returned on validation failure.
536 : : *
537 : : * Only Bearer tokens are accepted. The ABNF is defined in RFC 6750, Sec.
538 : : * 2.1:
539 : : *
540 : : * b64token = 1*( ALPHA / DIGIT /
541 : : * "-" / "." / "_" / "~" / "+" / "/" ) *"="
542 : : * credentials = "Bearer" 1*SP b64token
543 : : *
544 : : * The "credentials" construction is what we receive in our auth value.
545 : : *
546 : : * Since that spec is subordinate to HTTP (i.e. the HTTP Authorization
547 : : * header format; RFC 9110 Sec. 11), the "Bearer" scheme string must be
548 : : * compared case-insensitively. (This is not mentioned in RFC 6750, but the
549 : : * OAUTHBEARER spec points it out: RFC 7628 Sec. 4.)
550 : : *
551 : : * Invalid formats are technically a protocol violation, but we shouldn't
552 : : * reflect any information about the sensitive Bearer token back to the
553 : : * client; log at COMMERROR instead.
554 : : */
555 : : static const char *
556 : 88 : validate_token_format(const char *header)
557 : : {
558 : : size_t span;
559 : : const char *token;
560 : : static const char *const b64token_allowed_set =
561 : : "abcdefghijklmnopqrstuvwxyz"
562 : : "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
563 : : "0123456789-._~+/";
564 : :
565 : : /* Missing auth headers should be handled by the caller. */
566 [ - + ]: 88 : Assert(header);
567 : :
568 [ + + ]: 88 : if (header[0] == '\0')
569 : : {
570 : : /*
571 : : * A completely empty auth header represents a query for
572 : : * authentication parameters. The client expects it to fail; there's
573 : : * no need to make any extra noise in the logs.
574 : : *
575 : : * TODO: should we find a way to return STATUS_EOF at the top level,
576 : : * to suppress the authentication error entirely?
577 : : */
578 : 53 : return NULL;
579 : : }
580 : :
581 [ - + ]: 35 : if (pg_strncasecmp(header, BEARER_SCHEME, strlen(BEARER_SCHEME)))
582 : : {
198 dgustafsson@postgres 583 [ # # ]:UBC 0 : ereport(COMMERROR,
584 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
585 : : errmsg("malformed OAuth bearer token"),
586 : : errdetail_log("Client response indicated a non-Bearer authentication scheme."));
587 : 0 : return NULL;
588 : : }
589 : :
590 : : /* Pull the bearer token out of the auth value. */
198 dgustafsson@postgres 591 :CBC 35 : token = header + strlen(BEARER_SCHEME);
592 : :
593 : : /* Swallow any additional spaces. */
594 [ - + ]: 35 : while (*token == ' ')
198 dgustafsson@postgres 595 :UBC 0 : token++;
596 : :
597 : : /* Tokens must not be empty. */
198 dgustafsson@postgres 598 [ + + ]:CBC 35 : if (!*token)
599 : : {
600 [ + - ]: 1 : ereport(COMMERROR,
601 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
602 : : errmsg("malformed OAuth bearer token"),
603 : : errdetail_log("Bearer token is empty."));
604 : 1 : return NULL;
605 : : }
606 : :
607 : : /*
608 : : * Make sure the token contains only allowed characters. Tokens may end
609 : : * with any number of '=' characters.
610 : : */
611 : 34 : span = strspn(token, b64token_allowed_set);
612 [ - + ]: 34 : while (token[span] == '=')
198 dgustafsson@postgres 613 :UBC 0 : span++;
614 : :
198 dgustafsson@postgres 615 [ + + ]:CBC 34 : if (token[span] != '\0')
616 : : {
617 : : /*
618 : : * This error message could be more helpful by printing the
619 : : * problematic character(s), but that'd be a bit like printing a piece
620 : : * of someone's password into the logs.
621 : : */
622 [ + - ]: 1 : ereport(COMMERROR,
623 : : errcode(ERRCODE_PROTOCOL_VIOLATION),
624 : : errmsg("malformed OAuth bearer token"),
625 : : errdetail_log("Bearer token is not in the correct format."));
626 : 1 : return NULL;
627 : : }
628 : :
629 : 33 : return token;
630 : : }
631 : :
632 : : /*
633 : : * Checks that the "auth" kvpair in the client response contains a syntactically
634 : : * valid Bearer token, then passes it along to the loaded validator module for
635 : : * authorization. Returns true if validation succeeds.
636 : : */
637 : : static bool
638 : 88 : validate(Port *port, const char *auth)
639 : : {
640 : : int map_status;
641 : : ValidatorModuleResult *ret;
642 : : const char *token;
643 : : bool status;
644 : :
645 : : /* Ensure that we have a correct token to validate */
646 [ + + ]: 88 : if (!(token = validate_token_format(auth)))
647 : 55 : return false;
648 : :
649 : : /*
650 : : * Ensure that we have a validation library loaded, this should always be
651 : : * the case and an error here is indicative of a bug.
652 : : */
653 [ + - - + ]: 33 : if (!ValidatorCallbacks || !ValidatorCallbacks->validate_cb)
198 dgustafsson@postgres 654 [ # # ]:UBC 0 : ereport(FATAL,
655 : : errcode(ERRCODE_INTERNAL_ERROR),
656 : : errmsg("validation of OAuth token requested without a validator loaded"));
657 : :
658 : : /* Call the validation function from the validator module */
198 dgustafsson@postgres 659 :CBC 33 : ret = palloc0(sizeof(ValidatorModuleResult));
660 [ - + ]: 32 : if (!ValidatorCallbacks->validate_cb(validator_module_state, token,
661 : 33 : port->user_name, ret))
662 : : {
198 dgustafsson@postgres 663 [ # # ]:UBC 0 : ereport(WARNING,
664 : : errcode(ERRCODE_INTERNAL_ERROR),
665 : : errmsg("internal error in OAuth validator module"));
666 : 0 : return false;
667 : : }
668 : :
669 : : /*
670 : : * Log any authentication results even if the token isn't authorized; it
671 : : * might be useful for auditing or troubleshooting.
672 : : */
198 dgustafsson@postgres 673 [ + - ]:CBC 32 : if (ret->authn_id)
674 : 32 : set_authn_id(port, ret->authn_id);
675 : :
676 [ + + ]: 32 : if (!ret->authorized)
677 : : {
678 [ + - ]: 1 : ereport(LOG,
679 : : errmsg("OAuth bearer authentication failed for user \"%s\"",
680 : : port->user_name),
681 : : errdetail_log("Validator failed to authorize the provided token."));
682 : :
683 : 1 : status = false;
684 : 1 : goto cleanup;
685 : : }
686 : :
687 [ + + ]: 31 : if (port->hba->oauth_skip_usermap)
688 : : {
689 : : /*
690 : : * If the validator is our authorization authority, we're done.
691 : : * Authentication may or may not have been performed depending on the
692 : : * validator implementation; all that matters is that the validator
693 : : * says the user can log in with the target role.
694 : : */
695 : 1 : status = true;
696 : 1 : goto cleanup;
697 : : }
698 : :
699 : : /* Make sure the validator authenticated the user. */
700 [ + - + + ]: 30 : if (ret->authn_id == NULL || ret->authn_id[0] == '\0')
701 : : {
702 [ + - ]: 1 : ereport(LOG,
703 : : errmsg("OAuth bearer authentication failed for user \"%s\"",
704 : : port->user_name),
705 : : errdetail_log("Validator provided no identity."));
706 : :
707 : 1 : status = false;
708 : 1 : goto cleanup;
709 : : }
710 : :
711 : : /* Finally, check the user map. */
712 : 29 : map_status = check_usermap(port->hba->usermap, port->user_name,
713 : : MyClientConnectionInfo.authn_id, false);
714 : 29 : status = (map_status == STATUS_OK);
715 : :
716 : 32 : cleanup:
717 : :
718 : : /*
719 : : * Clear and free the validation result from the validator module once
720 : : * we're done with it.
721 : : */
722 [ + - ]: 32 : if (ret->authn_id != NULL)
723 : 32 : pfree(ret->authn_id);
724 : 32 : pfree(ret);
725 : :
726 : 32 : return status;
727 : : }
728 : :
729 : : /*
730 : : * load_validator_library
731 : : *
732 : : * Load the configured validator library in order to perform token validation.
733 : : * There is no built-in fallback since validation is implementation specific. If
734 : : * no validator library is configured, or if it fails to load, then error out
735 : : * since token validation won't be possible.
736 : : */
737 : : static void
738 : 89 : load_validator_library(const char *libname)
739 : : {
740 : : OAuthValidatorModuleInit validator_init;
741 : : MemoryContextCallback *mcb;
742 : :
743 : : /*
744 : : * The presence, and validity, of libname has already been established by
745 : : * check_oauth_validator so we don't need to perform more than Assert
746 : : * level checking here.
747 : : */
748 [ + - - + ]: 89 : Assert(libname && *libname);
749 : :
750 : 89 : validator_init = (OAuthValidatorModuleInit)
751 : 89 : load_external_function(libname, "_PG_oauth_validator_module_init",
752 : : false, NULL);
753 : :
754 : : /*
755 : : * The validator init function is required since it will set the callbacks
756 : : * for the validator library.
757 : : */
758 [ - + ]: 89 : if (validator_init == NULL)
198 dgustafsson@postgres 759 [ # # ]:UBC 0 : ereport(ERROR,
760 : : errmsg("%s module \"%s\" must define the symbol %s",
761 : : "OAuth validator", libname, "_PG_oauth_validator_module_init"));
762 : :
198 dgustafsson@postgres 763 :CBC 89 : ValidatorCallbacks = (*validator_init) ();
764 [ - + ]: 89 : Assert(ValidatorCallbacks);
765 : :
766 : : /*
767 : : * Check the magic number, to protect against break-glass scenarios where
768 : : * the ABI must change within a major version. load_external_function()
769 : : * already checks for compatibility across major versions.
770 : : */
771 [ + + ]: 89 : if (ValidatorCallbacks->magic != PG_OAUTH_VALIDATOR_MAGIC)
772 [ + - ]: 1 : ereport(ERROR,
773 : : errmsg("%s module \"%s\": magic number mismatch",
774 : : "OAuth validator", libname),
775 : : errdetail("Server has magic number 0x%08X, module has 0x%08X.",
776 : : PG_OAUTH_VALIDATOR_MAGIC, ValidatorCallbacks->magic));
777 : :
778 : : /*
779 : : * Make sure all required callbacks are present in the ValidatorCallbacks
780 : : * structure. Right now only the validation callback is required.
781 : : */
782 [ - + ]: 88 : if (ValidatorCallbacks->validate_cb == NULL)
198 dgustafsson@postgres 783 [ # # ]:UBC 0 : ereport(ERROR,
784 : : errmsg("%s module \"%s\" must provide a %s callback",
785 : : "OAuth validator", libname, "validate_cb"));
786 : :
787 : : /* Allocate memory for validator library private state data */
198 dgustafsson@postgres 788 :CBC 88 : validator_module_state = (ValidatorModuleState *) palloc0(sizeof(ValidatorModuleState));
789 : 88 : validator_module_state->sversion = PG_VERSION_NUM;
790 : :
791 [ + + ]: 88 : if (ValidatorCallbacks->startup_cb != NULL)
792 : 86 : ValidatorCallbacks->startup_cb(validator_module_state);
793 : :
794 : : /* Shut down the library before cleaning up its state. */
795 : 88 : mcb = palloc0(sizeof(*mcb));
796 : 88 : mcb->func = shutdown_validator_library;
797 : :
798 : 88 : MemoryContextRegisterResetCallback(CurrentMemoryContext, mcb);
799 : 88 : }
800 : :
801 : : /*
802 : : * Call the validator module's shutdown callback, if one is provided. This is
803 : : * invoked during memory context reset.
804 : : */
805 : : static void
806 : 88 : shutdown_validator_library(void *arg)
807 : : {
808 [ + + ]: 88 : if (ValidatorCallbacks->shutdown_cb != NULL)
809 : 86 : ValidatorCallbacks->shutdown_cb(validator_module_state);
810 : 88 : }
811 : :
812 : : /*
813 : : * Ensure an OAuth validator named in the HBA is permitted by the configuration.
814 : : *
815 : : * If the validator is currently unset and exactly one library is declared in
816 : : * oauth_validator_libraries, then that library will be used as the validator.
817 : : * Otherwise the name must be present in the list of oauth_validator_libraries.
818 : : */
819 : : bool
820 : 28 : check_oauth_validator(HbaLine *hbaline, int elevel, char **err_msg)
821 : : {
822 : 28 : int line_num = hbaline->linenumber;
823 : 28 : const char *file_name = hbaline->sourcefile;
824 : : char *rawstring;
825 : 28 : List *elemlist = NIL;
826 : :
827 : 28 : *err_msg = NULL;
828 : :
829 [ - + ]: 28 : if (oauth_validator_libraries_string[0] == '\0')
830 : : {
198 dgustafsson@postgres 831 [ # # ]:UBC 0 : ereport(elevel,
832 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
833 : : errmsg("oauth_validator_libraries must be set for authentication method %s",
834 : : "oauth"),
835 : : errcontext("line %d of configuration file \"%s\"",
836 : : line_num, file_name));
837 : 0 : *err_msg = psprintf("oauth_validator_libraries must be set for authentication method %s",
838 : : "oauth");
839 : 0 : return false;
840 : : }
841 : :
842 : : /* SplitDirectoriesString needs a modifiable copy */
198 dgustafsson@postgres 843 :CBC 28 : rawstring = pstrdup(oauth_validator_libraries_string);
844 : :
845 [ - + ]: 28 : if (!SplitDirectoriesString(rawstring, ',', &elemlist))
846 : : {
847 : : /* syntax error in list */
198 dgustafsson@postgres 848 [ # # ]:UBC 0 : ereport(elevel,
849 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
850 : : errmsg("invalid list syntax in parameter \"%s\"",
851 : : "oauth_validator_libraries"));
852 : 0 : *err_msg = psprintf("invalid list syntax in parameter \"%s\"",
853 : : "oauth_validator_libraries");
854 : 0 : goto done;
855 : : }
856 : :
198 dgustafsson@postgres 857 [ + + ]:CBC 28 : if (!hbaline->oauth_validator)
858 : : {
859 [ + + ]: 25 : if (elemlist->length == 1)
860 : : {
861 : 22 : hbaline->oauth_validator = pstrdup(linitial(elemlist));
862 : 22 : goto done;
863 : : }
864 : :
865 [ + - ]: 3 : ereport(elevel,
866 : : errcode(ERRCODE_CONFIG_FILE_ERROR),
867 : : errmsg("authentication method \"oauth\" requires argument \"validator\" to be set when oauth_validator_libraries contains multiple options"),
868 : : errcontext("line %d of configuration file \"%s\"",
869 : : line_num, file_name));
870 : 3 : *err_msg = "authentication method \"oauth\" requires argument \"validator\" to be set when oauth_validator_libraries contains multiple options";
871 : 3 : goto done;
872 : : }
873 : :
874 [ + - + - : 4 : foreach_ptr(char, allowed, elemlist)
+ - ]
875 : : {
876 [ + + ]: 4 : if (strcmp(allowed, hbaline->oauth_validator) == 0)
877 : 3 : goto done;
878 : : }
879 : :
198 dgustafsson@postgres 880 [ # # ]:UBC 0 : ereport(elevel,
881 : : errcode(ERRCODE_INVALID_PARAMETER_VALUE),
882 : : errmsg("validator \"%s\" is not permitted by %s",
883 : : hbaline->oauth_validator, "oauth_validator_libraries"),
884 : : errcontext("line %d of configuration file \"%s\"",
885 : : line_num, file_name));
886 : 0 : *err_msg = psprintf("validator \"%s\" is not permitted by %s",
887 : : hbaline->oauth_validator, "oauth_validator_libraries");
888 : :
198 dgustafsson@postgres 889 :CBC 28 : done:
890 : 28 : list_free_deep(elemlist);
891 : 28 : pfree(rawstring);
892 : :
893 : 28 : return (*err_msg == NULL);
894 : : }
|