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:
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 */