challenger-httpd_token.c (19170B)
1 /* 2 This file is part of Challenger 3 Copyright (C) 2023 Taler Systems SA 4 5 Challenger is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 Challenger is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 Challenger; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file challenger-httpd_token.c 18 * @brief functions to handle incoming /token requests 19 * @author Christian Grothoff 20 */ 21 #include "platform.h" 22 #include "challenger-httpd.h" 23 #include <gnunet/gnunet_util_lib.h> 24 #include <gnunet/gnunet_db_lib.h> 25 #include "challenger-httpd_token.h" 26 #include "challenger-httpd_common.h" 27 #include <taler/taler_json_lib.h> 28 #include <taler/taler_signatures.h> 29 #include "challenger_cm_enums.h" 30 #include "challenger-database/validation_get_pkce.h" 31 #include "challenger-database/client_check.h" 32 #include "challenger-database/token_add_token.h" 33 34 35 /** 36 * Context for a /token operation. 37 */ 38 struct TokenContext 39 { 40 41 /** 42 * Nonce of the validation process the request is about. 43 */ 44 struct CHALLENGER_ValidationNonceP nonce; 45 46 /** 47 * Handle for processing uploaded data. 48 */ 49 struct MHD_PostProcessor *pp; 50 51 /** 52 * Uploaded 'client_id' field from POST data. 53 */ 54 char *client_id; 55 56 /** 57 * Uploaded 'client_id' field from POST data. 58 */ 59 char *redirect_uri; 60 61 /** 62 * Uploaded 'client_secret' field from POST data. 63 */ 64 char *client_secret; 65 66 /** 67 * Uploaded 'code' field from POST data. 68 */ 69 char *code; 70 71 /** 72 * Uploaded 'grant_type' field from POST data. 73 */ 74 char *grant_type; 75 76 /** 77 * Uploaded 'code_verifier' field from POST data. 78 */ 79 char *code_verifier; 80 }; 81 82 83 /** 84 * Function called to clean up a backup context. 85 * 86 * @param cls a `struct TokenContext` 87 */ 88 static void 89 cleanup_ctx (void *cls) 90 { 91 struct TokenContext *bc = cls; 92 93 if (NULL != bc->pp) 94 { 95 GNUNET_break_op (MHD_YES == 96 MHD_destroy_post_processor (bc->pp)); 97 } 98 GNUNET_free (bc->client_id); 99 GNUNET_free (bc->redirect_uri); 100 GNUNET_free (bc->client_secret); 101 GNUNET_free (bc->code); 102 GNUNET_free (bc->grant_type); 103 GNUNET_free (bc->code_verifier); 104 GNUNET_free (bc); 105 } 106 107 108 /** 109 * Iterator over key-value pairs where the value may be made available 110 * in increments and/or may not be zero-terminated. Used for 111 * processing POST data. 112 * 113 * @param cls a `struct TokenContext *` 114 * @param kind type of the value, always #MHD_POSTDATA_KIND when called from MHD 115 * @param key 0-terminated key for the value 116 * @param filename name of the uploaded file, NULL if not known 117 * @param content_type mime-type of the data, NULL if not known 118 * @param transfer_encoding encoding of the data, NULL if not known 119 * @param data pointer to @a size bytes of data at the 120 * specified offset 121 * @param off offset of data in the overall value 122 * @param size number of bytes in @a data available 123 * @return #MHD_YES to continue iterating, 124 * #MHD_NO to abort the iteration 125 */ 126 static enum MHD_Result 127 post_iter (void *cls, 128 enum MHD_ValueKind kind, 129 const char *key, 130 const char *filename, 131 const char *content_type, 132 const char *transfer_encoding, 133 const char *data, 134 uint64_t off, 135 size_t size) 136 { 137 struct TokenContext *bc = cls; 138 struct Map 139 { 140 const char *name; 141 char **ptr; 142 } map[] = { 143 { 144 .name = "client_id", 145 .ptr = &bc->client_id 146 }, 147 { 148 .name = "redirect_uri", 149 .ptr = &bc->redirect_uri 150 }, 151 { 152 .name = "client_secret", 153 .ptr = &bc->client_secret 154 }, 155 { 156 .name = "code", 157 .ptr = &bc->code 158 }, 159 { 160 .name = "grant_type", 161 .ptr = &bc->grant_type 162 }, 163 { 164 .name = "code_verifier", 165 .ptr = &bc->code_verifier 166 }, 167 { 168 .name = NULL, 169 .ptr = NULL 170 }, 171 }; 172 char **ptr = NULL; 173 size_t slen; 174 175 (void) kind; 176 (void) filename; 177 (void) content_type; 178 (void) transfer_encoding; 179 (void) off; 180 for (unsigned int i = 0; NULL != map[i].name; i++) 181 if (0 == strcmp (key, 182 map[i].name)) 183 ptr = map[i].ptr; 184 if (NULL == ptr) 185 return MHD_YES; /* ignore */ 186 if (NULL == *ptr) 187 slen = 0; 188 else 189 slen = strlen (*ptr); 190 if (NULL == *ptr) 191 *ptr = GNUNET_malloc (size + 1); 192 else 193 *ptr = GNUNET_realloc (*ptr, 194 slen + size + 1); 195 memcpy (&(*ptr)[slen], 196 data, 197 size); 198 (*ptr)[slen + size] = '\0'; 199 return MHD_YES; 200 } 201 202 203 enum MHD_Result 204 CH_handler_token (struct CH_HandlerContext *hc, 205 const char *upload_data, 206 size_t *upload_data_size) 207 { 208 struct TokenContext *bc = hc->ctx; 209 210 if (NULL == bc) 211 { 212 /* first call, setup internals */ 213 bc = GNUNET_new (struct TokenContext); 214 hc->cc = &cleanup_ctx; 215 hc->ctx = bc; 216 bc->pp = MHD_create_post_processor (hc->connection, 217 2 * 1024, 218 &post_iter, 219 bc); 220 TALER_MHD_check_content_length (hc->connection, 221 2 * 1024); 222 return MHD_YES; 223 } 224 /* handle upload */ 225 if (0 != *upload_data_size) 226 { 227 enum MHD_Result res; 228 229 res = MHD_post_process (bc->pp, 230 upload_data, 231 *upload_data_size); 232 *upload_data_size = 0; 233 if (MHD_YES == res) 234 return MHD_YES; 235 return MHD_NO; 236 } 237 if ( (NULL == bc->grant_type) || 238 (0 != strcmp (bc->grant_type, 239 "authorization_code")) ) 240 { 241 GNUNET_break_op (0); 242 return CH_reply_with_oauth_error ( 243 hc->connection, 244 MHD_HTTP_BAD_REQUEST, 245 "unsupported_grant_type", 246 TALER_EC_GENERIC_PARAMETER_MALFORMED, 247 "authorization_code"); 248 } 249 250 if (NULL == bc->code) 251 { 252 GNUNET_break_op (0); 253 return CH_reply_with_oauth_error ( 254 hc->connection, 255 MHD_HTTP_BAD_REQUEST, 256 "invalid_request", 257 TALER_EC_GENERIC_PARAMETER_MISSING, 258 "code"); 259 } 260 if (NULL == bc->client_secret) 261 { 262 GNUNET_break_op (0); 263 return CH_reply_with_oauth_error ( 264 hc->connection, 265 MHD_HTTP_BAD_REQUEST, 266 "invalid_client", 267 TALER_EC_GENERIC_PARAMETER_MISSING, 268 "client_secret"); 269 } 270 if (NULL == bc->client_id) 271 { 272 GNUNET_break_op (0); 273 return CH_reply_with_oauth_error ( 274 hc->connection, 275 MHD_HTTP_BAD_REQUEST, 276 "invalid_client", 277 TALER_EC_GENERIC_PARAMETER_MISSING, 278 "client_id"); 279 } 280 if (NULL == bc->redirect_uri) 281 { 282 GNUNET_break_op (0); 283 return CH_reply_with_oauth_error ( 284 hc->connection, 285 MHD_HTTP_BAD_REQUEST, 286 "invalid_request", 287 TALER_EC_GENERIC_PARAMETER_MISSING, 288 "redirect_uri"); 289 } 290 291 /* Check this client is authorized to access the service */ 292 { 293 enum GNUNET_DB_QueryStatus qs; 294 char *client_url = NULL; 295 unsigned long long client_id; 296 char dummy; 297 298 if (1 != sscanf (bc->client_id, 299 "%llu%c", 300 &client_id, 301 &dummy)) 302 { 303 GNUNET_break_op (0); 304 return CH_reply_with_oauth_error ( 305 hc->connection, 306 MHD_HTTP_BAD_REQUEST, 307 "invalid_client", 308 TALER_EC_GENERIC_PARAMETER_MALFORMED, 309 "client_id"); 310 } 311 312 qs = CHALLENGERDB_client_check (CH_context, 313 client_id, 314 bc->client_secret, 315 0, /* do not increment */ 316 &client_url); 317 switch (qs) 318 { 319 case GNUNET_DB_STATUS_HARD_ERROR: 320 GNUNET_break (0); 321 return TALER_MHD_reply_with_error ( 322 hc->connection, 323 MHD_HTTP_INTERNAL_SERVER_ERROR, 324 TALER_EC_GENERIC_DB_FETCH_FAILED, 325 "client_check"); 326 case GNUNET_DB_STATUS_SOFT_ERROR: 327 GNUNET_break (0); 328 return MHD_NO; 329 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 330 GNUNET_break_op (0); 331 return CH_reply_with_oauth_error ( 332 hc->connection, 333 MHD_HTTP_NOT_FOUND, 334 "invalid_client", 335 TALER_EC_CHALLENGER_GENERIC_CLIENT_UNKNOWN, 336 NULL); 337 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 338 break; 339 } 340 if ( (NULL != client_url) && 341 (0 != strcmp (client_url, 342 bc->redirect_uri)) ) 343 { 344 GNUNET_break_op (0); 345 return CH_reply_with_oauth_error ( 346 hc->connection, 347 MHD_HTTP_UNAUTHORIZED, 348 "invalid_client", 349 TALER_EC_CHALLENGER_GENERIC_CLIENT_FORBIDDEN_BAD_REDIRECT_URI, 350 NULL); 351 } 352 GNUNET_free (client_url); 353 } 354 355 if (GNUNET_OK != 356 CH_code_to_nonce (bc->code, 357 &bc->nonce)) 358 { 359 GNUNET_break_op (0); 360 return CH_reply_with_oauth_error ( 361 hc->connection, 362 MHD_HTTP_UNAUTHORIZED, 363 "invalid_grant", 364 TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE, 365 NULL); 366 } 367 368 /* Check code is valid */ 369 { 370 char *client_secret; 371 json_t *address; 372 char *client_scope; 373 char *client_state; 374 char *client_redirect_uri; 375 char *code_challenge; 376 uint32_t code_challenge_method; 377 enum GNUNET_DB_QueryStatus qs; 378 char *code; 379 enum CHALLENGER_CM code_challenge_method_enum; 380 381 qs = CHALLENGERDB_validation_get_pkce (CH_context, 382 &bc->nonce, 383 &client_secret, 384 &address, 385 &client_scope, 386 &client_state, 387 &client_redirect_uri, 388 &code_challenge, 389 &code_challenge_method); 390 switch (qs) 391 { 392 case GNUNET_DB_STATUS_HARD_ERROR: 393 GNUNET_break (0); 394 return TALER_MHD_reply_with_error ( 395 hc->connection, 396 MHD_HTTP_INTERNAL_SERVER_ERROR, 397 TALER_EC_GENERIC_DB_FETCH_FAILED, 398 "validation_get_pkce"); 399 case GNUNET_DB_STATUS_SOFT_ERROR: 400 GNUNET_break (0); 401 return MHD_NO; 402 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 403 GNUNET_break_op (0); 404 return CH_reply_with_oauth_error ( 405 hc->connection, 406 MHD_HTTP_UNAUTHORIZED, 407 "invalid_grant", 408 TALER_EC_CHALLENGER_GENERIC_VALIDATION_UNKNOWN, 409 "validation_get_pkce"); 410 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 411 break; 412 } 413 414 code_challenge_method_enum = CHALLENGER_cm_from_int ( 415 code_challenge_method); 416 if (CHALLENGER_CM_UNKNOWN == code_challenge_method_enum) 417 { 418 GNUNET_break (0); 419 json_decref (address); 420 GNUNET_free (client_scope); 421 GNUNET_free (client_secret); 422 GNUNET_free (client_redirect_uri); 423 GNUNET_free (client_state); 424 GNUNET_free (code_challenge); 425 return TALER_MHD_reply_with_error ( 426 hc->connection, 427 MHD_HTTP_INTERNAL_SERVER_ERROR, 428 TALER_EC_GENERIC_PARAMETER_MALFORMED, 429 "Invalid code_challenge_method"); 430 } 431 432 /* Verify the code_challenge if present*/ 433 if (NULL != code_challenge) 434 { 435 if (NULL == bc->code_verifier) 436 { 437 GNUNET_break_op (0); 438 json_decref (address); 439 GNUNET_free (client_scope); 440 GNUNET_free (client_secret); 441 GNUNET_free (client_redirect_uri); 442 GNUNET_free (client_state); 443 GNUNET_free (code_challenge); 444 return CH_reply_with_oauth_error ( 445 hc->connection, 446 MHD_HTTP_UNAUTHORIZED, 447 "invalid_grant", 448 TALER_EC_GENERIC_PARAMETER_MISSING, 449 "code_verifier is missing"); 450 } 451 452 switch (code_challenge_method_enum) 453 { 454 case CHALLENGER_CM_S256: 455 { 456 gcry_md_hd_t hd; 457 unsigned char hash[32]; 458 char *encoded_hash = NULL; 459 size_t encoded_len; 460 const void *md; 461 462 if (GPG_ERR_NO_ERROR != 463 gcry_md_open (&hd, 464 GCRY_MD_SHA256, 465 0)) 466 { 467 GNUNET_break (0); 468 json_decref (address); 469 GNUNET_free (client_scope); 470 GNUNET_free (client_secret); 471 GNUNET_free (client_redirect_uri); 472 GNUNET_free (client_state); 473 GNUNET_free (code_challenge); 474 return CH_reply_with_oauth_error ( 475 hc->connection, 476 MHD_HTTP_INTERNAL_SERVER_ERROR, 477 "server_error", 478 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 479 "Failed to initialize SHA256 hash function"); 480 } 481 gcry_md_write (hd, 482 bc->code_verifier, 483 strlen (bc->code_verifier)); 484 md = gcry_md_read (hd, 485 0); 486 GNUNET_assert (NULL != md); 487 memcpy (hash, 488 md, 489 sizeof (hash)); 490 gcry_md_close (hd); 491 492 encoded_len 493 = GNUNET_STRINGS_base64url_encode (hash, 494 sizeof (hash), 495 &encoded_hash); 496 497 if ( (0 == encoded_len) || 498 (NULL == encoded_hash) ) 499 { 500 GNUNET_break (0); 501 json_decref (address); 502 GNUNET_free (client_scope); 503 GNUNET_free (client_secret); 504 GNUNET_free (client_redirect_uri); 505 GNUNET_free (client_state); 506 GNUNET_free (code_challenge); 507 GNUNET_free (encoded_hash); 508 return CH_reply_with_oauth_error ( 509 hc->connection, 510 MHD_HTTP_INTERNAL_SERVER_ERROR, 511 "server_error", 512 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, 513 "Failed to encode hash to Base64 URL"); 514 } 515 516 if (0 != strcmp (encoded_hash, 517 code_challenge)) 518 { 519 GNUNET_break_op (0); 520 json_decref (address); 521 GNUNET_free (client_scope); 522 GNUNET_free (client_secret); 523 GNUNET_free (client_redirect_uri); 524 GNUNET_free (client_state); 525 GNUNET_free (code_challenge); 526 GNUNET_free (encoded_hash); 527 return CH_reply_with_oauth_error ( 528 hc->connection, 529 MHD_HTTP_UNAUTHORIZED, 530 "invalid_grant", 531 TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE, 532 "code_verifier does not match code_challenge (SHA256)"); 533 } 534 GNUNET_free (encoded_hash); 535 } 536 break; 537 case CHALLENGER_CM_PLAIN: 538 { 539 if (0 != strcmp (bc->code_verifier, 540 code_challenge)) 541 { 542 GNUNET_break_op (0); 543 json_decref (address); 544 GNUNET_free (client_scope); 545 GNUNET_free (client_secret); 546 GNUNET_free (client_redirect_uri); 547 GNUNET_free (client_state); 548 GNUNET_free (code_challenge); 549 return CH_reply_with_oauth_error ( 550 hc->connection, 551 MHD_HTTP_UNAUTHORIZED, 552 "invalid_grant", 553 TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE, 554 "code_verifier does not match code_challenge (PLAIN)"); 555 } 556 } 557 break; 558 case CHALLENGER_CM_UNKNOWN: 559 case CHALLENGER_CM_EMPTY: 560 GNUNET_break (0); 561 json_decref (address); 562 GNUNET_free (client_scope); 563 GNUNET_free (client_secret); 564 GNUNET_free (client_redirect_uri); 565 GNUNET_free (client_state); 566 GNUNET_free (code_challenge); 567 return CH_reply_with_oauth_error ( 568 hc->connection, 569 MHD_HTTP_INTERNAL_SERVER_ERROR, 570 "server_error", 571 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 572 "Database has empty or unknown challenge mode but with code_challenge"); 573 } 574 } 575 576 if (NULL == address) 577 { 578 GNUNET_break_op (0); 579 GNUNET_free (client_scope); 580 GNUNET_free (client_secret); 581 GNUNET_free (client_redirect_uri); 582 GNUNET_free (client_state); 583 GNUNET_free (code_challenge); 584 return CH_reply_with_oauth_error ( 585 hc->connection, 586 MHD_HTTP_CONFLICT, 587 "invalid_request", 588 TALER_EC_CHALLENGER_MISSING_ADDRESS, 589 "code"); 590 } 591 code = CH_compute_code (&bc->nonce, 592 client_secret, 593 client_scope, 594 address, 595 client_redirect_uri); 596 json_decref (address); 597 GNUNET_free (client_scope); 598 GNUNET_free (client_secret); 599 GNUNET_free (client_redirect_uri); 600 GNUNET_free (client_state); 601 GNUNET_free (code_challenge); 602 if (0 != strcmp (code, 603 bc->code)) 604 { 605 GNUNET_break_op (0); 606 GNUNET_free (code); 607 return CH_reply_with_oauth_error ( 608 hc->connection, 609 MHD_HTTP_UNAUTHORIZED, 610 "invalid_grant", 611 TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE, 612 "code"); 613 } 614 GNUNET_free (code); 615 } 616 617 { 618 struct CHALLENGER_AccessTokenP token; 619 enum GNUNET_DB_QueryStatus qs; 620 621 GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, 622 &token, 623 sizeof (token)); 624 qs = CHALLENGERDB_token_add_token (CH_context, 625 &bc->nonce, 626 &token, 627 CH_token_expiration, 628 CH_validation_expiration); 629 switch (qs) 630 { 631 case GNUNET_DB_STATUS_HARD_ERROR: 632 GNUNET_break (0); 633 return TALER_MHD_reply_with_error ( 634 hc->connection, 635 MHD_HTTP_INTERNAL_SERVER_ERROR, 636 TALER_EC_GENERIC_DB_STORE_FAILED, 637 "token_add_token"); 638 case GNUNET_DB_STATUS_SOFT_ERROR: 639 GNUNET_break (0); 640 return MHD_NO; 641 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 642 GNUNET_break (0); 643 return CH_reply_with_oauth_error ( 644 hc->connection, 645 MHD_HTTP_UNAUTHORIZED, 646 "invalid_grant", 647 TALER_EC_CHALLENGER_GRANT_UNKNOWN, 648 "token_add_token"); 649 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 650 break; 651 } 652 653 return TALER_MHD_REPLY_JSON_PACK ( 654 hc->connection, 655 MHD_HTTP_OK, 656 GNUNET_JSON_pack_data_auto ("access_token", 657 &token), 658 GNUNET_JSON_pack_string ("token_type", 659 "Bearer"), 660 GNUNET_JSON_pack_uint64 ("expires_in", 661 CH_token_expiration.rel_value_us 662 / GNUNET_TIME_UNIT_SECONDS.rel_value_us)); 663 } 664 }