merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit ed39875bdd918584cd2f3e48797522dede1ab29e
parent 540dff0e1170c3e0f6da60f449e0056f3997f3e6
Author: Florian Dold <dold@taler.net>
Date:   Sat, 29 Aug 2026 18:50:07 +0200

authentication: require current password for credential changes

Diffstat:
Msrc/backend/taler-merchant-httpd_get-config.c | 2+-
Msrc/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.c | 40++++++++++++++++++++++++++++++++++++++++
Msrc/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.h | 5++---
Msrc/include/taler/merchant/post-management-instances-INSTANCE-auth.h | 30+++++++++++++++++++++++++++++-
Msrc/include/taler/taler_merchant_testing_lib.h | 21+++++++++++++++++++++
Msrc/lib/merchant_api_get-config.c | 4++--
Msrc/lib/merchant_api_post-management-instances-INSTANCE-auth.c | 17+++++++++++++++++
Msrc/testing/test_merchant_api.c | 6++++--
Msrc/testing/test_merchant_instance_auth.sh | 26++++++++++++++++++++++++++
Msrc/testing/test_merchant_mfa.sh | 19++++++++++++++++++-
Msrc/testing/testing_api_cmd_instance_auth.c | 37+++++++++++++++++++++++++++++++------
11 files changed, 191 insertions(+), 16 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_get-config.c b/src/backend/taler-merchant-httpd_get-config.c @@ -44,7 +44,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_get_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "39:0:27" +#define MERCHANT_PROTOCOL_VERSION "40:0:28" /** diff --git a/src/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.c b/src/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.c @@ -92,6 +92,8 @@ reply_with_login_token ( * @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 require_old_password require the current password when the + * instance currently uses password authentication * @param tcs set of multi-factor authorizations required * @param login_token_duration how long a login token returned after the * update should remain valid; zero means do not create a token @@ -102,6 +104,7 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, struct MHD_Connection *connection, struct TMH_HandlerContext *hc, bool auth_override, + bool require_old_password, enum TEH_TanChannelSet tcs, struct GNUNET_TIME_Relative login_token_duration) { @@ -110,6 +113,7 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, struct GNUNET_TIME_Timestamp token_creation_time; struct GNUNET_TIME_Timestamp token_expiration_time; const char *auth_pw = NULL; + const char *old_password = NULL; json_t *jauth = hc->request_body; bool issue_login_token = ! GNUNET_TIME_relative_is_zero (login_token_duration); @@ -123,6 +127,26 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, = GNUNET_TIME_relative_to_timestamp (login_token_duration); } + if (require_old_password) + { + json_t *jold_password = json_object_get (jauth, + "old_password"); + + if (NULL != jold_password) + { + old_password = json_string_value (jold_password); + if (NULL == old_password) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH, + "old_password must be a string"); + } + } + } + { enum GNUNET_GenericReturnValue ret; @@ -293,6 +317,19 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, NULL); } } + if (require_old_password && + (GNUNET_OK != + TMH_check_auth (old_password, + &db_ias.auth_salt, + &db_ias.auth_hash))) + { + TALER_MERCHANTDB_rollback (TMH_db); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_UNAUTHORIZED, + TALER_EC_MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_OLD_PASSWORD, + NULL); + } } qs = TALER_MERCHANTDB_update_instance_auth (TMH_db, @@ -380,6 +417,7 @@ TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh, connection, hc, false, + true, TMH_TCS_NONE, GNUNET_TIME_UNIT_ZERO); } @@ -440,6 +478,7 @@ TMH_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh, connection, hc, false, + false, TEH_mandatory_tan_channels, token_duration); } @@ -467,6 +506,7 @@ TMH_private_post_instances_default_ID_auth ( connection, hc, true, + false, TMH_TCS_NONE, GNUNET_TIME_UNIT_ZERO); return ret; diff --git a/src/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.h b/src/backend/taler-merchant-httpd_post-management-instances-INSTANCE-auth.h @@ -63,9 +63,8 @@ TMH_private_post_instances_default_ID_auth ( /** * 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. + * the original password was forgotten. Requires every TAN + * channel configured in MANDATORY_TAN_CHANNELS for the account. * * @param rh context of the handler * @param connection the MHD connection to handle diff --git a/src/include/taler/merchant/post-management-instances-INSTANCE-auth.h b/src/include/taler/merchant/post-management-instances-INSTANCE-auth.h @@ -71,7 +71,15 @@ enum TALER_MERCHANT_PostManagementInstancesAuthOption * the "external" authentication method is used. * Value type: const char *. */ - TALER_MERCHANT_POST_MANAGEMENT_INSTANCES_AUTH_OPTION_PASSWORD + TALER_MERCHANT_POST_MANAGEMENT_INSTANCES_AUTH_OPTION_PASSWORD, + + /** + * Set the current authentication password. Required when changing + * password authentication through the instance's private endpoint. + * Administrative changes do not require this option. + * Value type: const char *. + */ + TALER_MERCHANT_POST_MANAGEMENT_INSTANCES_AUTH_OPTION_OLD_PASSWORD }; @@ -97,6 +105,12 @@ struct TALER_MERCHANT_PostManagementInstancesAuthOptionValue */ const char *password; + /** + * Value if @e option is + * #TALER_MERCHANT_POST_MANAGEMENT_INSTANCES_AUTH_OPTION_OLD_PASSWORD. + */ + const char *old_password; + } details; }; @@ -116,6 +130,20 @@ struct TALER_MERCHANT_PostManagementInstancesAuthOptionValue /** + * Set current password for self-service reauthentication. + * + * @param pw current password + * @return representation of the option + */ +#define TALER_MERCHANT_post_management_instances_auth_option_old_password(pw) \ + (const struct TALER_MERCHANT_PostManagementInstancesAuthOptionValue) \ + { \ + .option = TALER_MERCHANT_POST_MANAGEMENT_INSTANCES_AUTH_OPTION_OLD_PASSWORD, \ + .details.old_password = (pw) \ + } + + +/** * Set up POST /management/instances/$INSTANCE/auth operation. * Note that you must explicitly start the operation after * possibly setting options. diff --git a/src/include/taler/taler_merchant_testing_lib.h b/src/include/taler/taler_merchant_testing_lib.h @@ -123,6 +123,27 @@ TALER_TESTING_cmd_merchant_post_instance_auth (const char *label, /** + * Define a "POST /private/auth" CMD with current-password + * reauthentication. + * + * @param label command label. + * @param merchant_url base URL of the merchant serving the request + * @param instance_id the ID of the instance, or NULL + * @param auth_token new auth token to use, can be NULL for no auth + * @param old_password current password, or NULL for administrative changes + * @param http_status expected HTTP response code + * @return the command + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_instance_auth2 (const char *label, + const char *merchant_url, + const char *instance_id, + const char *auth_token, + const char *old_password, + unsigned int http_status); + + +/** * Define a "POST /private/token" CMD. * * @param label command label. diff --git a/src/lib/merchant_api_get-config.c b/src/lib/merchant_api_get-config.c @@ -34,12 +34,12 @@ * Which version of the Taler protocol is implemented * by this library? Used to determine compatibility. */ -#define MERCHANT_PROTOCOL_CURRENT 39 +#define MERCHANT_PROTOCOL_CURRENT 40 /** * How many configs are we backwards-compatible with? */ -#define MERCHANT_PROTOCOL_AGE 15 +#define MERCHANT_PROTOCOL_AGE 16 /** * How many exchanges do we allow at most per merchant? diff --git a/src/lib/merchant_api_post-management-instances-INSTANCE-auth.c b/src/lib/merchant_api_post-management-instances-INSTANCE-auth.c @@ -81,6 +81,11 @@ struct TALER_MERCHANT_PostManagementInstancesAuthHandle * New authentication password. */ char *auth_password; + + /** + * Current authentication password for self-service changes. + */ + char *old_password; }; @@ -185,6 +190,12 @@ TALER_MERCHANT_post_management_instances_auth_set_options_ ( piah->auth_password = GNUNET_strdup ( options[i].details.password); break; + case TALER_MERCHANT_POST_MANAGEMENT_INSTANCES_AUTH_OPTION_OLD_PASSWORD: + GNUNET_free (piah->old_password); + if (NULL != options[i].details.old_password) + piah->old_password = GNUNET_strdup ( + options[i].details.old_password); + break; } } } @@ -237,6 +248,11 @@ TALER_MERCHANT_post_management_instances_auth_start ( GNUNET_JSON_pack_string ("password", piah->auth_password)); } + if (NULL != piah->old_password) + GNUNET_assert (0 == + json_object_set_new (req_obj, + "old_password", + json_string (piah->old_password))); eh = TALER_MERCHANT_curl_easy_get_ (piah->url); if ( (NULL == eh) || (GNUNET_OK != @@ -279,6 +295,7 @@ TALER_MERCHANT_post_management_instances_auth_cancel ( TALER_curl_easy_post_finished (&piah->post_ctx); GNUNET_free (piah->instance_id); GNUNET_free (piah->auth_password); + GNUNET_free (piah->old_password); GNUNET_free (piah->url); GNUNET_free (piah->base_url); GNUNET_free (piah); diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -1600,17 +1600,19 @@ run (void *cls, TALER_TESTING_cmd_merchant_set_instance_token ( "instance-create-i1a-token-set-again", "set-auth-valid-again"), - TALER_TESTING_cmd_merchant_post_instance_auth ( + TALER_TESTING_cmd_merchant_post_instance_auth2 ( "instance-create-i1a-auth-ok-idempotent", merchant_url_i1a, NULL, RFC_8959_PREFIX "my-other-secret", + "my-secret", MHD_HTTP_NO_CONTENT), - TALER_TESTING_cmd_merchant_post_instance_auth ( + TALER_TESTING_cmd_merchant_post_instance_auth2 ( "instance-create-i1a-clear-auth", merchant_url_i1a, NULL, NULL, + "my-other-secret", MHD_HTTP_NO_CONTENT), TALER_TESTING_cmd_set_authorization ("set-auth-none", NULL), diff --git a/src/testing/test_merchant_instance_auth.sh b/src/testing/test_merchant_instance_auth.sh @@ -302,6 +302,32 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -d '{"method":"token","password":"again"}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") +if [ "$STATUS" != "401" ] || + [ "$(jq -r .code < "$LAST_RESPONSE")" != "2604" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected missing old password to fail with 401/2604. Got: $STATUS" +fi + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Bearer '"$RWTOKEN" \ + http://localhost:9966/instances/second/private/auth \ + -d '{"method":"token","password":"again","old_password":"wrong"}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "401" ] || + [ "$(jq -r .code < "$LAST_RESPONSE")" != "2604" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected wrong old password to fail with 401/2604. Got: $STATUS" +fi + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Bearer '"$RWTOKEN" \ + http://localhost:9966/instances/second/private/auth \ + -d '{"method":"token","password":"again","old_password":"new_one"}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + BASIC_AUTH2=$(echo -n second:again | base64) if [ "$STATUS" != "204" ] diff --git a/src/testing/test_merchant_mfa.sh b/src/testing/test_merchant_mfa.sh @@ -171,7 +171,6 @@ then fi echo "OK" - echo -n "Requesting challenge $C2 " STATUS=$(curl -H "Content-Type: application/json" -X POST \ @@ -422,6 +421,24 @@ then fi echo "OK" +echo -n "Reject password reset with only one mandatory challenge solved " +STATUS=$(curl \ + -X POST \ + -H "Content-Type: application/json" \ + -H "Taler-Challenge-Ids: $C1" \ + http://localhost:9966/instances/self/forgot-password \ + -d '{"method":"token","password":"amnesia"}' \ + -w "%{http_code}" -s \ + -o "$LAST_RESPONSE") + +if [ "$STATUS" != "202" ] || + [ "$(jq -r .combi_and < "$LAST_RESPONSE")" != "true" ] +then + jq < "$LAST_RESPONSE" + exit_fail "Expected both mandatory channels to remain required. Got: $STATUS" +fi +echo "OK" + echo -n "Requesting challenge $C2 " diff --git a/src/testing/testing_api_cmd_instance_auth.c b/src/testing/testing_api_cmd_instance_auth.c @@ -63,6 +63,11 @@ struct AuthInstanceState const char *auth_token; /** + * Current password, required for self-service changes. + */ + const char *old_password; + + /** * Expected HTTP response code. */ unsigned int http_status; @@ -134,7 +139,9 @@ auth_instance_run (void *cls, ais->instance_id); TALER_MERCHANT_post_management_instances_auth_set_options ( ais->iaph, - TALER_MERCHANT_post_management_instances_auth_option_password (ais->auth_token)); + TALER_MERCHANT_post_management_instances_auth_option_password (ais->auth_token), + TALER_MERCHANT_post_management_instances_auth_option_old_password ( + ais->old_password)); { enum TALER_ErrorCode ec; @@ -199,11 +206,12 @@ auth_instance_traits (void *cls, struct TALER_TESTING_Command -TALER_TESTING_cmd_merchant_post_instance_auth (const char *label, - const char *merchant_url, - const char *instance_id, - const char *auth_token, - unsigned int http_status) +TALER_TESTING_cmd_merchant_post_instance_auth2 (const char *label, + const char *merchant_url, + const char *instance_id, + const char *auth_token, + const char *old_password, + unsigned int http_status) { struct AuthInstanceState *ais; @@ -211,6 +219,7 @@ TALER_TESTING_cmd_merchant_post_instance_auth (const char *label, ais->merchant_url = merchant_url; ais->instance_id = instance_id; ais->auth_token = auth_token; + ais->old_password = old_password; ais->http_status = http_status; { @@ -227,4 +236,20 @@ TALER_TESTING_cmd_merchant_post_instance_auth (const char *label, } +struct TALER_TESTING_Command +TALER_TESTING_cmd_merchant_post_instance_auth (const char *label, + const char *merchant_url, + const char *instance_id, + const char *auth_token, + unsigned int http_status) +{ + return TALER_TESTING_cmd_merchant_post_instance_auth2 (label, + merchant_url, + instance_id, + auth_token, + NULL, + http_status); +} + + /* end of testing_api_cmd_auth_instance.c */