taler-merchant-httpd_post-management-instances-INSTANCE-auth.c (19702B)
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 * @return MHD result code 102 */ 103 static enum MHD_Result 104 post_instances_ID_auth (struct TMH_MerchantInstance *mi, 105 struct MHD_Connection *connection, 106 struct TMH_HandlerContext *hc, 107 bool auth_override, 108 bool require_old_password, 109 enum TEH_TanChannelSet tcs, 110 bool mfa_combi_and, 111 struct GNUNET_TIME_Relative login_token_duration) 112 { 113 struct TALER_MERCHANTDB_InstanceAuthSettings ias; 114 struct TALER_MERCHANTDB_LoginTokenP login_token; 115 struct GNUNET_TIME_Timestamp token_creation_time; 116 struct GNUNET_TIME_Timestamp token_expiration_time; 117 const char *auth_pw = NULL; 118 const char *old_password = NULL; 119 json_t *jauth = hc->request_body; 120 bool issue_login_token 121 = ! GNUNET_TIME_relative_is_zero (login_token_duration); 122 123 if (issue_login_token) 124 { 125 GNUNET_CRYPTO_random_block (&login_token, 126 sizeof (login_token)); 127 token_creation_time = GNUNET_TIME_timestamp_get (); 128 token_expiration_time 129 = GNUNET_TIME_relative_to_timestamp (login_token_duration); 130 } 131 132 if (require_old_password) 133 { 134 json_t *jold_password = json_object_get (jauth, 135 "old_password"); 136 137 if (NULL != jold_password) 138 { 139 old_password = json_string_value (jold_password); 140 if (NULL == old_password) 141 { 142 GNUNET_break_op (0); 143 return TALER_MHD_reply_with_error ( 144 connection, 145 MHD_HTTP_BAD_REQUEST, 146 TALER_EC_MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH, 147 "old_password must be a string"); 148 } 149 } 150 } 151 152 { 153 enum GNUNET_GenericReturnValue ret; 154 155 ret = TMH_check_auth_config (connection, 156 jauth, 157 &auth_pw); 158 if (GNUNET_OK != ret) 159 return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; 160 } 161 162 { 163 enum TEH_TanChannelSet available_tcs = TMH_TCS_NONE; 164 bool have_sms = (NULL != mi->settings.phone) && 165 (NULL != TMH_helper_sms) && 166 mi->settings.phone_validated; 167 bool have_email = (NULL != mi->settings.email) && 168 (NULL != TMH_helper_email) && 169 mi->settings.email_validated; 170 171 if (have_sms && 172 (0 != (tcs & TMH_TCS_SMS))) 173 available_tcs |= TMH_TCS_SMS; 174 if (have_email && 175 (0 != (tcs & TMH_TCS_EMAIL))) 176 available_tcs |= TMH_TCS_EMAIL; 177 178 if (mfa_combi_and && 179 (0 != (tcs & TMH_TCS_SMS)) && 180 (! have_sms)) 181 { 182 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 183 "Cannot change authentication: SMS factor not available\n"); 184 return TALER_MHD_reply_with_error ( 185 connection, 186 MHD_HTTP_FORBIDDEN, 187 TALER_EC_MERCHANT_GENERIC_MFA_MISSING, 188 "phone_number"); 189 } 190 if (mfa_combi_and && 191 (0 != (tcs & TMH_TCS_EMAIL)) && 192 (! have_email)) 193 { 194 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 195 "Cannot change authentication: E-mail factor not available\n"); 196 return TALER_MHD_reply_with_error ( 197 connection, 198 MHD_HTTP_FORBIDDEN, 199 TALER_EC_MERCHANT_GENERIC_MFA_MISSING, 200 "email"); 201 } 202 if ( (TMH_TCS_NONE != tcs) && 203 (TMH_TCS_NONE == available_tcs) ) 204 { 205 const char *missing_factor; 206 207 switch (tcs) 208 { 209 case TMH_TCS_SMS: 210 missing_factor = "phone_number"; 211 break; 212 case TMH_TCS_EMAIL: 213 missing_factor = "email"; 214 break; 215 case TMH_TCS_EMAIL_AND_SMS: 216 missing_factor = "phone_number or email"; 217 break; 218 case TMH_TCS_NONE: 219 GNUNET_assert (0); 220 missing_factor = NULL; 221 break; 222 } 223 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 224 "Cannot change authentication: no MFA factor available\n"); 225 return TALER_MHD_reply_with_error ( 226 connection, 227 MHD_HTTP_FORBIDDEN, 228 TALER_EC_MERCHANT_GENERIC_MFA_MISSING, 229 missing_factor); 230 } 231 tcs = available_tcs; 232 } 233 if (! auth_override) 234 { 235 enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; // fix -Wmaybe-uninitialized 236 237 switch (tcs) 238 { 239 case TMH_TCS_NONE: 240 ret = GNUNET_OK; 241 break; 242 case TMH_TCS_SMS: 243 ret = TMH_mfa_challenges_do (hc, 244 mi->settings.id, 245 TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, 246 mfa_combi_and, 247 TALER_MERCHANT_MFA_CHANNEL_SMS, 248 mi->settings.phone, 249 TALER_MERCHANT_MFA_CHANNEL_NONE); 250 break; 251 case TMH_TCS_EMAIL: 252 ret = TMH_mfa_challenges_do (hc, 253 mi->settings.id, 254 TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, 255 mfa_combi_and, 256 TALER_MERCHANT_MFA_CHANNEL_EMAIL, 257 mi->settings.email, 258 TALER_MERCHANT_MFA_CHANNEL_NONE); 259 break; 260 case TMH_TCS_EMAIL_AND_SMS: 261 ret = TMH_mfa_challenges_do (hc, 262 mi->settings.id, 263 TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, 264 mfa_combi_and, 265 TALER_MERCHANT_MFA_CHANNEL_EMAIL, 266 mi->settings.email, 267 TALER_MERCHANT_MFA_CHANNEL_SMS, 268 mi->settings.phone, 269 TALER_MERCHANT_MFA_CHANNEL_NONE); 270 break; 271 } 272 if (GNUNET_OK != ret) 273 { 274 return (GNUNET_NO == ret) 275 ? MHD_YES 276 : MHD_NO; 277 } 278 } 279 280 if (NULL == auth_pw) 281 { 282 memset (&ias.auth_salt, 283 0, 284 sizeof (ias.auth_salt)); 285 memset (&ias.auth_hash, 286 0, 287 sizeof (ias.auth_hash)); 288 } 289 else 290 { 291 TMH_compute_auth (auth_pw, 292 &ias.auth_salt, 293 &ias.auth_hash); 294 } 295 296 /* Store the new auth information in the database */ 297 { 298 enum GNUNET_DB_QueryStatus qs; 299 300 for (unsigned int i = 0; i<MAX_RETRIES; i++) 301 { 302 if (GNUNET_OK != 303 TALER_MERCHANTDB_start (TMH_db, 304 "post /instances/$ID/auth")) 305 { 306 return TALER_MHD_reply_with_error (connection, 307 MHD_HTTP_INTERNAL_SERVER_ERROR, 308 TALER_EC_GENERIC_DB_START_FAILED, 309 NULL); 310 } 311 312 /* Make the authentication update a serializable operation. 313 We first check that the authentication information 314 that the caller's request authenticated with 315 is still up to date. 316 Otherwise, we've detected a conflicting update 317 to the authentication. */ 318 { 319 struct TALER_MERCHANTDB_InstanceAuthSettings db_ias; 320 enum TALER_ErrorCode ec; 321 322 qs = TALER_MERCHANTDB_get_instance_auth (TMH_db, 323 mi->settings.id, 324 &db_ias); 325 326 switch (qs) 327 { 328 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 329 /* Instance got purged. */ 330 TALER_MERCHANTDB_rollback (TMH_db); 331 return TALER_MHD_reply_with_error (connection, 332 MHD_HTTP_NOT_FOUND, 333 TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, 334 NULL); 335 case GNUNET_DB_STATUS_SOFT_ERROR: 336 TALER_MERCHANTDB_rollback (TMH_db); 337 goto retry; 338 case GNUNET_DB_STATUS_HARD_ERROR: 339 TALER_MERCHANTDB_rollback (TMH_db); 340 return TALER_MHD_reply_with_error (connection, 341 MHD_HTTP_INTERNAL_SERVER_ERROR, 342 TALER_EC_GENERIC_DB_FETCH_FAILED, 343 NULL); 344 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 345 /* Success! */ 346 break; 347 } 348 349 if (! auth_override) 350 { 351 // FIXME are we sure what the scope here is? 352 ec = TMH_check_token (hc->auth_token, 353 mi->settings.id, 354 &hc->auth_scope); 355 if (TALER_EC_NONE != ec) 356 { 357 TALER_MERCHANTDB_rollback (TMH_db); 358 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 359 "Refusing auth change: `%s'\n", 360 TALER_ErrorCode_get_hint (ec)); 361 return TALER_MHD_reply_with_error (connection, 362 MHD_HTTP_UNAUTHORIZED, 363 TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, 364 NULL); 365 } 366 } 367 if (require_old_password && 368 (GNUNET_OK != 369 TMH_check_auth (old_password, 370 &db_ias.auth_salt, 371 &db_ias.auth_hash))) 372 { 373 TALER_MERCHANTDB_rollback (TMH_db); 374 return TALER_MHD_reply_with_error ( 375 connection, 376 MHD_HTTP_UNAUTHORIZED, 377 TALER_EC_MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_OLD_PASSWORD, 378 NULL); 379 } 380 } 381 382 qs = TALER_MERCHANTDB_update_instance_auth (TMH_db, 383 mi->settings.id, 384 &ias); 385 if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) 386 { 387 GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); 388 TALER_MERCHANTDB_rollback (TMH_db); 389 if (GNUNET_DB_STATUS_HARD_ERROR == qs) 390 { 391 return TALER_MHD_reply_with_error (connection, 392 MHD_HTTP_INTERNAL_SERVER_ERROR, 393 TALER_EC_GENERIC_DB_FETCH_FAILED, 394 NULL); 395 } 396 goto retry; 397 } 398 if (issue_login_token) 399 { 400 qs = TALER_MERCHANTDB_insert_login_token ( 401 TMH_db, 402 mi->settings.id, 403 &login_token, 404 token_creation_time, 405 token_expiration_time, 406 TMH_AS_REFRESHABLE | TMH_AS_SPA, 407 "login token from password reset"); 408 switch (qs) 409 { 410 case GNUNET_DB_STATUS_SOFT_ERROR: 411 TALER_MERCHANTDB_rollback (TMH_db); 412 goto retry; 413 case GNUNET_DB_STATUS_HARD_ERROR: 414 case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: 415 GNUNET_break (0); 416 TALER_MERCHANTDB_rollback (TMH_db); 417 return TALER_MHD_reply_with_error ( 418 connection, 419 MHD_HTTP_INTERNAL_SERVER_ERROR, 420 TALER_EC_GENERIC_DB_STORE_FAILED, 421 "insert_login_token"); 422 case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: 423 break; 424 } 425 } 426 qs = TALER_MERCHANTDB_commit (TMH_db); 427 if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) 428 qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; 429 retry: 430 if (GNUNET_DB_STATUS_SOFT_ERROR != qs) 431 break; /* success! -- or hard failure */ 432 } /* for .. MAX_RETRIES */ 433 if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) 434 { 435 return TALER_MHD_reply_with_error (connection, 436 MHD_HTTP_INTERNAL_SERVER_ERROR, 437 TALER_EC_GENERIC_DB_COMMIT_FAILED, 438 NULL); 439 } 440 /* Finally, also update our running process */ 441 mi->auth = ias; 442 } 443 TMH_reload_instances (mi->settings.id); 444 if (issue_login_token) 445 return reply_with_login_token (connection, 446 &login_token, 447 token_expiration_time); 448 return TALER_MHD_reply_static (connection, 449 MHD_HTTP_NO_CONTENT, 450 NULL, 451 NULL, 452 0); 453 } 454 455 456 enum MHD_Result 457 TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh, 458 struct MHD_Connection *connection, 459 struct TMH_HandlerContext *hc) 460 { 461 struct TMH_MerchantInstance *mi = hc->instance; 462 463 return post_instances_ID_auth (mi, 464 connection, 465 hc, 466 false, 467 true, 468 (GNUNET_YES == TMH_password_change_mfa) 469 ? TEH_mandatory_tan_channels 470 : TMH_TCS_NONE, 471 false, 472 GNUNET_TIME_UNIT_ZERO); 473 } 474 475 476 enum MHD_Result 477 TMH_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh, 478 struct MHD_Connection *connection, 479 struct TMH_HandlerContext *hc) 480 { 481 struct TMH_MerchantInstance *mi = hc->instance; 482 struct GNUNET_TIME_Relative token_duration = GNUNET_TIME_UNIT_ZERO; 483 struct GNUNET_JSON_Specification spec[] = { 484 GNUNET_JSON_spec_mark_optional ( 485 GNUNET_JSON_spec_relative_time ("token_duration", 486 &token_duration), 487 NULL), 488 GNUNET_JSON_spec_end () 489 }; 490 491 { 492 enum GNUNET_GenericReturnValue res; 493 494 res = TALER_MHD_parse_json_data (connection, 495 hc->request_body, 496 spec); 497 if (GNUNET_OK != res) 498 return (GNUNET_NO == res) ? MHD_YES : MHD_NO; 499 } 500 GNUNET_JSON_parse_free (spec); 501 502 if (0 == strcmp ("admin", 503 mi->settings.id)) 504 { 505 GNUNET_break_op (0); 506 return TALER_MHD_reply_with_error ( 507 connection, 508 MHD_HTTP_FORBIDDEN, 509 TALER_EC_MERCHANT_GENERIC_MFA_MISSING, 510 "not allowed for 'admin' account"); 511 } 512 if (TMH_TCS_NONE == TEH_mandatory_tan_channels) 513 { 514 /* This endpoint changes the instance password *without* requiring 515 the current password; the only thing standing between an 516 anonymous client and a full account takeover is the MFA 517 challenge. If no TAN channel is mandatory, we have no second 518 factor to require and thus must refuse the request. */ 519 GNUNET_log (GNUNET_ERROR_TYPE_WARNING, 520 "Refusing password reset: no mandatory TAN channel configured\n"); 521 return TALER_MHD_reply_with_error ( 522 connection, 523 MHD_HTTP_FORBIDDEN, 524 TALER_EC_MERCHANT_GENERIC_MFA_MISSING, 525 "MANDATORY_TAN_CHANNELS"); 526 } 527 return post_instances_ID_auth (mi, 528 connection, 529 hc, 530 false, 531 false, 532 TEH_mandatory_tan_channels, 533 true, 534 token_duration); 535 } 536 537 538 enum MHD_Result 539 TMH_private_post_instances_default_ID_auth ( 540 const struct TMH_RequestHandler *rh, 541 struct MHD_Connection *connection, 542 struct TMH_HandlerContext *hc) 543 { 544 struct TMH_MerchantInstance *mi; 545 enum MHD_Result ret; 546 547 mi = TMH_lookup_instance (hc->infix); 548 if (NULL == mi) 549 { 550 return TALER_MHD_reply_with_error ( 551 connection, 552 MHD_HTTP_NOT_FOUND, 553 TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, 554 hc->infix); 555 } 556 ret = post_instances_ID_auth (mi, 557 connection, 558 hc, 559 true, 560 false, 561 TMH_TCS_NONE, 562 false, 563 GNUNET_TIME_UNIT_ZERO); 564 return ret; 565 } 566 567 568 /* end of taler-merchant-httpd_post-management-instances-INSTANCE-auth.c */