merchant

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

taler-merchant-httpd_mfa.c (22855B)


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