merchant

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

taler-merchant-httpd_mfa.c (22385B)


      1 /*
      2   This file is part of TALER
      3   (C) 2025 Taler Systems SA
      4 
      5   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   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 taler-merchant-httpd_mfa.c
     22  * @brief internal APIs for multi-factor authentication (MFA)
     23  * @author Christian Grothoff
     24  */
     25 #include "taler/platform.h"
     26 #include "taler-merchant-httpd.h"
     27 #include "taler-merchant-httpd_mfa.h"
     28 
     29 
     30 /**
     31  * How many challenges do we allow at most per request?
     32  */
     33 #define MAX_CHALLENGES 9
     34 
     35 /**
     36  * How long are challenges valid?
     37  */
     38 #define CHALLENGE_LIFETIME GNUNET_TIME_UNIT_DAYS
     39 
     40 
     41 enum GNUNET_GenericReturnValue
     42 TMH_mfa_parse_challenge_id (struct TMH_HandlerContext *hc,
     43                             const char *challenge_id,
     44                             uint64_t *challenge_serial,
     45                             struct TALER_MERCHANT_MFA_BodyHash *h_body)
     46 {
     47   const char *dash = strchr (challenge_id,
     48                              '-');
     49   unsigned long long ser;
     50   char min;
     51 
     52   if (NULL == dash)
     53   {
     54     GNUNET_break_op (0);
     55     return (MHD_NO ==
     56             TALER_MHD_reply_with_error (hc->connection,
     57                                         MHD_HTTP_BAD_REQUEST,
     58                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     59                                         "'-' missing in challenge ID"))
     60       ? GNUNET_SYSERR
     61       : GNUNET_NO;
     62   }
     63   if ( (2 !=
     64         sscanf (challenge_id,
     65                 "%llu%c%*s",
     66                 &ser,
     67                 &min)) ||
     68        ('-' != min) )
     69   {
     70     GNUNET_break_op (0);
     71     return (MHD_NO ==
     72             TALER_MHD_reply_with_error (hc->connection,
     73                                         MHD_HTTP_BAD_REQUEST,
     74                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     75                                         "Invalid number for challenge ID"))
     76       ? GNUNET_SYSERR
     77       : GNUNET_NO;
     78   }
     79   if (GNUNET_OK !=
     80       GNUNET_STRINGS_string_to_data (dash + 1,
     81                                      strlen (dash + 1),
     82                                      h_body,
     83                                      sizeof (*h_body)))
     84   {
     85     GNUNET_break_op (0);
     86     return (MHD_NO ==
     87             TALER_MHD_reply_with_error (hc->connection,
     88                                         MHD_HTTP_BAD_REQUEST,
     89                                         TALER_EC_GENERIC_PARAMETER_MALFORMED,
     90                                         "Malformed challenge ID"))
     91       ? GNUNET_SYSERR
     92       : GNUNET_NO;
     93   }
     94   *challenge_serial = (uint64_t) ser;
     95   return GNUNET_OK;
     96 }
     97 
     98 
     99 /**
    100  * Check if the given authentication check was already completed.
    101  *
    102  * @param[in,out] hc handler context of the connection to authorize
    103  * @param op operation for which we are requiring authorization
    104  * @param challenge_id ID of the challenge to check if it is done
    105  * @param[out] solved set to true if the challenge was solved,
    106  *             set to false if @a challenge_id was not found
    107  * @param[out] channel TAN channel that was used,
    108  *             set to #TALER_MERCHANT_MFA_CHANNEL_NONE if @a challenge_id
    109  *             was not found
    110  * @param[out] target_address address which was validated,
    111  *             set to NULL if @a challenge_id was not found
    112  * @param[out] retry_counter how many attempts are left on the challenge
    113  * @return #GNUNET_OK on success (challenge found)
    114  *         #GNUNET_NO if an error message was returned to the client
    115  *         #GNUNET_SYSERR to just close the connection
    116  */
    117 static enum GNUNET_GenericReturnValue
    118 mfa_challenge_check (
    119   struct TMH_HandlerContext *hc,
    120   enum TALER_MERCHANT_MFA_CriticalOperation op,
    121   const char *challenge_id,
    122   bool *solved,
    123   enum TALER_MERCHANT_MFA_Channel *channel,
    124   char **target_address,
    125   uint32_t *retry_counter)
    126 {
    127   uint64_t challenge_serial;
    128   struct TALER_MERCHANT_MFA_BodyHash h_body;
    129   struct TALER_MERCHANT_MFA_BodyHash x_h_body;
    130   struct TALER_MERCHANT_MFA_BodySalt salt;
    131   struct GNUNET_TIME_Absolute retransmission_date;
    132   enum TALER_MERCHANT_MFA_CriticalOperation xop;
    133   enum GNUNET_DB_QueryStatus qs;
    134   struct GNUNET_TIME_Absolute confirmation_date;
    135   enum GNUNET_GenericReturnValue ret;
    136 
    137   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    138               "Checking status of challenge %s\n",
    139               challenge_id);
    140   ret = TMH_mfa_parse_challenge_id (hc,
    141                                     challenge_id,
    142                                     &challenge_serial,
    143                                     &x_h_body);
    144   if (GNUNET_OK != ret)
    145     return ret;
    146   *target_address = NULL;
    147   *solved = false;
    148   *channel = TALER_MERCHANT_MFA_CHANNEL_NONE;
    149   *retry_counter = UINT_MAX;
    150   qs = TMH_db->lookup_mfa_challenge (TMH_db->cls,
    151                                      challenge_serial,
    152                                      &x_h_body,
    153                                      &salt,
    154                                      target_address,
    155                                      &xop,
    156                                      &confirmation_date,
    157                                      &retransmission_date,
    158                                      retry_counter,
    159                                      channel);
    160   switch (qs)
    161   {
    162   case GNUNET_DB_STATUS_HARD_ERROR:
    163     GNUNET_break (0);
    164     return (MHD_NO ==
    165             TALER_MHD_reply_with_error (hc->connection,
    166                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    167                                         TALER_EC_GENERIC_DB_COMMIT_FAILED,
    168                                         NULL))
    169       ? GNUNET_SYSERR
    170       : GNUNET_NO;
    171   case GNUNET_DB_STATUS_SOFT_ERROR:
    172     GNUNET_break (0);
    173     return (MHD_NO ==
    174             TALER_MHD_reply_with_error (hc->connection,
    175                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    176                                         TALER_EC_GENERIC_DB_SOFT_FAILURE,
    177                                         NULL))
    178       ? GNUNET_SYSERR
    179       : GNUNET_NO;
    180   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    181     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    182                 "Challenge %s not found\n",
    183                 challenge_id);
    184     return GNUNET_OK;
    185   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    186     break;
    187   }
    188 
    189   if (xop != op)
    190   {
    191     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    192                 "Challenge was for a different operation (%d!=%d)!\n",
    193                 (int) op,
    194                 (int) xop);
    195     *solved = false;
    196     return GNUNET_OK;
    197   }
    198   TALER_MERCHANT_mfa_body_hash (hc->request_body,
    199                                 &salt,
    200                                 &h_body);
    201   if (0 !=
    202       GNUNET_memcmp (&h_body,
    203                      &x_h_body))
    204   {
    205     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    206                 "Challenge was for a different request body!\n");
    207     *solved = false;
    208     return GNUNET_OK;
    209   }
    210   *solved = (! GNUNET_TIME_absolute_is_future (confirmation_date));
    211   return GNUNET_OK;
    212 }
    213 
    214 
    215 /**
    216  * Multi-factor authentication check to see if for the given @a instance_id
    217  * and the @a op operation all the TAN channels given in @a required_tans have
    218  * been satisfied.  Note that we always satisfy @a required_tans in the order
    219  * given in the array, so if the last one is satisfied, all previous ones must
    220  * have been satisfied before.
    221  *
    222  * If the challenges has not been satisfied, an appropriate response
    223  * is returned to the client of @a hc.
    224  *
    225  * @param[in,out] hc handler context of the connection to authorize
    226  * @param op operation for which we are performing
    227  * @param channel TAN channel to try
    228  * @param expiration_date when should the challenge expire
    229  * @param required_address addresses to use for
    230  *        the respective challenge
    231  * @param[out] challenge_id set to the challenge ID, to be freed by
    232  *   the caller
    233  * @return #GNUNET_OK on success,
    234  *         #GNUNET_NO if an error message was returned to the client
    235  *         #GNUNET_SYSERR to just close the connection
    236  */
    237 static enum GNUNET_GenericReturnValue
    238 mfa_challenge_start (
    239   struct TMH_HandlerContext *hc,
    240   enum TALER_MERCHANT_MFA_CriticalOperation op,
    241   enum TALER_MERCHANT_MFA_Channel channel,
    242   struct GNUNET_TIME_Absolute expiration_date,
    243   const char *required_address,
    244   char **challenge_id)
    245 {
    246   enum GNUNET_DB_QueryStatus qs;
    247   struct TALER_MERCHANT_MFA_BodySalt salt;
    248   struct TALER_MERCHANT_MFA_BodyHash h_body;
    249   uint64_t challenge_serial;
    250   unsigned long long challenge_num;
    251   char *code;
    252 
    253   GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
    254                               &salt,
    255                               sizeof (salt));
    256   TALER_MERCHANT_mfa_body_hash (hc->request_body,
    257                                 &salt,
    258                                 &h_body);
    259   challenge_num = (unsigned long long)
    260                   GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE,
    261                                             1000 * 1000 * 100);
    262   /* Note: if this is changed, the code in
    263      taler-merchant-httpd_post-challenge-ID.c and
    264      taler-merchant-httpd_post-challenge-ID-confirm.c must
    265      possibly also be updated! */
    266   GNUNET_asprintf (&code,
    267                    "%04llu-%04llu",
    268                    challenge_num / 10000,
    269                    challenge_num % 10000);
    270   qs = TMH_db->create_mfa_challenge (TMH_db->cls,
    271                                      op,
    272                                      &h_body,
    273                                      &salt,
    274                                      code,
    275                                      expiration_date,
    276                                      GNUNET_TIME_UNIT_ZERO_ABS,
    277                                      channel,
    278                                      required_address,
    279                                      &challenge_serial);
    280   GNUNET_free (code);
    281   switch (qs)
    282   {
    283   case GNUNET_DB_STATUS_HARD_ERROR:
    284     GNUNET_break (0);
    285     return (MHD_NO ==
    286             TALER_MHD_reply_with_error (hc->connection,
    287                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    288                                         TALER_EC_GENERIC_DB_COMMIT_FAILED,
    289                                         NULL))
    290       ? GNUNET_SYSERR
    291       : GNUNET_NO;
    292   case GNUNET_DB_STATUS_SOFT_ERROR:
    293     GNUNET_break (0);
    294     return (MHD_NO ==
    295             TALER_MHD_reply_with_error (hc->connection,
    296                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
    297                                         TALER_EC_GENERIC_DB_SOFT_FAILURE,
    298                                         NULL))
    299       ? GNUNET_SYSERR
    300       : GNUNET_NO;
    301   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    302     GNUNET_assert (0);
    303     break;
    304   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    305     break;
    306   }
    307   {
    308     char *h_body_s;
    309 
    310     h_body_s = GNUNET_STRINGS_data_to_string_alloc (&h_body,
    311                                                     sizeof (h_body));
    312     GNUNET_asprintf (challenge_id,
    313                      "%llu-%s",
    314                      (unsigned long long) challenge_serial,
    315                      h_body_s);
    316     GNUNET_free (h_body_s);
    317   }
    318   return GNUNET_OK;
    319 }
    320 
    321 
    322 /**
    323  * Internal book-keeping for #TMH_mfa_challenges_do().
    324  */
    325 struct Challenge
    326 {
    327   /**
    328    * Channel on which the challenge is transmitted.
    329    */
    330   enum TALER_MERCHANT_MFA_Channel channel;
    331 
    332   /**
    333    * Address to send the challenge to.
    334    */
    335   const char *required_address;
    336 
    337   /**
    338    * Internal challenge ID.
    339    */
    340   char *challenge_id;
    341 
    342   /**
    343    * True if the challenge was solved.
    344    */
    345   bool solved;
    346 
    347   /**
    348    * True if the challenge could still be solved.
    349    */
    350   bool solvable;
    351 
    352 };
    353 
    354 
    355 /**
    356  * Obtain hint about the @a target_address of type @a channel to
    357  * return to the client.
    358  *
    359  * @param channel type of challenge
    360  * @param target_address address we will sent the challenge to
    361  * @return hint for the user about the address
    362  */
    363 static char *
    364 get_hint (enum TALER_MERCHANT_MFA_Channel channel,
    365           const char *target_address)
    366 {
    367   switch (channel)
    368   {
    369   case TALER_MERCHANT_MFA_CHANNEL_NONE:
    370     GNUNET_assert (0);
    371     return NULL;
    372   case TALER_MERCHANT_MFA_CHANNEL_SMS:
    373     {
    374       size_t slen = strlen (target_address);
    375       const char *end;
    376 
    377       if (slen > 4)
    378         end = &target_address[slen - 4];
    379       else
    380         end = &target_address[slen / 2];
    381       return GNUNET_strdup (end);
    382     }
    383   case TALER_MERCHANT_MFA_CHANNEL_EMAIL:
    384     {
    385       const char *at;
    386       size_t len;
    387 
    388       at = strchr (target_address,
    389                    '@');
    390       if (NULL == at)
    391         len = 0;
    392       else
    393         len = at - target_address;
    394       return GNUNET_strndup (target_address,
    395                              len);
    396     }
    397   case TALER_MERCHANT_MFA_CHANNEL_TOTP:
    398     GNUNET_break (0);
    399     return GNUNET_strdup ("TOTP is not implemented: #10327");
    400   }
    401   GNUNET_break (0);
    402   return NULL;
    403 }
    404 
    405 
    406 /**
    407  * Check that a set of MFA challenges has been satisfied by the
    408  * client for the request in @a hc.
    409  *
    410  * @param[in,out] hc handler context with the connection to the client
    411  * @param op operation for which we should check challenges for
    412  * @param combi_and true to tell the client to solve all challenges (AND),
    413  *       false means that any of the challenges will do (OR)
    414  * @param ... pairs of channel and address, terminated by
    415  *        #TALER_MERCHANT_MFA_CHANNEL_NONE
    416  * @return #GNUNET_OK on success (challenges satisfied)
    417  *         #GNUNET_NO if an error message was returned to the client
    418  *         #GNUNET_SYSERR to just close the connection
    419  */
    420 enum GNUNET_GenericReturnValue
    421 TMH_mfa_challenges_do (
    422   struct TMH_HandlerContext *hc,
    423   enum TALER_MERCHANT_MFA_CriticalOperation op,
    424   bool combi_and,
    425   ...)
    426 {
    427   struct Challenge challenges[MAX_CHALLENGES];
    428   const char *challenge_ids[MAX_CHALLENGES];
    429   size_t num_challenges;
    430   char *challenge_ids_copy = NULL;
    431   size_t num_provided_challenges;
    432   enum GNUNET_GenericReturnValue ret;
    433 
    434   {
    435     va_list ap;
    436 
    437     va_start (ap,
    438               combi_and);
    439     num_challenges = 0;
    440     while (num_challenges < MAX_CHALLENGES)
    441     {
    442       enum TALER_MERCHANT_MFA_Channel channel;
    443       const char *address;
    444 
    445       channel = va_arg (ap,
    446                         enum TALER_MERCHANT_MFA_Channel);
    447       if (TALER_MERCHANT_MFA_CHANNEL_NONE == channel)
    448         break;
    449       address = va_arg (ap,
    450                         const char *);
    451       if (NULL == address)
    452         continue;
    453       challenges[num_challenges].channel = channel;
    454       challenges[num_challenges].required_address = address;
    455       challenges[num_challenges].challenge_id = NULL;
    456       challenges[num_challenges].solved = false;
    457       challenges[num_challenges].solvable = true;
    458       num_challenges++;
    459     }
    460     va_end (ap);
    461   }
    462 
    463   if (0 == num_challenges)
    464   {
    465     /* No challenges required. Strange... */
    466     return GNUNET_OK;
    467   }
    468 
    469   {
    470     const char *challenge_ids_header;
    471 
    472     challenge_ids_header
    473       = MHD_lookup_connection_value (hc->connection,
    474                                      MHD_HEADER_KIND,
    475                                      "Taler-Challenge-Ids");
    476     num_provided_challenges = 0;
    477     if (NULL != challenge_ids_header)
    478     {
    479       challenge_ids_copy = GNUNET_strdup (challenge_ids_header);
    480 
    481       for (char *token = strtok (challenge_ids_copy,
    482                                  ",");
    483            NULL != token;
    484            token = strtok (NULL,
    485                            ","))
    486       {
    487         if (num_provided_challenges >= MAX_CHALLENGES)
    488         {
    489           GNUNET_break_op (0);
    490           GNUNET_free (challenge_ids_copy);
    491           return (MHD_NO ==
    492                   TALER_MHD_reply_with_error (
    493                     hc->connection,
    494                     MHD_HTTP_BAD_REQUEST,
    495                     TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
    496                     "Taler-Challenge-Ids"))
    497           ? GNUNET_SYSERR
    498           : GNUNET_NO;
    499         }
    500         challenge_ids[num_provided_challenges] = token;
    501         num_provided_challenges++;
    502       }
    503     }
    504   }
    505 
    506   /* Check provided challenges against requirements */
    507   for (size_t i = 0; i < num_provided_challenges; i++)
    508   {
    509     bool solved;
    510     enum TALER_MERCHANT_MFA_Channel channel;
    511     char *target_address;
    512     uint32_t retry_counter;
    513 
    514     ret = mfa_challenge_check (hc,
    515                                op,
    516                                challenge_ids[i],
    517                                &solved,
    518                                &channel,
    519                                &target_address,
    520                                &retry_counter);
    521     if (GNUNET_OK != ret)
    522       goto cleanup;
    523     for (size_t j = 0; j < num_challenges; j++)
    524     {
    525       if ( (challenges[j].channel == channel) &&
    526            (NULL == challenges[j].challenge_id) &&
    527            (NULL != target_address /* just to be sure */) &&
    528            (0 == strcmp (target_address,
    529                          challenges[j].required_address) ) )
    530       {
    531         challenges[j].solved
    532           = solved;
    533         challenges[j].challenge_id
    534           = GNUNET_strdup (challenge_ids[i]);
    535         if ( (! solved) &&
    536              (0 == retry_counter) )
    537         {
    538           /* can't be solved anymore! */
    539           challenges[i].solvable = false;
    540         }
    541         break;
    542       }
    543     }
    544     GNUNET_free (target_address);
    545   }
    546 
    547   {
    548     struct GNUNET_TIME_Absolute expiration_date
    549       = GNUNET_TIME_relative_to_absolute (CHALLENGE_LIFETIME);
    550 
    551     /* Start new challenges for unsolved requirements */
    552     for (size_t i = 0; i < num_challenges; i++)
    553     {
    554       if (NULL == challenges[i].challenge_id)
    555       {
    556         GNUNET_assert (! challenges[i].solved);
    557         GNUNET_assert (challenges[i].solvable);
    558         ret = mfa_challenge_start (hc,
    559                                    op,
    560                                    challenges[i].channel,
    561                                    expiration_date,
    562                                    challenges[i].required_address,
    563                                    &challenges[i].challenge_id);
    564         if (GNUNET_OK != ret)
    565           goto cleanup;
    566       }
    567     }
    568   }
    569 
    570   {
    571     bool all_solved = true;
    572     bool any_solved = false;
    573     bool solvable = true;
    574 
    575     for (size_t i = 0; i < num_challenges; i++)
    576     {
    577       if (challenges[i].solved)
    578       {
    579         any_solved = true;
    580       }
    581       else
    582       {
    583         all_solved = false;
    584         if (combi_and &&
    585             (! challenges[i].solvable) )
    586           solvable = false;
    587       }
    588     }
    589 
    590     if ( (combi_and && all_solved) ||
    591          (! combi_and && any_solved) )
    592     {
    593       /* Authorization successful */
    594       ret = GNUNET_OK;
    595       goto cleanup;
    596     }
    597     if (! solvable)
    598     {
    599       ret = (MHD_NO ==
    600              TALER_MHD_reply_with_error (
    601                hc->connection,
    602                MHD_HTTP_FORBIDDEN,
    603                TALER_EC_MERCHANT_MFA_FORBIDDEN,
    604                GNUNET_TIME_relative2s (CHALLENGE_LIFETIME,
    605                                        false)))
    606         ? GNUNET_SYSERR
    607         : GNUNET_NO;
    608       goto cleanup;
    609     }
    610   }
    611 
    612   /* Return challenges to client */
    613   {
    614     json_t *jchallenges;
    615 
    616     jchallenges = json_array ();
    617     GNUNET_assert (NULL != jchallenges);
    618     for (size_t i = 0; i<num_challenges; i++)
    619     {
    620       const struct Challenge *c = &challenges[i];
    621       json_t *jc;
    622       char *hint;
    623 
    624       hint = get_hint (c->channel,
    625                        c->required_address);
    626 
    627       jc = GNUNET_JSON_PACK (
    628         GNUNET_JSON_pack_string ("tan_info",
    629                                  hint),
    630         GNUNET_JSON_pack_string ("tan_channel",
    631                                  TALER_MERCHANT_MFA_channel_to_string (
    632                                    c->channel)),
    633         GNUNET_JSON_pack_string ("challenge_id",
    634                                  c->challenge_id));
    635       GNUNET_free (hint);
    636       GNUNET_assert (0 ==
    637                      json_array_append_new (
    638                        jchallenges,
    639                        jc));
    640     }
    641     ret = (MHD_NO ==
    642            TALER_MHD_REPLY_JSON_PACK (
    643              hc->connection,
    644              MHD_HTTP_ACCEPTED,
    645              GNUNET_JSON_pack_bool ("combi_and",
    646                                     combi_and),
    647              GNUNET_JSON_pack_array_steal ("challenges",
    648                                            jchallenges)))
    649       ? GNUNET_SYSERR
    650       : GNUNET_NO;
    651   }
    652 
    653 cleanup:
    654   for (size_t i = 0; i < num_challenges; i++)
    655     GNUNET_free (challenges[i].challenge_id);
    656   GNUNET_free (challenge_ids_copy);
    657   return ret;
    658 }
    659 
    660 
    661 enum GNUNET_GenericReturnValue
    662 TMH_mfa_check_simple (
    663   struct TMH_HandlerContext *hc,
    664   enum TALER_MERCHANT_MFA_CriticalOperation op,
    665   struct TMH_MerchantInstance *mi)
    666 {
    667   enum GNUNET_GenericReturnValue ret;
    668   bool have_sms = (NULL != mi->settings.phone) &&
    669                   (NULL != TMH_helper_sms) &&
    670                   (mi->settings.phone_validated);
    671   bool have_email = (NULL != mi->settings.email) &&
    672                     (NULL != TMH_helper_email) &&
    673                     (mi->settings.email_validated);
    674 
    675   /* Note: we check for 'validated' above, but in theory
    676      we could also use unvalidated for this operation.
    677      That's a policy-decision we may want to revise,
    678      but probably need to look at the global threat model to
    679      make sure alternative configurations are still sane. */
    680   if (have_email)
    681   {
    682     ret = TMH_mfa_challenges_do (hc,
    683                                  op,
    684                                  false,
    685                                  TALER_MERCHANT_MFA_CHANNEL_EMAIL,
    686                                  mi->settings.email,
    687                                  have_sms
    688                                  ? TALER_MERCHANT_MFA_CHANNEL_SMS
    689                                  : TALER_MERCHANT_MFA_CHANNEL_NONE,
    690                                  mi->settings.phone,
    691                                  TALER_MERCHANT_MFA_CHANNEL_NONE);
    692   }
    693   else if (have_sms)
    694   {
    695     ret = TMH_mfa_challenges_do (hc,
    696                                  op,
    697                                  false,
    698                                  TALER_MERCHANT_MFA_CHANNEL_SMS,
    699                                  mi->settings.phone,
    700                                  TALER_MERCHANT_MFA_CHANNEL_NONE);
    701   }
    702   else
    703   {
    704     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    705                 "No MFA possible, skipping 2-FA\n");
    706     ret = GNUNET_OK;
    707   }
    708   return ret;
    709 }