taler-merchant-httpd_auth.c (20658B)
1 /* 2 This file is part of TALER 3 (C) 2014--2025 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Lesser General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER 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 General Public License for more details. 12 13 You should have received a copy of the GNU General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file taler-merchant-httpd_auth.c 18 * @brief client authentication logic 19 * @author Martin Schanzenbach 20 * @author Christian Grothoff 21 */ 22 #include "taler/platform.h" 23 #include <gnunet/gnunet_util_lib.h> 24 #include <gnunet/gnunet_db_lib.h> 25 #include <taler/taler_json_lib.h> 26 #include "taler-merchant-httpd_auth.h" 27 #include "taler-merchant-httpd_helper.h" 28 29 /** 30 * Maximum length of a permissions string of a scope 31 */ 32 #define TMH_MAX_SCOPE_PERMISSIONS_LEN 4096 33 34 /** 35 * Maximum length of a name of a scope 36 */ 37 #define TMH_MAX_NAME_LEN 255 38 39 /** 40 * Represents a hard-coded set of default scopes with their 41 * permissions and names 42 */ 43 struct ScopePermissionMap 44 { 45 /** 46 * The scope enum value 47 */ 48 enum TMH_AuthScope as; 49 50 /** 51 * The scope name 52 */ 53 char name[TMH_MAX_NAME_LEN]; 54 55 /** 56 * The scope permissions string. 57 * Comma-separated. 58 */ 59 char permissions[TMH_MAX_SCOPE_PERMISSIONS_LEN]; 60 }; 61 62 /** 63 * The default scopes array for merchant 64 */ 65 static struct ScopePermissionMap scope_permissions[] = { 66 /* Deprecated since v19 */ 67 { 68 .as = TMH_AS_ALL, 69 .name = "write", 70 .permissions = "*" 71 }, 72 /* Full access for SPA */ 73 { 74 .as = TMH_AS_ALL, 75 .name = "all", 76 .permissions = "*" 77 }, 78 /* Full access for SPA */ 79 { 80 .as = TMH_AS_SPA, 81 .name = "spa", 82 .permissions = "*" 83 }, 84 /* Read-only access */ 85 { 86 .as = TMH_AS_READ_ONLY, 87 .name = "readonly", 88 .permissions = "*-read" 89 }, 90 /* Simple order management */ 91 { 92 .as = TMH_AS_ORDER_SIMPLE, 93 .name = "order-simple", 94 .permissions = "orders-read,orders-write" 95 }, 96 /* Simple order management for PoS, also allows inventory locking */ 97 { 98 .as = TMH_AS_ORDER_POS, 99 .name = "order-pos", 100 .permissions = "orders-read,orders-write,inventory-lock" 101 }, 102 /* Simple order management, also allows refunding */ 103 { 104 .as = TMH_AS_ORDER_MGMT, 105 .name = "order-mgmt", 106 .permissions = "orders-read,orders-write,orders-refund" 107 }, 108 /* Full order management, allows inventory locking and refunds */ 109 { 110 .as = TMH_AS_ORDER_FULL, 111 .name = "order-full", 112 .permissions = "orders-read,orders-write,inventory-lock,orders-refund" 113 }, 114 /* No permissions, dummy scope */ 115 { 116 .as = TMH_AS_NONE, 117 } 118 }; 119 120 121 /** 122 * Get permissions string for scope. 123 * Also extracts the leftmost bit into the @a refreshable 124 * output parameter. 125 * 126 * @param as the scope to get the permissions string from 127 * @param[out] refreshable true if the token associated with this scope is refreshable. 128 * @return the permissions string, or NULL if no such scope found 129 */ 130 static const char* 131 get_scope_permissions (enum TMH_AuthScope as, 132 bool *refreshable) 133 { 134 *refreshable = as & TMH_AS_REFRESHABLE; 135 for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++) 136 { 137 /* We ignore the TMH_AS_REFRESHABLE bit */ 138 if ( (as & ~TMH_AS_REFRESHABLE) == 139 (scope_permissions[i].as & ~TMH_AS_REFRESHABLE) ) 140 return scope_permissions[i].permissions; 141 } 142 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 143 "Failed to find required permissions for scope %d\n", 144 as); 145 return NULL; 146 } 147 148 149 /** 150 * Extract the token from authorization header value @a auth. 151 * The @a auth value can be a bearer token or a Basic 152 * authentication header. In both cases, this function 153 * updates @a auth to point to the actual credential, 154 * skipping spaces. 155 * 156 * NOTE: We probably want to replace this function with MHD2 157 * API calls in the future that are more robust. 158 * 159 * @param[in,out] auth pointer to authorization header value, 160 * will be updated to point to the start of the token 161 * or set to NULL if header value is invalid 162 * @param[out] is_basic_auth will be set to true if the 163 * authorization header uses basic authentication, 164 * otherwise to false 165 */ 166 static void 167 extract_auth (const char **auth, 168 bool *is_basic_auth) 169 { 170 const char *bearer = "Bearer "; 171 const char *basic = "Basic "; 172 const char *tok = *auth; 173 size_t offset = 0; 174 bool is_bearer = false; 175 176 *is_basic_auth = false; 177 if (0 == strncmp (tok, 178 bearer, 179 strlen (bearer))) 180 { 181 offset = strlen (bearer); 182 is_bearer = true; 183 } 184 else if (0 == strncmp (tok, 185 basic, 186 strlen (basic))) 187 { 188 offset = strlen (basic); 189 *is_basic_auth = true; 190 } 191 else 192 { 193 *auth = NULL; 194 return; 195 } 196 tok += offset; 197 while (' ' == *tok) 198 tok++; 199 if ( (is_bearer) && 200 (0 != strncasecmp (tok, 201 RFC_8959_PREFIX, 202 strlen (RFC_8959_PREFIX))) ) 203 { 204 *auth = NULL; 205 return; 206 } 207 *auth = tok; 208 } 209 210 211 /** 212 * Check if @a userpass grants access to @a instance. 213 * 214 * @param userpass base64 encoded "$USERNAME:$PASSWORD" value 215 * from HTTP Basic "Authentication" header 216 * @param instance the access controlled instance 217 */ 218 static enum GNUNET_GenericReturnValue 219 check_auth_instance (const char *userpass, 220 struct TMH_MerchantInstance *instance) 221 { 222 char *tmp; 223 char *colon; 224 const char *instance_name; 225 const char *password; 226 const char *target_instance = "admin"; 227 enum GNUNET_GenericReturnValue ret; 228 229 /* implicitly a zeroed out hash means no authentication */ 230 if (GNUNET_is_zero (&instance->auth.auth_hash)) 231 return GNUNET_OK; 232 if (NULL == userpass) 233 { 234 GNUNET_break_op (0); 235 return GNUNET_SYSERR; 236 } 237 if (0 == 238 GNUNET_STRINGS_base64_decode (userpass, 239 strlen (userpass), 240 (void**) &tmp)) 241 { 242 GNUNET_break_op (0); 243 return GNUNET_SYSERR; 244 } 245 colon = strchr (tmp, 246 ':'); 247 if (NULL == colon) 248 { 249 GNUNET_break_op (0); 250 GNUNET_free (tmp); 251 return GNUNET_SYSERR; 252 } 253 *colon = '\0'; 254 instance_name = tmp; 255 password = colon + 1; 256 /* instance->settings.id can be NULL if there is no instance yet */ 257 if (NULL != instance->settings.id) 258 target_instance = instance->settings.id; 259 if (0 != strcmp (instance_name, 260 target_instance)) 261 { 262 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 263 "Somebody tried to login to instance %s with username %s (login failed).\n", 264 target_instance, 265 instance_name); 266 GNUNET_free (tmp); 267 return GNUNET_SYSERR; 268 } 269 ret = TMH_check_auth (password, 270 &instance->auth.auth_salt, 271 &instance->auth.auth_hash); 272 GNUNET_free (tmp); 273 if (GNUNET_OK != ret) 274 { 275 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 276 "Password provided does not match credentials for %s\n", 277 target_instance); 278 } 279 return ret; 280 } 281 282 283 void 284 TMH_compute_auth (const char *token, 285 struct TALER_MerchantAuthenticationSaltP *salt, 286 struct TALER_MerchantAuthenticationHashP *hash) 287 { 288 GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, 289 salt, 290 sizeof (*salt)); 291 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 292 "Computing initial auth using token with salt %s\n", 293 TALER_B2S (salt)); 294 TALER_merchant_instance_auth_hash_with_salt (hash, 295 salt, 296 token); 297 } 298 299 300 /** 301 * Function used to process Basic authorization header value. 302 * Sets correct scope in the auth_scope parameter of the 303 * #TMH_HandlerContext. 304 * 305 * @param hc the handler context 306 * @param authn_s the value of the authorization header 307 */ 308 static void 309 process_basic_auth (struct TMH_HandlerContext *hc, 310 const char *authn_s) 311 { 312 /* Handle token endpoint slightly differently: Only allow 313 * instance password (Basic auth) to retrieve access token. 314 * We need to handle authorization with Basic auth here first 315 * The only time we need to handle authentication like this is 316 * for the token endpoint! 317 */ 318 if ( (0 != strncmp (hc->rh->url_prefix, 319 "/token", 320 strlen ("/token"))) || 321 (0 != strncmp (MHD_HTTP_METHOD_POST, 322 hc->rh->method, 323 strlen (MHD_HTTP_METHOD_POST))) || 324 (NULL == hc->instance)) 325 { 326 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 327 "Called endpoint `%s' with Basic authentication. Rejecting...\n", 328 hc->rh->url_prefix); 329 hc->auth_scope = TMH_AS_NONE; 330 return; 331 } 332 if (GNUNET_OK == 333 check_auth_instance (authn_s, 334 hc->instance)) 335 { 336 hc->auth_scope = TMH_AS_ALL; 337 } 338 else 339 { 340 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 341 "Basic authentication failed!\n"); 342 hc->auth_scope = TMH_AS_NONE; 343 } 344 } 345 346 347 /** 348 * Function used to process Bearer authorization header value. 349 * Sets correct scope in the auth_scope parameter of the 350 * #TMH_HandlerContext.. 351 * 352 * @param hc the handler context 353 * @param authn_s the value of the authorization header 354 * @return TALER_EC_NONE on success. 355 */ 356 static enum TALER_ErrorCode 357 process_bearer_auth (struct TMH_HandlerContext *hc, 358 const char *authn_s) 359 { 360 if (NULL == hc->instance) 361 { 362 hc->auth_scope = TMH_AS_NONE; 363 return TALER_EC_NONE; 364 } 365 if (GNUNET_is_zero (&hc->instance->auth.auth_hash)) 366 { 367 /* hash zero means no authentication for instance */ 368 hc->auth_scope = TMH_AS_ALL; 369 return TALER_EC_NONE; 370 } 371 { 372 enum TALER_ErrorCode ec; 373 374 ec = TMH_check_token (authn_s, 375 hc->instance->settings.id, 376 &hc->auth_scope); 377 if (TALER_EC_NONE != ec) 378 { 379 char *dec; 380 size_t dec_len; 381 const char *token; 382 383 /* NOTE: Deprecated, remove sometime after v1.1 */ 384 if (0 != strncasecmp (authn_s, 385 RFC_8959_PREFIX, 386 strlen (RFC_8959_PREFIX))) 387 { 388 GNUNET_break_op (0); 389 hc->auth_scope = TMH_AS_NONE; 390 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 391 "Authentication token invalid: %d\n", 392 (int) ec); 393 return ec; 394 } 395 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 396 "Trying deprecated secret-token:password API authN\n"); 397 token = authn_s + strlen (RFC_8959_PREFIX); 398 dec_len = GNUNET_STRINGS_urldecode (token, 399 strlen (token), 400 &dec); 401 if ( (0 == dec_len) || 402 (GNUNET_OK != 403 TMH_check_auth (dec, 404 &hc->instance->auth.auth_salt, 405 &hc->instance->auth.auth_hash)) ) 406 { 407 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 408 "Login failed\n"); 409 hc->auth_scope = TMH_AS_NONE; 410 GNUNET_free (dec); 411 return TALER_EC_NONE; 412 } 413 hc->auth_scope = TMH_AS_ALL; 414 GNUNET_free (dec); 415 } 416 } 417 return TALER_EC_NONE; 418 } 419 420 421 /** 422 * Checks if @a permission_required is in permissions of 423 * @a scope. 424 * 425 * @param permission_required the permission to check. 426 * @param scope the scope to check. 427 * @return true if @a permission_required is in the permissions set of @a scope. 428 */ 429 static bool 430 permission_in_scope (const char *permission_required, 431 enum TMH_AuthScope scope) 432 { 433 char *permissions; 434 const char *perms_tmp; 435 bool is_read_perm = false; 436 bool is_write_perm = false; 437 bool refreshable; 438 const char *last_dash; 439 440 perms_tmp = get_scope_permissions (scope, 441 &refreshable); 442 if (NULL == perms_tmp) 443 { 444 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 445 "Permission check failed: scope %d not understood\n", 446 (int) scope); 447 return false; 448 } 449 last_dash = strrchr (permission_required, 450 '-'); 451 if (NULL != last_dash) 452 { 453 is_write_perm = (0 == strcmp (last_dash, 454 "-write")); 455 is_read_perm = (0 == strcmp (last_dash, 456 "-read")); 457 } 458 459 if (0 == strcmp ("token-refresh", 460 permission_required)) 461 { 462 if (! refreshable) 463 { 464 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 465 "Permission check failed: token not refreshable\n"); 466 } 467 return refreshable; 468 } 469 permissions = GNUNET_strdup (perms_tmp); 470 { 471 const char *perm = strtok (permissions, 472 ","); 473 474 if (NULL == perm) 475 { 476 GNUNET_free (permissions); 477 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 478 "Permission check failed: empty permission set\n"); 479 return false; 480 } 481 while (NULL != perm) 482 { 483 if (0 == strcmp ("*", 484 perm)) 485 { 486 GNUNET_free (permissions); 487 return true; 488 } 489 if ( (0 == strcmp ("*-write", 490 perm)) && 491 (is_write_perm) ) 492 { 493 GNUNET_free (permissions); 494 return true; 495 } 496 if ( (0 == strcmp ("*-read", 497 perm)) && 498 (is_read_perm) ) 499 { 500 GNUNET_free (permissions); 501 return true; 502 } 503 if (0 == strcmp (permission_required, 504 perm)) 505 { 506 GNUNET_free (permissions); 507 return true; 508 } 509 perm = strtok (NULL, 510 ","); 511 } 512 } 513 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 514 "Permission check failed: %s not found in %s\n", 515 permission_required, 516 permissions); 517 GNUNET_free (permissions); 518 return false; 519 } 520 521 522 bool 523 TMH_scope_is_subset (enum TMH_AuthScope as, 524 enum TMH_AuthScope candidate) 525 { 526 const char *as_perms; 527 const char *candidate_perms; 528 char *permissions; 529 bool as_refreshable; 530 bool cand_refreshable; 531 532 as_perms = get_scope_permissions (as, 533 &as_refreshable); 534 candidate_perms = get_scope_permissions (candidate, 535 &cand_refreshable); 536 if (! as_refreshable && cand_refreshable) 537 return false; 538 if ( (NULL == as_perms) && 539 (NULL != candidate_perms) ) 540 return false; 541 if ( (NULL == candidate_perms) || 542 (0 == strcmp ("*", 543 as_perms))) 544 return true; 545 permissions = GNUNET_strdup (candidate_perms); 546 { 547 const char *perm; 548 549 perm = strtok (permissions, 550 ","); 551 if (NULL == perm) 552 { 553 GNUNET_free (permissions); 554 return true; 555 } 556 while (NULL != perm) 557 { 558 if (! permission_in_scope (perm, 559 as)) 560 { 561 GNUNET_free (permissions); 562 return false; 563 } 564 perm = strtok (NULL, 565 ","); 566 } 567 } 568 GNUNET_free (permissions); 569 return true; 570 } 571 572 573 enum TMH_AuthScope 574 TMH_get_scope_by_name (const char *name) 575 { 576 if (NULL == name) 577 return TMH_AS_NONE; 578 for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++) 579 { 580 if (0 == strcasecmp (scope_permissions[i].name, 581 name)) 582 return scope_permissions[i].as; 583 } 584 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 585 "Name `%s' does not match any scope we understand\n", 586 name); 587 return TMH_AS_NONE; 588 } 589 590 591 const char* 592 TMH_get_name_by_scope (enum TMH_AuthScope scope, 593 bool *refreshable) 594 { 595 *refreshable = scope & TMH_AS_REFRESHABLE; 596 for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++) 597 { 598 /* We ignore the TMH_AS_REFRESHABLE bit */ 599 if ( (scope & ~TMH_AS_REFRESHABLE) == 600 (scope_permissions[i].as & ~TMH_AS_REFRESHABLE) ) 601 return scope_permissions[i].name; 602 } 603 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 604 "Scope #%d does not match any scope we understand\n", 605 (int) scope); 606 return NULL; 607 } 608 609 610 enum GNUNET_GenericReturnValue 611 TMH_check_auth (const char *password, 612 struct TALER_MerchantAuthenticationSaltP *salt, 613 struct TALER_MerchantAuthenticationHashP *hash) 614 { 615 struct TALER_MerchantAuthenticationHashP val; 616 617 if (GNUNET_is_zero (hash)) 618 return GNUNET_OK; 619 if (NULL == password) 620 { 621 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 622 "Denying access: empty password provided\n"); 623 return GNUNET_SYSERR; 624 } 625 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 626 "Checking against token with salt %s\n", 627 TALER_B2S (salt)); 628 TALER_merchant_instance_auth_hash_with_salt (&val, 629 salt, 630 password); 631 if (0 != 632 GNUNET_memcmp (&val, 633 hash)) 634 { 635 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 636 "Access denied: password does not match\n"); 637 return GNUNET_SYSERR; 638 } 639 return GNUNET_OK; 640 } 641 642 643 /** 644 * Check if the client has provided the necessary credentials 645 * to access the selected endpoint of the selected instance. 646 * 647 * @param[in,out] hc handler context 648 * @return #GNUNET_OK on success, 649 * #GNUNET_NO if an error was queued (return #MHD_YES) 650 * #GNUNET_SYSERR to close the connection (return #MHD_NO) 651 */ 652 enum GNUNET_GenericReturnValue 653 TMH_perform_access_control (struct TMH_HandlerContext *hc) 654 { 655 const char *auth; 656 bool is_basic_auth = false; 657 bool auth_malformed = false; 658 659 auth = MHD_lookup_connection_value (hc->connection, 660 MHD_HEADER_KIND, 661 MHD_HTTP_HEADER_AUTHORIZATION); 662 663 if (NULL != auth) 664 { 665 extract_auth (&auth, 666 &is_basic_auth); 667 if (NULL == auth) 668 auth_malformed = true; 669 hc->auth_token = auth; 670 } 671 672 /* If we have zero configured instances (not even ones that have been 673 purged) or explicitly disabled authentication, THEN we accept anything 674 (no access control), as we then also have no data to protect. */ 675 if ((0 == GNUNET_CONTAINER_multihashmap_size (TMH_by_id_map)) || 676 (GNUNET_YES == TMH_auth_disabled)) 677 { 678 hc->auth_scope = TMH_AS_ALL; 679 } 680 else if (is_basic_auth) 681 { 682 process_basic_auth (hc, 683 auth); 684 } 685 else /* Check bearer token */ 686 { 687 enum TALER_ErrorCode ec; 688 689 ec = process_bearer_auth (hc, 690 auth); 691 if (TALER_EC_NONE != ec) 692 { 693 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 694 "Bearer authentication failed: %d\n", 695 (int) ec); 696 return (MHD_YES == 697 TALER_MHD_reply_with_ec (hc->connection, 698 ec, 699 NULL)) 700 ? GNUNET_NO 701 : GNUNET_SYSERR; 702 } 703 } 704 /* We grant access if: 705 - Endpoint does not require permissions 706 - Authorization scope of bearer token contains permissions 707 required by endpoint. 708 */ 709 if ( (NULL != hc->rh->permission) && 710 (! permission_in_scope (hc->rh->permission, 711 hc->auth_scope))) 712 { 713 if (auth_malformed && 714 (TMH_AS_NONE == hc->auth_scope) ) 715 { 716 GNUNET_break_op (0); 717 return (MHD_YES == 718 TALER_MHD_reply_with_error ( 719 hc->connection, 720 MHD_HTTP_UNAUTHORIZED, 721 TALER_EC_GENERIC_PARAMETER_MALFORMED, 722 "'" RFC_8959_PREFIX 723 "' prefix or 'Bearer' missing in 'Authorization' header")) 724 ? GNUNET_NO 725 : GNUNET_SYSERR; 726 } 727 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 728 "Credentials provided are %d which are insufficient for access to `%s'\n", 729 (int) hc->auth_scope, 730 hc->rh->permission); 731 return (MHD_YES == 732 TALER_MHD_reply_with_error ( 733 hc->connection, 734 MHD_HTTP_UNAUTHORIZED, 735 TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, 736 "Check credentials in 'Authorization' header")) 737 ? GNUNET_NO 738 : GNUNET_SYSERR; 739 } 740 return GNUNET_OK; 741 }