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