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