taler-merchant-httpd_post-management-instances-INSTANCE-auth.c (20042B)
1 /* 2 This file is part of GNU Taler 3 (C) 2021 Taler Systems SA 4 5 GNU Taler is free software; you can redistribute it and/or modify 6 it under the terms of the GNU Affero General Public License as 7 published by the Free Software Foundation; either version 3, 8 or (at your option) any later version. 9 10 GNU Taler is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public 16 License along with TALER; see the file COPYING. If not, 17 see <http://www.gnu.org/licenses/> 18 */ 19 20 /** 21 * @file src/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.c 22 * @brief implementing POST /instances/$ID/auth request handling 23 * @author Christian Grothoff 24 * @author Florian Dold 25 */ 26 #include "platform.h" 27 #include "taler-merchant-httpd_post-management-instances-INSTANCE-auth.h" 28 #include "taler-merchant-httpd_auth.h" 29 #include "taler-merchant-httpd_helper.h" 30 #include "taler-merchant-httpd_mfa.h" 31 #include <taler/taler_json_lib.h> 32 #include "merchant-database/get_instance_auth.h" 33 #include "merchant-database/insert_login_token.h" 34 #include "merchant-database/update_instance_auth.h" 35 #include "merchant-database/start.h" 36 37 38 /** 39 * How often do we retry the simple INSERT database transaction? 40 */ 41 #define MAX_RETRIES 3 42 43 44 /** 45 * Return a login token created as part of a password reset. 46 * 47 * @param connection connection to respond on 48 * @param token binary value of the token 49 * @param expiration_time when the token expires 50 * @return MHD result code 51 */ 52 static enum MHD_Result 53 reply_with_login_token ( 54 struct MHD_Connection *connection, 55 const struct TALER_MERCHANTDB_LoginTokenP *token, 56 struct GNUNET_TIME_Timestamp expiration_time) 57 { 58 char *token_data; 59 char *access_token; 60 enum MHD_Result ret; 61 62 token_data = GNUNET_STRINGS_data_to_string_alloc (token, 63 sizeof (*token)); 64 GNUNET_asprintf (&access_token, 65 RFC_8959_PREFIX "%s", 66 token_data); 67 GNUNET_free (token_data); 68 ret = TALER_MHD_REPLY_JSON_PACK ( 69 connection, 70 MHD_HTTP_OK, 71 GNUNET_JSON_pack_string ("access_token", 72 access_token), 73 GNUNET_JSON_pack_string ("token", 74 access_token), 75 GNUNET_JSON_pack_string ("scope", 76 "spa"), 77 GNUNET_JSON_pack_bool ("refreshable", 78 true), 79 GNUNET_JSON_pack_timestamp ("expiration", 80 expiration_time)); 81 GNUNET_free (access_token); 82 return ret; 83 } 84 85 86 /** 87 * Change the authentication settings of an instance. 88 * 89 * @param mi instance to modify settings of 90 * @param connection the MHD connection to handle 91 * @param[in,out] hc context with further information about the request 92 * @param auth_override The authentication settings for this instance 93 * do not apply due to administrative action. Do not check 94 * against the DB value when updating the auth token. 95 * @param require_old_password require the current password when the 96 * instance currently uses password authentication 97 * @param tcs set of multi-factor authorizations required 98 * @param mfa_combi_and require all MFA channels in @a tcs instead of any one 99 * @param login_token_duration how long a login token returned after the 100 * update should remain valid; zero means do not create a token 101 * @param password_reset true if this is an unauthenticated password 102 * reset, which may only be started once per 15 minutes per instance 103 * @return MHD result code 104 */ 105 static enum MHD_Result 106 post_instances_ID_auth (struct TMH_MerchantInstance *mi, 107 struct MHD_Connection *connection, 108 struct TMH_HandlerContext *hc, 109 bool auth_override, 110 bool require_old_password, 111 enum TEH_TanChannelSet tcs, 112 bool mfa_combi_and, 113 struct GNUNET_TIME_Relative login_token_duration, 114 bool password_reset) 115 { 116 struct TALER_MERCHANTDB_InstanceAuthSettings ias; 117 struct TALER_MERCHANTDB_LoginTokenP login_token; 118 struct GNUNET_TIME_Timestamp token_creation_time; 119 struct GNUNET_TIME_Timestamp token_expiration_time; 120 const char *auth_pw = NULL; 121 const char *old_password = NULL; 122 json_t *jauth = hc->request_body; 123 bool issue_login_token 124 = ! GNUNET_TIME_relative_is_zero (login_token_duration); 125 126 if (issue_login_token) 127 { 128 GNUNET_CRYPTO_random_block (&login_token, 129 sizeof (login_token)); 130 token_creation_time = GNUNET_TIME_timestamp_get (); 131 token_expiration_time 132 = GNUNET_TIME_relative_to_timestamp (login_token_duration); 133 } 134 135 if (require_old_password) 136 { 137 json_t *jold_password = json_object_get (jauth, 138 "old_password"); 139 140 if (NULL != jold_password) 141 { 142 old_password = json_string_value (jold_password); 143 if (NULL == old_password) 144 { 145 GNUNET_break_op (0); 146 return TALER_MHD_reply_with_error ( 147 connection, 148 MHD_HTTP_BAD_REQUEST, 149 TALER_EC_MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH, 150 "old_password must be a string"); 151 } 152 } 153 } 154 155 { 156 enum GNUNET_GenericReturnValue ret; 157 158 ret = TMH_check_auth_config (connection, 159 jauth, 160 &auth_pw); 161 if (GNUNET_OK != ret) 162 return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; 163 } 164 165 { 166 enum TEH_TanChannelSet available_tcs = TMH_TCS_NONE; 167 bool have_sms = (NULL != mi->settings.phone) && 168 (NULL != TMH_helper_sms) && 169 mi->settings.phone_validated; 170 bool have_email = (NULL != mi->settings.email) && 171 (NULL != TMH_helper_email) && 172 mi->settings.email_validated; 173 174 if (have_sms && 175 (0 != (tcs & TMH_TCS_SMS))) 176 available_tcs |= TMH_TCS_SMS; 177 if (have_email && 178 (0 != (tcs & TMH_TCS_EMAIL))) 179 available_tcs |= TMH_TCS_EMAIL; 180 181 if (mfa_combi_and && 182 (0 != (tcs & TMH_TCS_SMS)) && 183 (! have_sms)) 184 { 185 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 186 "Cannot change authentication: SMS factor not available\n"); 187 return TALER_MHD_reply_with_error ( 188 connection, 189 MHD_HTTP_FORBIDDEN, 190 TALER_EC_MERCHANT_GENERIC_MFA_MISSING, 191 "phone_number"); 192 } 193 if (mfa_combi_and && 194 (0 != (tcs & TMH_TCS_EMAIL)) && 195 (! have_email)) 196 { 197 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 198 "Cannot change authentication: E-mail factor not available\n"); 199 return TALER_MHD_reply_with_error ( 200 connection, 201 MHD_HTTP_FORBIDDEN, 202 TALER_EC_MERCHANT_GENERIC_MFA_MISSING, 203 "email"); 204 } 205 if ( (TMH_TCS_NONE != tcs) && 206 (TMH_TCS_NONE == available_tcs) ) 207 { 208 const char *missing_factor; 209 210 switch (tcs) 211 { 212 case TMH_TCS_SMS: 213 missing_factor = "phone_number"; 214 break; 215 case TMH_TCS_EMAIL: 216 missing_factor = "email"; 217 break; 218 case TMH_TCS_EMAIL_AND_SMS: 219 missing_factor = "phone_number or email"; 220 break; 221 case TMH_TCS_NONE: 222 GNUNET_assert (0); 223 missing_factor = NULL; 224 break; 225 } 226 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 227 "Cannot change authentication: no MFA factor available\n"); 228 return TALER_MHD_reply_with_error ( 229 connection, 230 MHD_HTTP_FORBIDDEN, 231 TALER_EC_MERCHANT_GENERIC_MFA_MISSING, 232 missing_factor); 233 } 234 tcs = available_tcs; 235 } 236 if (! auth_override) 237 { 238 enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; // fix -Wmaybe-uninitialized 239 240 switch (tcs) 241 { 242 case TMH_TCS_NONE: 243 ret = GNUNET_OK; 244 break; 245 case TMH_TCS_SMS: 246 ret = TMH_mfa_auth_challenges_do (hc, 247 mi->settings.id, 248 password_reset, 249 mfa_combi_and, 250 TALER_MERCHANT_MFA_CHANNEL_SMS, 251 mi->settings.phone, 252 TALER_MERCHANT_MFA_CHANNEL_NONE); 253 break; 254 case TMH_TCS_EMAIL: 255 ret = TMH_mfa_auth_challenges_do (hc, 256 mi->settings.id, 257 password_reset, 258 mfa_combi_and, 259 TALER_MERCHANT_MFA_CHANNEL_EMAIL, 260 mi->settings.email, 261 TALER_MERCHANT_MFA_CHANNEL_NONE); 262 break; 263 case TMH_TCS_EMAIL_AND_SMS: 264 ret = TMH_mfa_auth_challenges_do (hc, 265 mi->settings.id, 266 password_reset, 267 mfa_combi_and, 268 TALER_MERCHANT_MFA_CHANNEL_EMAIL, 269 mi->settings.email, 270 TALER_MERCHANT_MFA_CHANNEL_SMS, 271 mi->settings.phone, 272 TALER_MERCHANT_MFA_CHANNEL_NONE); 273 break; 274 } 275 if (GNUNET_OK != ret) 276 { 277 return (GNUNET_NO == ret) 278 ? MHD_YES 279 : MHD_NO; 280 } 281 } 282 283 if (NULL == auth_pw) 284 { 285 memset (&ias.auth_salt, 286 0, 287 sizeof (ias.auth_salt)); 288 memset (&ias.auth_hash, 289 0, 290 sizeof (ias.auth_hash)); 291 } 292 else 293 { 294 TMH_compute_auth (auth_pw, 295 &ias.auth_salt, 296 &ias.auth_hash); 297 } 298 299 /* Store the new auth information in the database */ 300 { 301 enum GNUNET_DB_QueryStatus qs; 302 303 for (unsigned int i = 0; i<MAX_RETRIES; i++) 304 { 305 if (GNUNET_OK != 306 TALER_MERCHANTDB_start (TMH_db, 307 "post /instances/$ID/auth")) 308 { 309 return TALER_MHD_reply_with_error (connection, 310 MHD_HTTP_INTERNAL_SERVER_ERROR, 311 TALER_EC_GENERIC_DB_START_FAILED, 312 NULL); 313 } 314 315 /* Make the authentication update a serializable operation. 316 We first check that the authentication information 317 that the caller's request authenticated with 318 is still up to date. 319 Otherwise, we've detected a conflicting update 320 to the authentication. */ 321 { 322 struct TALER_MERCHANTDB_InstanceAuthSettings db_ias; 323 enum TALER_ErrorCode ec; 324 325 qs = TALER_MERCHANTDB_get_instance_auth (TMH_db, 326 mi->settings.id, 327 &db_ias); 328 329 switch (qs) 330 { 331 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 332 /* Instance got purged. */ 333 TALER_MERCHANTDB_rollback (TMH_db); 334 return TALER_MHD_reply_with_error (connection, 335 MHD_HTTP_NOT_FOUND, 336 TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, 337 NULL); 338 case GNUNET_DB_STATUS_SOFT_ERROR: 339 TALER_MERCHANTDB_rollback (TMH_db); 340 goto retry; 341 case GNUNET_DB_STATUS_HARD_ERROR: 342 TALER_MERCHANTDB_rollback (TMH_db); 343 return TALER_MHD_reply_with_error (connection, 344 MHD_HTTP_INTERNAL_SERVER_ERROR, 345 TALER_EC_GENERIC_DB_FETCH_FAILED, 346 NULL); 347 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 348 /* Success! */ 349 break; 350 } 351 352 if (! auth_override) 353 { 354 // FIXME are we sure what the scope here is? 355 ec = TMH_check_token (hc->auth_token, 356 mi->settings.id, 357 &hc->auth_scope); 358 if (TALER_EC_NONE != ec) 359 { 360 TALER_MERCHANTDB_rollback (TMH_db); 361 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 362 "Refusing auth change: `%s'\n", 363 TALER_ErrorCode_get_hint (ec)); 364 return TALER_MHD_reply_with_error (connection, 365 MHD_HTTP_UNAUTHORIZED, 366 TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, 367 NULL); 368 } 369 } 370 if (require_old_password && 371 (GNUNET_OK != 372 TMH_check_auth (old_password, 373 &db_ias.auth_salt, 374 &db_ias.auth_hash))) 375 { 376 TALER_MERCHANTDB_rollback (TMH_db); 377 return TALER_MHD_reply_with_error ( 378 connection, 379 MHD_HTTP_UNAUTHORIZED, 380 TALER_EC_MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_OLD_PASSWORD, 381 NULL); 382 } 383 } 384 385 qs = TALER_MERCHANTDB_update_instance_auth (TMH_db, 386 mi->settings.id, 387 &ias); 388 if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) 389 { 390 GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); 391 TALER_MERCHANTDB_rollback (TMH_db); 392 if (GNUNET_DB_STATUS_HARD_ERROR == qs) 393 { 394 return TALER_MHD_reply_with_error (connection, 395 MHD_HTTP_INTERNAL_SERVER_ERROR, 396 TALER_EC_GENERIC_DB_FETCH_FAILED, 397 NULL); 398 } 399 goto retry; 400 } 401 if (issue_login_token) 402 { 403 qs = TALER_MERCHANTDB_insert_login_token ( 404 TMH_db, 405 mi->settings.id, 406 &login_token, 407 token_creation_time, 408 token_expiration_time, 409 TMH_AS_REFRESHABLE | TMH_AS_SPA, 410 "login token from password reset"); 411 switch (qs) 412 { 413 case GNUNET_DB_STATUS_SOFT_ERROR: 414 TALER_MERCHANTDB_rollback (TMH_db); 415 goto retry; 416 case GNUNET_DB_STATUS_HARD_ERROR: 417 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 418 GNUNET_break (0); 419 TALER_MERCHANTDB_rollback (TMH_db); 420 return TALER_MHD_reply_with_error ( 421 connection, 422 MHD_HTTP_INTERNAL_SERVER_ERROR, 423 TALER_EC_GENERIC_DB_STORE_FAILED, 424 "insert_login_token"); 425 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 426 break; 427 } 428 } 429 qs = TALER_MERCHANTDB_commit (TMH_db); 430 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) 431 qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; 432 retry: 433 if (GNUNET_DB_STATUS_SOFT_ERROR != qs) 434 break; /* success! -- or hard failure */ 435 } /* for .. MAX_RETRIES */ 436 if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) 437 { 438 return TALER_MHD_reply_with_error (connection, 439 MHD_HTTP_INTERNAL_SERVER_ERROR, 440 TALER_EC_GENERIC_DB_COMMIT_FAILED, 441 NULL); 442 } 443 /* Finally, also update our running process */ 444 mi->auth = ias; 445 } 446 TMH_reload_instances (mi->settings.id); 447 if (issue_login_token) 448 return reply_with_login_token (connection, 449 &login_token, 450 token_expiration_time); 451 return TALER_MHD_reply_static (connection, 452 MHD_HTTP_NO_CONTENT, 453 NULL, 454 NULL, 455 0); 456 } 457 458 459 enum MHD_Result 460 TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh, 461 struct MHD_Connection *connection, 462 struct TMH_HandlerContext *hc) 463 { 464 struct TMH_MerchantInstance *mi = hc->instance; 465 466 return post_instances_ID_auth (mi, 467 connection, 468 hc, 469 false, 470 true, 471 (GNUNET_YES == TMH_password_change_mfa) 472 ? TEH_mandatory_tan_channels 473 : TMH_TCS_NONE, 474 false, 475 GNUNET_TIME_UNIT_ZERO, 476 false); 477 } 478 479 480 enum MHD_Result 481 TMH_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh, 482 struct MHD_Connection *connection, 483 struct TMH_HandlerContext *hc) 484 { 485 struct TMH_MerchantInstance *mi = hc->instance; 486 struct GNUNET_TIME_Relative token_duration = GNUNET_TIME_UNIT_ZERO; 487 struct GNUNET_JSON_Specification spec[] = { 488 GNUNET_JSON_spec_mark_optional ( 489 GNUNET_JSON_spec_relative_time ("token_duration", 490 &token_duration), 491 NULL), 492 GNUNET_JSON_spec_end () 493 }; 494 495 { 496 enum GNUNET_GenericReturnValue res; 497 498 res = TALER_MHD_parse_json_data (connection, 499 hc->request_body, 500 spec); 501 if (GNUNET_OK != res) 502 return (GNUNET_NO == res) ? MHD_YES : MHD_NO; 503 } 504 GNUNET_JSON_parse_free (spec); 505 506 if (0 == strcmp ("admin", 507 mi->settings.id)) 508 { 509 GNUNET_break_op (0); 510 return TALER_MHD_reply_with_error ( 511 connection, 512 MHD_HTTP_FORBIDDEN, 513 TALER_EC_MERCHANT_GENERIC_MFA_MISSING, 514 "not allowed for 'admin' account"); 515 } 516 if (TMH_TCS_NONE == TEH_mandatory_tan_channels) 517 { 518 /* This endpoint changes the instance password *without* requiring 519 the current password; the only thing standing between an 520 anonymous client and a full account takeover is the MFA 521 challenge. If no TAN channel is mandatory, we have no second 522 factor to require and thus must refuse the request. */ 523 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 524 "Refusing password reset: no mandatory TAN channel configured\n"); 525 return TALER_MHD_reply_with_error ( 526 connection, 527 MHD_HTTP_FORBIDDEN, 528 TALER_EC_MERCHANT_GENERIC_MFA_MISSING, 529 "MANDATORY_TAN_CHANNELS"); 530 } 531 return post_instances_ID_auth (mi, 532 connection, 533 hc, 534 false, 535 false, 536 TEH_mandatory_tan_channels, 537 true, 538 token_duration, 539 true); 540 } 541 542 543 enum MHD_Result 544 TMH_private_post_instances_default_ID_auth ( 545 const struct TMH_RequestHandler *rh, 546 struct MHD_Connection *connection, 547 struct TMH_HandlerContext *hc) 548 { 549 struct TMH_MerchantInstance *mi; 550 enum MHD_Result ret; 551 552 mi = TMH_lookup_instance (hc->infix); 553 if (NULL == mi) 554 { 555 return TALER_MHD_reply_with_error ( 556 connection, 557 MHD_HTTP_NOT_FOUND, 558 TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, 559 hc->infix); 560 } 561 ret = post_instances_ID_auth (mi, 562 connection, 563 hc, 564 true, 565 false, 566 TMH_TCS_NONE, 567 false, 568 GNUNET_TIME_UNIT_ZERO, 569 false); 570 return ret; 571 } 572 573 574 /* end of taler-merchant-httpd_post-management-instances-INSTANCE-auth.c */