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