commit 968865572e61929567730cb22c26e135372c3ae1 parent 1867e4bd6ad9928db921db6c7866464bc45c6696 Author: Christian Grothoff <christian@grothoff.org> Date: Thu, 4 Sep 2025 22:21:46 +0200 add MFA to various endpoints (needs testing) Diffstat:
14 files changed, 551 insertions(+), 109 deletions(-)
diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -1997,6 +1997,14 @@ url_handler (void *cls, in the code... */ .max_upload = 1024 * 1024 * 8 }, + /* POST /forgot-password: */ + { + .url_prefix = "/forgot-password", + .method = MHD_HTTP_METHOD_POST, + .handler = &TMH_public_post_instances_ID_auth, + /* Body should be pretty small. */ + .max_upload = 1024 * 1024 + }, { .url_prefix = "*", .method = MHD_HTTP_METHOD_OPTIONS, @@ -2830,6 +2838,30 @@ run (void *cls, return; } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "merchant", + "HELPER_SMS", + &TMH_helper_sms)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, + "merchant", + "HELPER_SMS", + "no helper specified"); + } + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "merchant", + "HELPER_EMAIL", + &TMH_helper_email)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, + "merchant", + "HELPER_EMAIL", + "no helper specified"); + } + { char *tan_channels; @@ -2847,10 +2879,36 @@ run (void *cls, { if (0 == strcasecmp (tok, "sms")) + { + if (NULL == TMH_helper_sms) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "merchant", + "MANDATORY_TAN_CHANNELS", + "SMS mandatory, but no HELPER_SMS configured"); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + GNUNET_free (tan_channels); + return; + } TEH_mandatory_tan_channels |= TEH_TCS_SMS; + } else if (0 == strcasecmp (tok, "email")) + { + if (NULL == TMH_helper_email) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "merchant", + "MANDATORY_TAN_CHANNELS", + "EMAIL mandatory, but no HELPER_EMAIL configured"); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + GNUNET_free (tan_channels); + return; + } TEH_mandatory_tan_channels |= TEH_TCS_EMAIL; + } else { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, @@ -2885,30 +2943,6 @@ run (void *cls, } } - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (cfg, - "merchant", - "HELPER_SMS", - &TMH_helper_sms)) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, - "merchant", - "HELPER_SMS", - "no helper specified"); - } - - if (GNUNET_OK != - GNUNET_CONFIGURATION_get_value_string (cfg, - "merchant", - "HELPER_EMAIL", - &TMH_helper_email)) - { - GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, - "merchant", - "HELPER_EMAIL", - "no helper specified"); - } - if (GNUNET_YES == GNUNET_CONFIGURATION_get_value_yesno (cfg, "merchant", diff --git a/src/backend/taler-merchant-httpd.h b/src/backend/taler-merchant-httpd.h @@ -177,13 +177,6 @@ struct TMH_MerchantInstance bool deleted; /** - * The authentication settings for this instance - * do not apply due to administrative action. Do not check - * against the DB value when updating the auth token. - */ - bool auth_override; - - /** * True if email/sms validation is needed before the * instance can be used. */ @@ -802,7 +795,8 @@ enum TEH_TanChannelSet { TEH_TCS_NONE = 0, TEH_TCS_SMS = 1, - TEH_TCS_EMAIL = 2 + TEH_TCS_EMAIL = 2, + TEH_TCS_EMAIL_AND_SMS = 3 }; diff --git a/src/backend/taler-merchant-httpd_mfa.c b/src/backend/taler-merchant-httpd_mfa.c @@ -30,7 +30,7 @@ /** * How many challenges do we allow at most per request? */ -#define MAX_CHALLENGES 4 +#define MAX_CHALLENGES 9 /** * How long are challenges valid? @@ -637,3 +637,52 @@ cleanup: GNUNET_free (challenge_ids_copy); return ret; } + + +enum GNUNET_GenericReturnValue +TMH_mfa_check_simple ( + struct TMH_HandlerContext *hc, + enum TALER_MERCHANT_MFA_CriticalOperation op, + struct TMH_MerchantInstance *mi) +{ + enum GNUNET_GenericReturnValue ret; + bool have_sms = (NULL != mi->settings.phone) && + (NULL != TMH_helper_sms) && + (! mi->settings.phone_validated); + bool have_email = (NULL != mi->settings.email) && + (NULL != TMH_helper_email) && + (! mi->settings.email_validated); + + /* Note: we check for 'validated' above, but in theory + we could also use unvalidated for this operation. + That's a policy-decision we may want to revise, + but probably need to look at the global threat model to + make sure alternative configurations are still sane. */ + if (have_email) + { + ret = TMH_mfa_challenges_do (hc, + op, + false, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + mi->settings.email, + have_sms + ? TALER_MERCHANT_MFA_CHANNEL_SMS + : TALER_MERCHANT_MFA_CHANNEL_NONE, + mi->settings.phone, + TALER_MERCHANT_MFA_CHANNEL_NONE); + } + else if (have_sms) + { + ret = TMH_mfa_challenges_do (hc, + op, + false, + TALER_MERCHANT_MFA_CHANNEL_SMS, + mi->settings.phone, + TALER_MERCHANT_MFA_CHANNEL_NONE); + } + else + { + ret = GNUNET_OK; + } + return ret; +} diff --git a/src/backend/taler-merchant-httpd_mfa.h b/src/backend/taler-merchant-httpd_mfa.h @@ -69,4 +69,22 @@ TMH_mfa_challenges_do ( ...); +/** + * Check MFA for a simple operation that simply requires + * a single additional factor (if any are configured). + * + * @param[in,out] hc handler context with the connection to the client + * @param op operation for which we should check challenges for + * @param mi instance to check authentication for + * @return #GNUNET_OK on success (challenges satisfied) + * #GNUNET_NO if an error message was returned to the client + * #GNUNET_SYSERR to just close the connection + */ +enum GNUNET_GenericReturnValue +TMH_mfa_check_simple ( + struct TMH_HandlerContext *hc, + enum TALER_MERCHANT_MFA_CriticalOperation op, + struct TMH_MerchantInstance *mi); + + #endif diff --git a/src/backend/taler-merchant-httpd_private-delete-instances-ID.c b/src/backend/taler-merchant-httpd_private-delete-instances-ID.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2020-2021 Taler Systems SA + (C) 2020-2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -22,17 +22,19 @@ #include "taler-merchant-httpd_private-delete-instances-ID.h" #include <taler/taler_json_lib.h> #include <taler/taler_dbevents.h> - +#include "taler-merchant-httpd_mfa.h" /** * Handle a DELETE "/instances/$ID" request. * + * @param[in,out] hc http request context * @param mi instance to delete * @param connection the MHD connection to handle * @return MHD result code */ static MHD_RESULT -delete_instances_ID (struct TMH_MerchantInstance *mi, +delete_instances_ID (struct TMH_HandlerContext *hc, + struct TMH_MerchantInstance *mi, struct MHD_Connection *connection) { const char *purge_s; @@ -40,6 +42,20 @@ delete_instances_ID (struct TMH_MerchantInstance *mi, enum GNUNET_DB_QueryStatus qs; GNUNET_assert (NULL != mi); + { + enum GNUNET_GenericReturnValue ret = + TMH_mfa_check_simple (hc, + TALER_MERCHANT_MFA_CO_INSTANCE_DELETION, + mi); + + if (GNUNET_OK != ret) + { + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + } + purge_s = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "purge"); @@ -98,22 +114,25 @@ delete_instances_ID (struct TMH_MerchantInstance *mi, MHD_RESULT -TMH_private_delete_instances_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) +TMH_private_delete_instances_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) { struct TMH_MerchantInstance *mi = hc->instance; (void) rh; - return delete_instances_ID (mi, + return delete_instances_ID (hc, + mi, connection); } MHD_RESULT -TMH_private_delete_instances_default_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) +TMH_private_delete_instances_default_ID ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) { struct TMH_MerchantInstance *mi; @@ -121,12 +140,14 @@ TMH_private_delete_instances_default_ID (const struct TMH_RequestHandler *rh, mi = TMH_lookup_instance (hc->infix); if (NULL == mi) { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->infix); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->infix); } - return delete_instances_ID (mi, + return delete_instances_ID (hc, + mi, connection); } diff --git a/src/backend/taler-merchant-httpd_private-patch-accounts-ID.c b/src/backend/taler-merchant-httpd_private-patch-accounts-ID.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2023 Taler Systems SA + (C) 2023, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -26,6 +26,7 @@ #include "taler-merchant-httpd_private-patch-accounts-ID.h" #include "taler-merchant-httpd_helper.h" #include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_mfa.h" /** @@ -38,8 +39,8 @@ */ MHD_RESULT TMH_private_patch_accounts_ID (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) { struct TMH_MerchantInstance *mi = hc->instance; const char *h_wire_s = hc->infix; @@ -84,7 +85,7 @@ TMH_private_patch_accounts_ID (const struct TMH_RequestHandler *rh, ? MHD_YES : MHD_NO; } - + qs = TMH_db->update_account (TMH_db->cls, mi->settings.id, &h_wire, diff --git a/src/backend/taler-merchant-httpd_private-patch-instances-ID.c b/src/backend/taler-merchant-httpd_private-patch-instances-ID.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2020-2023 Taler Systems SA + (C) 2020-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -27,6 +27,7 @@ #include "taler-merchant-httpd_helper.h" #include <taler/taler_json_lib.h> #include <taler/taler_dbevents.h> +#include "taler-merchant-httpd_mfa.h" /** @@ -144,6 +145,124 @@ patch_instances_ID (struct TMH_MerchantInstance *mi, "jurisdiction"); } + if ( (NULL != is.phone) && + (NULL != mi->settings.phone) && + 0 == strcmp (mi->settings.phone, + is.phone) ) + is.phone_validated = mi->settings.phone_validated; + if ( (NULL != is.email) && + (NULL != mi->settings.email) && + 0 == strcmp (mi->settings.email, + is.email) ) + is.email_validated = mi->settings.email_validated; + { + enum GNUNET_GenericReturnValue ret; + enum TEH_TanChannelSet mtc = TEH_mandatory_tan_channels; + + if ( (0 != (mtc & TEH_TCS_SMS)) && + (NULL == is.phone) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "phone_number"); + } + if ( (0 != (mtc & TEH_TCS_EMAIL)) && + (NULL == is.email) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "email"); + } + if ( (is.phone_validated) && + (0 != (mtc & TEH_TCS_SMS)) ) + mtc -= TEH_TCS_SMS; + if ( (is.email_validated) && + (0 != (mtc & TEH_TCS_EMAIL)) ) + mtc -= TEH_TCS_EMAIL; + switch (mtc) + { + case TEH_TCS_NONE: + ret = GNUNET_OK; + break; + case TEH_TCS_SMS: + if (NULL == is.phone) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "phone_number"); + } + is.phone_validated = true; + /* validate new phone number, if possible require old e-mail + address for authorization */ + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + is.phone, + 0 == (TALER_MERCHANT_MFA_CHANNEL_EMAIL + & TEH_mandatory_tan_channels) + ? TALER_MERCHANT_MFA_CHANNEL_NONE + : TALER_MERCHANT_MFA_CHANNEL_EMAIL, + mi->settings.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL: + is.email_validated = true; + /* validate new e-mail address, if possible require old phone + address for authorization */ + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, + true, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + is.email, + 0 == (TALER_MERCHANT_MFA_CHANNEL_SMS + & TEH_mandatory_tan_channels) + ? TALER_MERCHANT_MFA_CHANNEL_NONE + : TALER_MERCHANT_MFA_CHANNEL_SMS, + mi->settings.phone, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL_AND_SMS: + is.phone_validated = true; + is.email_validated = true; + /* To change both, we require both old and both new + addresses to consent */ + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + mi->settings.phone, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + mi->settings.email, + TALER_MERCHANT_MFA_CHANNEL_SMS, + is.phone, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + is.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + } + if (GNUNET_OK != ret) + { + GNUNET_JSON_parse_free (spec); + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + + } + for (unsigned int retry = 0; retry<MAX_RETRIES; retry++) { /* Cleanup after earlier loops */ @@ -175,6 +294,10 @@ patch_instances_ID (struct TMH_MerchantInstance *mi, (NULL != is.email && NULL != mi->settings.email && 0 == strcmp (mi->settings.email, is.email))) && + ((mi->settings.phone == is.phone) || + (NULL != is.phone && NULL != mi->settings.phone && + 0 == strcmp (mi->settings.phone, + is.phone))) && ((mi->settings.website == is.website) || (NULL != is.website && NULL != mi->settings.website && 0 == strcmp (mi->settings.website, diff --git a/src/backend/taler-merchant-httpd_private-post-account.c b/src/backend/taler-merchant-httpd_private-post-account.c @@ -28,6 +28,7 @@ #include "taler_merchant_bank_lib.h" #include <taler/taler_dbevents.h> #include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_mfa.h" MHD_RESULT @@ -114,6 +115,21 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, TALER_MERCHANT_BANK_auth_free (&auth); } + { + enum GNUNET_GenericReturnValue ret = + TMH_mfa_check_simple (hc, + TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, + mi); + + if (GNUNET_OK != ret) + { + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + } + + /* convert provided payto URI into internal data structure with salts */ wm = TMH_setup_wire_account (uri, credit_facade_url, diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c b/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c @@ -26,6 +26,7 @@ #include "platform.h" #include "taler-merchant-httpd_private-post-instances-ID-auth.h" #include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_mfa.h" #include <taler/taler_json_lib.h> @@ -41,12 +42,18 @@ * @param mi instance to modify settings of * @param connection the MHD connection to handle * @param[in,out] hc context with further information about the request + * @param auth_override The authentication settings for this instance + * do not apply due to administrative action. Do not check + * against the DB value when updating the auth token. + * @param tcs set of multi-factor authorizations required * @return MHD result code */ static MHD_RESULT post_instances_ID_auth (struct TMH_MerchantInstance *mi, struct MHD_Connection *connection, - struct TMH_HandlerContext *hc) + struct TMH_HandlerContext *hc, + bool auth_override, + enum TEH_TanChannelSet tcs) { struct TALER_MERCHANTDB_InstanceAuthSettings ias; const char *auth_pw = NULL; @@ -62,6 +69,72 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; } + if ( (0 != (tcs & TEH_TCS_SMS) && + ( (NULL == mi->settings.phone) || + (NULL == TMH_helper_sms) || + (! mi->settings.phone_validated) ) ) ) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_MFA_MISSING, + "phone_number"); + } + if ( (0 != (tcs & TEH_TCS_EMAIL) && + ( (NULL == mi->settings.email) || + (NULL == TMH_helper_email) || + (! mi->settings.email_validated) ) ) ) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_MFA_MISSING, + "email"); + } + if (! auth_override) + { + enum GNUNET_GenericReturnValue ret; + + switch (tcs) + { + case TEH_TCS_NONE: + ret = GNUNET_OK; + break; + case TEH_TCS_SMS: + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + mi->settings.phone, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL: + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, + true, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + mi->settings.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL_AND_SMS: + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + mi->settings.phone, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + mi->settings.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + } + if (GNUNET_OK != ret) + { + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + } + if (NULL == auth_pw) { memset (&ias.auth_salt, @@ -131,7 +204,7 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, break; } - if (! mi->auth_override) + if (! auth_override) { // FIXME are we sure what the scope here is? ec = TMH_check_token (hc->auth_token, @@ -184,7 +257,6 @@ retry: /* Finally, also update our running process */ mi->auth = ias; } - mi->auth_override = false; TMH_reload_instances (mi->settings.id); return TALER_MHD_reply_static (connection, MHD_HTTP_NO_CONTENT, @@ -203,7 +275,24 @@ TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh, return post_instances_ID_auth (mi, connection, - hc); + hc, + false, + TEH_TCS_NONE); +} + + +MHD_RESULT +TMH_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + + return post_instances_ID_auth (mi, + connection, + hc, + false, + TEH_TCS_EMAIL_AND_SMS); } @@ -216,18 +305,31 @@ TMH_private_post_instances_default_ID_auth ( struct TMH_MerchantInstance *mi; MHD_RESULT ret; + if ( (NULL == hc->infix) || + (0 == strcmp ("admin", + hc->infix)) ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_MFA_MISSING, + "not allowed for 'admin' account"); + } mi = TMH_lookup_instance (hc->infix); if (NULL == mi) { - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, - hc->infix); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + hc->infix); } - mi->auth_override = true; ret = post_instances_ID_auth (mi, connection, - hc); + hc, + true, + TEH_TCS_NONE); return ret; } diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.h b/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.h @@ -38,9 +38,10 @@ * @return MHD result code */ MHD_RESULT -TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); +TMH_private_post_instances_ID_auth ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); /** @@ -53,8 +54,27 @@ TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh, * @return MHD result code */ MHD_RESULT -TMH_private_post_instances_default_ID_auth (const struct TMH_RequestHandler *rh, - struct MHD_Connection *connection, - struct TMH_HandlerContext *hc); +TMH_private_post_instances_default_ID_auth ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +/** + * Change the instance's auth settings. + * This is the public handler used to reset a password if + * the original password was forgotten. Always requires + * 2-FA to be configured for the account with two additional + * factors. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); #endif diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-token.c b/src/backend/taler-merchant-httpd_private-post-instances-ID-token.c @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2023 Taler Systems SA + (C) 2023, 2025 Taler Systems SA GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -25,6 +25,7 @@ #include "platform.h" #include "taler-merchant-httpd_private-post-instances-ID-token.h" #include "taler-merchant-httpd_helper.h" +#include "taler-merchant-httpd_mfa.h" #include <taler/taler_json_lib.h> @@ -122,6 +123,21 @@ TMH_private_post_instances_ID_token (const struct TMH_RequestHandler *rh, { description = ""; } + + { + enum GNUNET_GenericReturnValue ret = + TMH_mfa_check_simple (hc, + TALER_MERCHANT_MFA_CO_AUTH_TOKEN_CREATION, + mi); + + if (GNUNET_OK != ret) + { + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + } + qs = TMH_db->insert_login_token (TMH_db->cls, mi->settings.id, &btoken, diff --git a/src/backend/taler-merchant-httpd_private-post-instances.c b/src/backend/taler-merchant-httpd_private-post-instances.c @@ -26,6 +26,7 @@ #include "taler-merchant-httpd_private-post-instances.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_mfa.h" #include "taler_merchant_bank_lib.h" #include <taler/taler_dbevents.h> #include <taler/taler_json_lib.h> @@ -247,6 +248,78 @@ post_instances (const struct TMH_RequestHandler *rh, } } + /* Check MFA is satisfied */ + if (validation_needed) + { + enum GNUNET_GenericReturnValue ret; + + if ( (0 != (TEH_TCS_SMS & TEH_mandatory_tan_channels)) && + (NULL == is.phone) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "phone_number"); + + } + if ( (0 != (TEH_TCS_EMAIL & TEH_mandatory_tan_channels)) && + (NULL == is.email) ) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "email"); + } + switch (TEH_mandatory_tan_channels) + { + case TEH_TCS_NONE: + GNUNET_assert (0); + ret = GNUNET_OK; + break; + case TEH_TCS_SMS: + is.phone_validated = true; + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + is.phone, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL: + is.email_validated = true; + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, + true, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + is.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + case TEH_TCS_EMAIL_AND_SMS: + is.phone_validated = true; + is.email_validated = true; + ret = TMH_mfa_challenges_do (hc, + TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, + true, + TALER_MERCHANT_MFA_CHANNEL_SMS, + is.phone, + TALER_MERCHANT_MFA_CHANNEL_EMAIL, + is.email, + TALER_MERCHANT_MFA_CHANNEL_NONE); + break; + } + if (GNUNET_OK != ret) + { + GNUNET_JSON_parse_free (spec); + return (GNUNET_NO == ret) + ? MHD_YES + : MHD_NO; + } + } + /* handle authentication token setup */ if (NULL == auth_password) { @@ -424,6 +497,7 @@ TMH_public_post_instances (const struct TMH_RequestHandler *rh, TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, "Self-provisioning is not enabled"); } + return post_instances (rh, connection, hc, diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h @@ -79,7 +79,7 @@ enum TALER_MERCHANT_MFA_CriticalOperation TALER_MERCHANT_MFA_CO_INSTANCE_PROVISION, /** - * Bank account configuration or reconfiguratio ("account_config"). + * Bank account configuration or reconfiguration ("account_config"). */ TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, @@ -172,7 +172,7 @@ struct TALER_MERCHANT_MFA_BodySalt * Hash the given request @a body with the given @a salt to * produce @a h_body for MFA checks. * - * @param body HTTP request body + * @param body HTTP request body, NULL if body was empty * @param salt salt to use * @param h_body resulting hash */ diff --git a/src/util/mfa.c b/src/util/mfa.c @@ -49,12 +49,6 @@ static const char *channel_strings[] = { }; -/** - * Convert critical operation enumeration value to string. - * - * @param co input to convert - * @return operation value as string - */ const char * TALER_MERCHANT_MFA_co_to_string ( enum TALER_MERCHANT_MFA_CriticalOperation co) @@ -69,12 +63,6 @@ TALER_MERCHANT_MFA_co_to_string ( } -/** - * Convert string to critical operation enumeration value. - * - * @param str input to convert - * @return #TALER_MERCHANT_MFA_CO_NONE on failure - */ enum TALER_MERCHANT_MFA_CriticalOperation TALER_MERCHANT_MFA_co_from_string (const char *str) { @@ -93,12 +81,6 @@ TALER_MERCHANT_MFA_co_from_string (const char *str) } -/** - * Convert MFA channel enumeration value to string. - * - * @param ch input to convert - * @return operation value as string - */ const char * TALER_MERCHANT_MFA_channel_to_string ( enum TALER_MERCHANT_MFA_Channel ch) @@ -113,12 +95,6 @@ TALER_MERCHANT_MFA_channel_to_string ( } -/** - * Convert string to MFA channel enumeration value. - * - * @param str input to convert - * @return #TALER_MERCHANT_MFA_CHANNEL_NONE on failure - */ enum TALER_MERCHANT_MFA_Channel TALER_MERCHANT_MFA_channel_from_string (const char *str) { @@ -137,14 +113,6 @@ TALER_MERCHANT_MFA_channel_from_string (const char *str) } -/** - * Hash the given request @a body with the given @a salt to - * produce @a h_body for MFA checks. - * - * @param body HTTP request body - * @param salt salt to use - * @param h_body resulting hash - */ void TALER_MERCHANT_mfa_body_hash ( const json_t *body, @@ -155,16 +123,22 @@ TALER_MERCHANT_mfa_body_hash ( struct GNUNET_HashCode hash; struct GNUNET_HashContext *hc; - json_str = json_dumps (body, - JSON_COMPACT | JSON_SORT_KEYS); + if (NULL == body) + json_str = NULL; + else + json_str = json_dumps (body, + JSON_COMPACT | JSON_SORT_KEYS); GNUNET_assert (NULL != json_str); hc = GNUNET_CRYPTO_hash_context_start (); GNUNET_CRYPTO_hash_context_read (hc, salt, sizeof (*salt)); - GNUNET_CRYPTO_hash_context_read (hc, - json_str, - strlen (json_str)); + if (NULL != json_str) + { + GNUNET_CRYPTO_hash_context_read (hc, + json_str, + strlen (json_str)); + } GNUNET_CRYPTO_hash_context_finish (hc, &hash); GNUNET_free (json_str);