merchant

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

taler-merchant-httpd_post-management-instances-INSTANCE-auth.c (20042B)


      1 /*
      2   This file is part of GNU Taler
      3   (C) 2021 Taler Systems SA
      4 
      5   GNU 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   GNU 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_post-management-instances-INSTANCE-auth.c
     22  * @brief implementing POST /instances/$ID/auth request handling
     23  * @author Christian Grothoff
     24  * @author Florian Dold
     25  */
     26 #include "platform.h"
     27 #include "taler-merchant-httpd_post-management-instances-INSTANCE-auth.h"
     28 #include "taler-merchant-httpd_auth.h"
     29 #include "taler-merchant-httpd_helper.h"
     30 #include "taler-merchant-httpd_mfa.h"
     31 #include <taler/taler_json_lib.h>
     32 #include "merchant-database/get_instance_auth.h"
     33 #include "merchant-database/insert_login_token.h"
     34 #include "merchant-database/update_instance_auth.h"
     35 #include "merchant-database/start.h"
     36 
     37 
     38 /**
     39  * How often do we retry the simple INSERT database transaction?
     40  */
     41 #define MAX_RETRIES 3
     42 
     43 
     44 /**
     45  * Return a login token created as part of a password reset.
     46  *
     47  * @param connection connection to respond on
     48  * @param token binary value of the token
     49  * @param expiration_time when the token expires
     50  * @return MHD result code
     51  */
     52 static enum MHD_Result
     53 reply_with_login_token (
     54   struct MHD_Connection *connection,
     55   const struct TALER_MERCHANTDB_LoginTokenP *token,
     56   struct GNUNET_TIME_Timestamp expiration_time)
     57 {
     58   char *token_data;
     59   char *access_token;
     60   enum MHD_Result ret;
     61 
     62   token_data = GNUNET_STRINGS_data_to_string_alloc (token,
     63                                                     sizeof (*token));
     64   GNUNET_asprintf (&access_token,
     65                    RFC_8959_PREFIX "%s",
     66                    token_data);
     67   GNUNET_free (token_data);
     68   ret = TALER_MHD_REPLY_JSON_PACK (
     69     connection,
     70     MHD_HTTP_OK,
     71     GNUNET_JSON_pack_string ("access_token",
     72                              access_token),
     73     GNUNET_JSON_pack_string ("token",
     74                              access_token),
     75     GNUNET_JSON_pack_string ("scope",
     76                              "spa"),
     77     GNUNET_JSON_pack_bool ("refreshable",
     78                            true),
     79     GNUNET_JSON_pack_timestamp ("expiration",
     80                                 expiration_time));
     81   GNUNET_free (access_token);
     82   return ret;
     83 }
     84 
     85 
     86 /**
     87  * Change the authentication settings of an instance.
     88  *
     89  * @param mi instance to modify settings of
     90  * @param connection the MHD connection to handle
     91  * @param[in,out] hc context with further information about the request
     92  * @param auth_override The authentication settings for this instance
     93  *   do not apply due to administrative action. Do not check
     94  *   against the DB value when updating the auth token.
     95  * @param require_old_password require the current password when the
     96  *   instance currently uses password authentication
     97  * @param tcs set of multi-factor authorizations required
     98  * @param mfa_combi_and require all MFA channels in @a tcs instead of any one
     99  * @param login_token_duration how long a login token returned after the
    100  *   update should remain valid; zero means do not create a token
    101  * @param password_reset true if this is an unauthenticated password
    102  *   reset, which may only be started once per 15 minutes per instance
    103  * @return MHD result code
    104  */
    105 static enum MHD_Result
    106 post_instances_ID_auth (struct TMH_MerchantInstance *mi,
    107                         struct MHD_Connection *connection,
    108                         struct TMH_HandlerContext *hc,
    109                         bool auth_override,
    110                         bool require_old_password,
    111                         enum TEH_TanChannelSet tcs,
    112                         bool mfa_combi_and,
    113                         struct GNUNET_TIME_Relative login_token_duration,
    114                         bool password_reset)
    115 {
    116   struct TALER_MERCHANTDB_InstanceAuthSettings ias;
    117   struct TALER_MERCHANTDB_LoginTokenP login_token;
    118   struct GNUNET_TIME_Timestamp token_creation_time;
    119   struct GNUNET_TIME_Timestamp token_expiration_time;
    120   const char *auth_pw = NULL;
    121   const char *old_password = NULL;
    122   json_t *jauth = hc->request_body;
    123   bool issue_login_token
    124     = ! GNUNET_TIME_relative_is_zero (login_token_duration);
    125 
    126   if (issue_login_token)
    127   {
    128     GNUNET_CRYPTO_random_block (&login_token,
    129                                 sizeof (login_token));
    130     token_creation_time = GNUNET_TIME_timestamp_get ();
    131     token_expiration_time
    132       = GNUNET_TIME_relative_to_timestamp (login_token_duration);
    133   }
    134 
    135   if (require_old_password)
    136   {
    137     json_t *jold_password = json_object_get (jauth,
    138                                              "old_password");
    139 
    140     if (NULL != jold_password)
    141     {
    142       old_password = json_string_value (jold_password);
    143       if (NULL == old_password)
    144       {
    145         GNUNET_break_op (0);
    146         return TALER_MHD_reply_with_error (
    147           connection,
    148           MHD_HTTP_BAD_REQUEST,
    149           TALER_EC_MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH,
    150           "old_password must be a string");
    151       }
    152     }
    153   }
    154 
    155   {
    156     enum GNUNET_GenericReturnValue ret;
    157 
    158     ret = TMH_check_auth_config (connection,
    159                                  jauth,
    160                                  &auth_pw);
    161     if (GNUNET_OK != ret)
    162       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    163   }
    164 
    165   {
    166     enum TEH_TanChannelSet available_tcs = TMH_TCS_NONE;
    167     bool have_sms = (NULL != mi->settings.phone) &&
    168                     (NULL != TMH_helper_sms) &&
    169                     mi->settings.phone_validated;
    170     bool have_email = (NULL != mi->settings.email) &&
    171                       (NULL != TMH_helper_email) &&
    172                       mi->settings.email_validated;
    173 
    174     if (have_sms &&
    175         (0 != (tcs & TMH_TCS_SMS)))
    176       available_tcs |= TMH_TCS_SMS;
    177     if (have_email &&
    178         (0 != (tcs & TMH_TCS_EMAIL)))
    179       available_tcs |= TMH_TCS_EMAIL;
    180 
    181     if (mfa_combi_and &&
    182         (0 != (tcs & TMH_TCS_SMS)) &&
    183         (! have_sms))
    184     {
    185       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    186                   "Cannot change authentication: SMS factor not available\n");
    187       return TALER_MHD_reply_with_error (
    188         connection,
    189         MHD_HTTP_FORBIDDEN,
    190         TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
    191         "phone_number");
    192     }
    193     if (mfa_combi_and &&
    194         (0 != (tcs & TMH_TCS_EMAIL)) &&
    195         (! have_email))
    196     {
    197       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    198                   "Cannot change authentication: E-mail factor not available\n");
    199       return TALER_MHD_reply_with_error (
    200         connection,
    201         MHD_HTTP_FORBIDDEN,
    202         TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
    203         "email");
    204     }
    205     if ( (TMH_TCS_NONE != tcs) &&
    206          (TMH_TCS_NONE == available_tcs) )
    207     {
    208       const char *missing_factor;
    209 
    210       switch (tcs)
    211       {
    212       case TMH_TCS_SMS:
    213         missing_factor = "phone_number";
    214         break;
    215       case TMH_TCS_EMAIL:
    216         missing_factor = "email";
    217         break;
    218       case TMH_TCS_EMAIL_AND_SMS:
    219         missing_factor = "phone_number or email";
    220         break;
    221       case TMH_TCS_NONE:
    222         GNUNET_assert (0);
    223         missing_factor = NULL;
    224         break;
    225       }
    226       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    227                   "Cannot change authentication: no MFA factor available\n");
    228       return TALER_MHD_reply_with_error (
    229         connection,
    230         MHD_HTTP_FORBIDDEN,
    231         TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
    232         missing_factor);
    233     }
    234     tcs = available_tcs;
    235   }
    236   if (! auth_override)
    237   {
    238     enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; // fix -Wmaybe-uninitialized
    239 
    240     switch (tcs)
    241     {
    242     case TMH_TCS_NONE:
    243       ret = GNUNET_OK;
    244       break;
    245     case TMH_TCS_SMS:
    246       ret = TMH_mfa_auth_challenges_do (hc,
    247                                         mi->settings.id,
    248                                         password_reset,
    249                                         mfa_combi_and,
    250                                         TALER_MERCHANT_MFA_CHANNEL_SMS,
    251                                         mi->settings.phone,
    252                                         TALER_MERCHANT_MFA_CHANNEL_NONE);
    253       break;
    254     case TMH_TCS_EMAIL:
    255       ret = TMH_mfa_auth_challenges_do (hc,
    256                                         mi->settings.id,
    257                                         password_reset,
    258                                         mfa_combi_and,
    259                                         TALER_MERCHANT_MFA_CHANNEL_EMAIL,
    260                                         mi->settings.email,
    261                                         TALER_MERCHANT_MFA_CHANNEL_NONE);
    262       break;
    263     case TMH_TCS_EMAIL_AND_SMS:
    264       ret = TMH_mfa_auth_challenges_do (hc,
    265                                         mi->settings.id,
    266                                         password_reset,
    267                                         mfa_combi_and,
    268                                         TALER_MERCHANT_MFA_CHANNEL_EMAIL,
    269                                         mi->settings.email,
    270                                         TALER_MERCHANT_MFA_CHANNEL_SMS,
    271                                         mi->settings.phone,
    272                                         TALER_MERCHANT_MFA_CHANNEL_NONE);
    273       break;
    274     }
    275     if (GNUNET_OK != ret)
    276     {
    277       return (GNUNET_NO == ret)
    278         ? MHD_YES
    279         : MHD_NO;
    280     }
    281   }
    282 
    283   if (NULL == auth_pw)
    284   {
    285     memset (&ias.auth_salt,
    286             0,
    287             sizeof (ias.auth_salt));
    288     memset (&ias.auth_hash,
    289             0,
    290             sizeof (ias.auth_hash));
    291   }
    292   else
    293   {
    294     TMH_compute_auth (auth_pw,
    295                       &ias.auth_salt,
    296                       &ias.auth_hash);
    297   }
    298 
    299   /* Store the new auth information in the database */
    300   {
    301     enum GNUNET_DB_QueryStatus qs;
    302 
    303     for (unsigned int i = 0; i<MAX_RETRIES; i++)
    304     {
    305       if (GNUNET_OK !=
    306           TALER_MERCHANTDB_start (TMH_db,
    307                                   "post /instances/$ID/auth"))
    308       {
    309         return TALER_MHD_reply_with_error (connection,
    310                                            MHD_HTTP_INTERNAL_SERVER_ERROR,
    311                                            TALER_EC_GENERIC_DB_START_FAILED,
    312                                            NULL);
    313       }
    314 
    315       /* Make the authentication update a serializable operation.
    316          We first check that the authentication information
    317          that the caller's request authenticated with
    318          is still up to date.
    319          Otherwise, we've detected a conflicting update
    320          to the authentication. */
    321       {
    322         struct TALER_MERCHANTDB_InstanceAuthSettings db_ias;
    323         enum TALER_ErrorCode ec;
    324 
    325         qs = TALER_MERCHANTDB_get_instance_auth (TMH_db,
    326                                                  mi->settings.id,
    327                                                  &db_ias);
    328 
    329         switch (qs)
    330         {
    331         case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    332           /* Instance got purged. */
    333           TALER_MERCHANTDB_rollback (TMH_db);
    334           return TALER_MHD_reply_with_error (connection,
    335                                              MHD_HTTP_NOT_FOUND,
    336                                              TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
    337                                              NULL);
    338         case GNUNET_DB_STATUS_SOFT_ERROR:
    339           TALER_MERCHANTDB_rollback (TMH_db);
    340           goto retry;
    341         case GNUNET_DB_STATUS_HARD_ERROR:
    342           TALER_MERCHANTDB_rollback (TMH_db);
    343           return TALER_MHD_reply_with_error (connection,
    344                                              MHD_HTTP_INTERNAL_SERVER_ERROR,
    345                                              TALER_EC_GENERIC_DB_FETCH_FAILED,
    346                                              NULL);
    347         case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    348           /* Success! */
    349           break;
    350         }
    351 
    352         if (! auth_override)
    353         {
    354           // FIXME are we sure what the scope here is?
    355           ec = TMH_check_token (hc->auth_token,
    356                                 mi->settings.id,
    357                                 &hc->auth_scope);
    358           if (TALER_EC_NONE != ec)
    359           {
    360             TALER_MERCHANTDB_rollback (TMH_db);
    361             GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    362                         "Refusing auth change: `%s'\n",
    363                         TALER_ErrorCode_get_hint (ec));
    364             return TALER_MHD_reply_with_error (connection,
    365                                                MHD_HTTP_UNAUTHORIZED,
    366                                                TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED,
    367                                                NULL);
    368           }
    369         }
    370         if (require_old_password &&
    371             (GNUNET_OK !=
    372              TMH_check_auth (old_password,
    373                              &db_ias.auth_salt,
    374                              &db_ias.auth_hash)))
    375         {
    376           TALER_MERCHANTDB_rollback (TMH_db);
    377           return TALER_MHD_reply_with_error (
    378             connection,
    379             MHD_HTTP_UNAUTHORIZED,
    380             TALER_EC_MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_OLD_PASSWORD,
    381             NULL);
    382         }
    383       }
    384 
    385       qs = TALER_MERCHANTDB_update_instance_auth (TMH_db,
    386                                                   mi->settings.id,
    387                                                   &ias);
    388       if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    389       {
    390         GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    391         TALER_MERCHANTDB_rollback (TMH_db);
    392         if (GNUNET_DB_STATUS_HARD_ERROR == qs)
    393         {
    394           return TALER_MHD_reply_with_error (connection,
    395                                              MHD_HTTP_INTERNAL_SERVER_ERROR,
    396                                              TALER_EC_GENERIC_DB_FETCH_FAILED,
    397                                              NULL);
    398         }
    399         goto retry;
    400       }
    401       if (issue_login_token)
    402       {
    403         qs = TALER_MERCHANTDB_insert_login_token (
    404           TMH_db,
    405           mi->settings.id,
    406           &login_token,
    407           token_creation_time,
    408           token_expiration_time,
    409           TMH_AS_REFRESHABLE | TMH_AS_SPA,
    410           "login token from password reset");
    411         switch (qs)
    412         {
    413         case GNUNET_DB_STATUS_SOFT_ERROR:
    414           TALER_MERCHANTDB_rollback (TMH_db);
    415           goto retry;
    416         case GNUNET_DB_STATUS_HARD_ERROR:
    417         case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    418           GNUNET_break (0);
    419           TALER_MERCHANTDB_rollback (TMH_db);
    420           return TALER_MHD_reply_with_error (
    421             connection,
    422             MHD_HTTP_INTERNAL_SERVER_ERROR,
    423             TALER_EC_GENERIC_DB_STORE_FAILED,
    424             "insert_login_token");
    425         case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    426           break;
    427         }
    428       }
    429       qs = TALER_MERCHANTDB_commit (TMH_db);
    430       if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    431         qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
    432 retry:
    433       if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
    434         break; /* success! -- or hard failure */
    435     } /* for .. MAX_RETRIES */
    436     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    437     {
    438       return TALER_MHD_reply_with_error (connection,
    439                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    440                                          TALER_EC_GENERIC_DB_COMMIT_FAILED,
    441                                          NULL);
    442     }
    443     /* Finally, also update our running process */
    444     mi->auth = ias;
    445   }
    446   TMH_reload_instances (mi->settings.id);
    447   if (issue_login_token)
    448     return reply_with_login_token (connection,
    449                                    &login_token,
    450                                    token_expiration_time);
    451   return TALER_MHD_reply_static (connection,
    452                                  MHD_HTTP_NO_CONTENT,
    453                                  NULL,
    454                                  NULL,
    455                                  0);
    456 }
    457 
    458 
    459 enum MHD_Result
    460 TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh,
    461                                     struct MHD_Connection *connection,
    462                                     struct TMH_HandlerContext *hc)
    463 {
    464   struct TMH_MerchantInstance *mi = hc->instance;
    465 
    466   return post_instances_ID_auth (mi,
    467                                  connection,
    468                                  hc,
    469                                  false,
    470                                  true,
    471                                  (GNUNET_YES == TMH_password_change_mfa)
    472                                  ? TEH_mandatory_tan_channels
    473                                  : TMH_TCS_NONE,
    474                                  false,
    475                                  GNUNET_TIME_UNIT_ZERO,
    476                                  false);
    477 }
    478 
    479 
    480 enum MHD_Result
    481 TMH_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh,
    482                                    struct MHD_Connection *connection,
    483                                    struct TMH_HandlerContext *hc)
    484 {
    485   struct TMH_MerchantInstance *mi = hc->instance;
    486   struct GNUNET_TIME_Relative token_duration = GNUNET_TIME_UNIT_ZERO;
    487   struct GNUNET_JSON_Specification spec[] = {
    488     GNUNET_JSON_spec_mark_optional (
    489       GNUNET_JSON_spec_relative_time ("token_duration",
    490                                       &token_duration),
    491       NULL),
    492     GNUNET_JSON_spec_end ()
    493   };
    494 
    495   {
    496     enum GNUNET_GenericReturnValue res;
    497 
    498     res = TALER_MHD_parse_json_data (connection,
    499                                      hc->request_body,
    500                                      spec);
    501     if (GNUNET_OK != res)
    502       return (GNUNET_NO == res) ? MHD_YES : MHD_NO;
    503   }
    504   GNUNET_JSON_parse_free (spec);
    505 
    506   if (0 == strcmp ("admin",
    507                    mi->settings.id))
    508   {
    509     GNUNET_break_op (0);
    510     return TALER_MHD_reply_with_error (
    511       connection,
    512       MHD_HTTP_FORBIDDEN,
    513       TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
    514       "not allowed for 'admin' account");
    515   }
    516   if (TMH_TCS_NONE == TEH_mandatory_tan_channels)
    517   {
    518     /* This endpoint changes the instance password *without* requiring
    519        the current password; the only thing standing between an
    520        anonymous client and a full account takeover is the MFA
    521        challenge.  If no TAN channel is mandatory, we have no second
    522        factor to require and thus must refuse the request. */
    523     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    524                 "Refusing password reset: no mandatory TAN channel configured\n");
    525     return TALER_MHD_reply_with_error (
    526       connection,
    527       MHD_HTTP_FORBIDDEN,
    528       TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
    529       "MANDATORY_TAN_CHANNELS");
    530   }
    531   return post_instances_ID_auth (mi,
    532                                  connection,
    533                                  hc,
    534                                  false,
    535                                  false,
    536                                  TEH_mandatory_tan_channels,
    537                                  true,
    538                                  token_duration,
    539                                  true);
    540 }
    541 
    542 
    543 enum MHD_Result
    544 TMH_private_post_instances_default_ID_auth (
    545   const struct TMH_RequestHandler *rh,
    546   struct MHD_Connection *connection,
    547   struct TMH_HandlerContext *hc)
    548 {
    549   struct TMH_MerchantInstance *mi;
    550   enum MHD_Result ret;
    551 
    552   mi = TMH_lookup_instance (hc->infix);
    553   if (NULL == mi)
    554   {
    555     return TALER_MHD_reply_with_error (
    556       connection,
    557       MHD_HTTP_NOT_FOUND,
    558       TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
    559       hc->infix);
    560   }
    561   ret = post_instances_ID_auth (mi,
    562                                 connection,
    563                                 hc,
    564                                 true,
    565                                 false,
    566                                 TMH_TCS_NONE,
    567                                 false,
    568                                 GNUNET_TIME_UNIT_ZERO,
    569                                 false);
    570   return ret;
    571 }
    572 
    573 
    574 /* end of taler-merchant-httpd_post-management-instances-INSTANCE-auth.c */