taler-merchant-httpd_auth.c (21175B)
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 src/backend/taler-merchant-httpd_auth.c 18 * @brief client authentication logic 19 * @author Martin Schanzenbach 20 * @author Christian Grothoff 21 */ 22 #include "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,pos-read,products-read,products-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,pos-read,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,pos-read,products-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 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_STRINGS_base64_decode() always allocates its output 243 buffer, even when it returns 0; free it to avoid a leak. */ 244 GNUNET_break_op (0); 245 GNUNET_free (tmp); 246 return GNUNET_SYSERR; 247 } 248 colon = strchr (tmp, 249 ':'); 250 if (NULL == colon) 251 { 252 GNUNET_break_op (0); 253 GNUNET_free (tmp); 254 return GNUNET_SYSERR; 255 } 256 *colon = '\0'; 257 password = colon + 1; 258 /* Instance IDs are stored in canonical (lower-case) form (see 259 GNUNET_STRINGS_utf8_tolower() in the instance creation and lookup 260 paths), so we must fold the username to the same canonical form 261 before comparing; otherwise a mixed-case username would not match 262 the stored id ("myshop") and Basic auth would fail with HTTP 401. */ 263 instance_name = GNUNET_STRINGS_utf8_tolower (tmp); 264 /* instance->settings.id can be NULL if there is no instance yet */ 265 if (NULL != instance->settings.id) 266 target_instance = instance->settings.id; 267 if (0 != strcmp (instance_name, 268 target_instance)) 269 { 270 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 271 "Somebody tried to login to instance %s with username %s (login failed).\n", 272 target_instance, 273 instance_name); 274 GNUNET_free (instance_name); 275 GNUNET_free (tmp); 276 return GNUNET_SYSERR; 277 } 278 GNUNET_free (instance_name); 279 ret = TMH_check_auth (password, 280 &instance->auth.auth_salt, 281 &instance->auth.auth_hash); 282 GNUNET_free (tmp); 283 if (GNUNET_OK != ret) 284 { 285 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 286 "Password provided does not match credentials for %s\n", 287 target_instance); 288 } 289 return ret; 290 } 291 292 293 void 294 TMH_compute_auth (const char *token, 295 struct TALER_MerchantAuthenticationSaltP *salt, 296 struct TALER_MerchantAuthenticationHashP *hash) 297 { 298 GNUNET_CRYPTO_random_block (salt, 299 sizeof (*salt)); 300 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 301 "Computing initial auth using token with salt %s\n", 302 TALER_B2S (salt)); 303 TALER_merchant_instance_auth_hash_with_salt (hash, 304 salt, 305 token); 306 } 307 308 309 /** 310 * Function used to process Basic authorization header value. 311 * Sets correct scope in the auth_scope parameter of the 312 * #TMH_HandlerContext. 313 * 314 * @param hc the handler context 315 * @param authn_s the value of the authorization header 316 */ 317 static void 318 process_basic_auth (struct TMH_HandlerContext *hc, 319 const char *authn_s) 320 { 321 /* Handle token endpoint slightly differently: Only allow 322 * instance password (Basic auth) to retrieve access token. 323 * We need to handle authorization with Basic auth here first 324 * The only time we need to handle authentication like this is 325 * for the token endpoint! 326 */ 327 if ( (0 != strcmp (hc->rh->url_prefix, 328 "/token")) || 329 (NULL == hc->rh->method) || 330 (0 != strcmp (MHD_HTTP_METHOD_POST, 331 hc->rh->method)) || 332 (NULL == hc->instance)) 333 { 334 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 335 "Called endpoint `%s' with Basic authentication. Rejecting...\n", 336 hc->rh->url_prefix); 337 hc->auth_scope = TMH_AS_NONE; 338 return; 339 } 340 if (GNUNET_OK == 341 check_auth_instance (authn_s, 342 hc->instance)) 343 { 344 hc->auth_scope = TMH_AS_ALL; 345 } 346 else 347 { 348 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 349 "Basic authentication failed!\n"); 350 hc->auth_scope = TMH_AS_NONE; 351 } 352 } 353 354 355 /** 356 * Function used to process Bearer authorization header value. 357 * Sets correct scope in the auth_scope parameter of the 358 * #TMH_HandlerContext.. 359 * 360 * @param hc the handler context 361 * @param authn_s the value of the authorization header 362 * @return TALER_EC_NONE on success. 363 */ 364 static enum TALER_ErrorCode 365 process_bearer_auth (struct TMH_HandlerContext *hc, 366 const char *authn_s) 367 { 368 if (NULL == hc->instance) 369 { 370 hc->auth_scope = TMH_AS_NONE; 371 return TALER_EC_NONE; 372 } 373 if (GNUNET_is_zero (&hc->instance->auth.auth_hash)) 374 { 375 /* hash zero means no authentication for instance */ 376 hc->auth_scope = TMH_AS_ALL; 377 return TALER_EC_NONE; 378 } 379 { 380 enum TALER_ErrorCode ec; 381 382 ec = TMH_check_token (authn_s, 383 hc->instance->settings.id, 384 &hc->auth_scope); 385 if (TALER_EC_NONE != ec) 386 { 387 char *dec; 388 size_t dec_len; 389 const char *token; 390 391 /* NOTE: Deprecated, remove sometime after v1.1 */ 392 if (0 != strncasecmp (authn_s, 393 RFC_8959_PREFIX, 394 strlen (RFC_8959_PREFIX))) 395 { 396 GNUNET_break_op (0); 397 hc->auth_scope = TMH_AS_NONE; 398 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 399 "Authentication token invalid: %d\n", 400 (int) ec); 401 return ec; 402 } 403 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 404 "Trying deprecated secret-token:password API authN\n"); 405 token = authn_s + strlen (RFC_8959_PREFIX); 406 dec_len = GNUNET_STRINGS_urldecode (token, 407 strlen (token), 408 &dec); 409 if ( (0 == dec_len) || 410 (GNUNET_OK != 411 TMH_check_auth (dec, 412 &hc->instance->auth.auth_salt, 413 &hc->instance->auth.auth_hash)) ) 414 { 415 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 416 "Login failed\n"); 417 hc->auth_scope = TMH_AS_NONE; 418 GNUNET_free (dec); 419 return TALER_EC_NONE; 420 } 421 hc->auth_scope = TMH_AS_ALL; 422 GNUNET_free (dec); 423 } 424 } 425 return TALER_EC_NONE; 426 } 427 428 429 /** 430 * Checks if @a permission_required is in permissions of 431 * @a scope. 432 * 433 * @param permission_required the permission to check. 434 * @param scope the scope to check. 435 * @return true if @a permission_required is in the permissions set of @a scope. 436 */ 437 static bool 438 permission_in_scope (const char *permission_required, 439 enum TMH_AuthScope scope) 440 { 441 char *permissions; 442 const char *perms_tmp; 443 bool is_read_perm = false; 444 bool is_write_perm = false; 445 bool refreshable; 446 const char *last_dash; 447 448 perms_tmp = get_scope_permissions (scope, 449 &refreshable); 450 if (NULL == perms_tmp) 451 { 452 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 453 "Permission check failed: scope %d not understood\n", 454 (int) scope); 455 return false; 456 } 457 last_dash = strrchr (permission_required, 458 '-'); 459 if (NULL != last_dash) 460 { 461 is_write_perm = (0 == strcmp (last_dash, 462 "-write")); 463 is_read_perm = (0 == strcmp (last_dash, 464 "-read")); 465 } 466 467 if (0 == strcmp ("token-refresh", 468 permission_required)) 469 { 470 if (! refreshable) 471 { 472 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 473 "Permission check failed: token not refreshable\n"); 474 } 475 return refreshable; 476 } 477 permissions = GNUNET_strdup (perms_tmp); 478 { 479 const char *perm = strtok (permissions, 480 ","); 481 482 if (NULL == perm) 483 { 484 GNUNET_free (permissions); 485 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 486 "Permission check failed: empty permission set\n"); 487 return false; 488 } 489 while (NULL != perm) 490 { 491 if (0 == strcmp ("*", 492 perm)) 493 { 494 GNUNET_free (permissions); 495 return true; 496 } 497 if ( (0 == strcmp ("*-write", 498 perm)) && 499 (is_write_perm) ) 500 { 501 GNUNET_free (permissions); 502 return true; 503 } 504 if ( (0 == strcmp ("*-read", 505 perm)) && 506 (is_read_perm) ) 507 { 508 GNUNET_free (permissions); 509 return true; 510 } 511 if (0 == strcmp (permission_required, 512 perm)) 513 { 514 GNUNET_free (permissions); 515 return true; 516 } 517 perm = strtok (NULL, 518 ","); 519 } 520 } 521 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 522 "Permission check failed: %s not found in %s\n", 523 permission_required, 524 permissions); 525 GNUNET_free (permissions); 526 return false; 527 } 528 529 530 bool 531 TMH_scope_is_subset (enum TMH_AuthScope as, 532 enum TMH_AuthScope candidate) 533 { 534 const char *as_perms; 535 const char *candidate_perms; 536 char *permissions; 537 bool as_refreshable; 538 bool cand_refreshable; 539 540 as_perms = get_scope_permissions (as, 541 &as_refreshable); 542 candidate_perms = get_scope_permissions (candidate, 543 &cand_refreshable); 544 if (! as_refreshable && cand_refreshable) 545 return false; 546 if ( (NULL == as_perms) && 547 (NULL != candidate_perms) ) 548 return false; 549 if ( (NULL == candidate_perms) || 550 (0 == strcmp ("*", 551 as_perms))) 552 return true; 553 permissions = GNUNET_strdup (candidate_perms); 554 { 555 const char *perm; 556 557 perm = strtok (permissions, 558 ","); 559 if (NULL == perm) 560 { 561 GNUNET_free (permissions); 562 return true; 563 } 564 while (NULL != perm) 565 { 566 if (! permission_in_scope (perm, 567 as)) 568 { 569 GNUNET_free (permissions); 570 return false; 571 } 572 perm = strtok (NULL, 573 ","); 574 } 575 } 576 GNUNET_free (permissions); 577 return true; 578 } 579 580 581 enum TMH_AuthScope 582 TMH_get_scope_by_name (const char *name) 583 { 584 if (NULL == name) 585 return TMH_AS_NONE; 586 for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++) 587 { 588 if (0 == strcasecmp (scope_permissions[i].name, 589 name)) 590 return scope_permissions[i].as; 591 } 592 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 593 "Name `%s' does not match any scope we understand\n", 594 name); 595 return TMH_AS_NONE; 596 } 597 598 599 const char* 600 TMH_get_name_by_scope (enum TMH_AuthScope scope, 601 bool *refreshable) 602 { 603 *refreshable = scope & TMH_AS_REFRESHABLE; 604 for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++) 605 { 606 /* We ignore the TMH_AS_REFRESHABLE bit */ 607 if ( (scope & ~TMH_AS_REFRESHABLE) == 608 (scope_permissions[i].as & ~TMH_AS_REFRESHABLE) ) 609 return scope_permissions[i].name; 610 } 611 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 612 "Scope #%d does not match any scope we understand\n", 613 (int) scope); 614 return NULL; 615 } 616 617 618 enum GNUNET_GenericReturnValue 619 TMH_check_auth (const char *password, 620 struct TALER_MerchantAuthenticationSaltP *salt, 621 struct TALER_MerchantAuthenticationHashP *hash) 622 { 623 struct TALER_MerchantAuthenticationHashP val; 624 625 if (GNUNET_is_zero (hash)) 626 return GNUNET_OK; 627 if (NULL == password) 628 { 629 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 630 "Denying access: empty password provided\n"); 631 return GNUNET_SYSERR; 632 } 633 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 634 "Checking against token with salt %s\n", 635 TALER_B2S (salt)); 636 TALER_merchant_instance_auth_hash_with_salt (&val, 637 salt, 638 password); 639 if (0 != 640 GNUNET_memcmp (&val, 641 hash)) 642 { 643 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 644 "Access denied: password does not match\n"); 645 return GNUNET_SYSERR; 646 } 647 return GNUNET_OK; 648 } 649 650 651 /** 652 * Check if the client has provided the necessary credentials 653 * to access the selected endpoint of the selected instance. 654 * 655 * @param[in,out] hc handler context 656 * @return #GNUNET_OK on success, 657 * #GNUNET_NO if an error was queued (return #MHD_YES) 658 * #GNUNET_SYSERR to close the connection (return #MHD_NO) 659 */ 660 enum GNUNET_GenericReturnValue 661 TMH_perform_access_control (struct TMH_HandlerContext *hc) 662 { 663 const char *auth; 664 bool is_basic_auth = false; 665 bool auth_malformed = false; 666 667 auth = MHD_lookup_connection_value (hc->connection, 668 MHD_HEADER_KIND, 669 MHD_HTTP_HEADER_AUTHORIZATION); 670 671 if (NULL != auth) 672 { 673 extract_auth (&auth, 674 &is_basic_auth); 675 if (NULL == auth) 676 auth_malformed = true; 677 hc->auth_token = auth; 678 } 679 680 /* If we have zero configured instances (not even ones that have been 681 purged) or explicitly disabled authentication, THEN we accept anything 682 (no access control), as we then also have no data to protect. */ 683 if ((0 == GNUNET_CONTAINER_multihashmap_size (TMH_by_id_map)) || 684 (GNUNET_YES == TMH_auth_disabled)) 685 { 686 hc->auth_scope = TMH_AS_ALL; 687 } 688 else if (is_basic_auth) 689 { 690 process_basic_auth (hc, 691 auth); 692 } 693 else /* Check bearer token */ 694 { 695 enum TALER_ErrorCode ec; 696 697 ec = process_bearer_auth (hc, 698 auth); 699 if (TALER_EC_NONE != ec) 700 { 701 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 702 "Bearer authentication failed: %d\n", 703 (int) ec); 704 return (MHD_YES == 705 TALER_MHD_reply_with_ec (hc->connection, 706 ec, 707 NULL)) 708 ? GNUNET_NO 709 : GNUNET_SYSERR; 710 } 711 } 712 /* We grant access if: 713 - Endpoint does not require permissions 714 - Authorization scope of bearer token contains permissions 715 required by endpoint. 716 */ 717 if ( (NULL != hc->rh->permission) && 718 (! permission_in_scope (hc->rh->permission, 719 hc->auth_scope))) 720 { 721 if (auth_malformed && 722 (TMH_AS_NONE == hc->auth_scope) ) 723 { 724 GNUNET_break_op (0); 725 return (MHD_YES == 726 TALER_MHD_reply_with_error ( 727 hc->connection, 728 MHD_HTTP_UNAUTHORIZED, 729 TALER_EC_GENERIC_PARAMETER_MALFORMED, 730 "'" RFC_8959_PREFIX 731 "' prefix or 'Bearer' missing in 'Authorization' header")) 732 ? GNUNET_NO 733 : GNUNET_SYSERR; 734 } 735 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 736 "Credentials provided are %d which are insufficient for access to `%s'\n", 737 (int) hc->auth_scope, 738 hc->rh->permission); 739 return (MHD_YES == 740 TALER_MHD_reply_with_error ( 741 hc->connection, 742 MHD_HTTP_UNAUTHORIZED, 743 TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, 744 "Check credentials in 'Authorization' header")) 745 ? GNUNET_NO 746 : GNUNET_SYSERR; 747 } 748 return GNUNET_OK; 749 }