merchant

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

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


      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 login_token_duration how long a login token returned after the
     99  *   update should remain valid; zero means do not create a token
    100  * @return MHD result code
    101  */
    102 static enum MHD_Result
    103 post_instances_ID_auth (struct TMH_MerchantInstance *mi,
    104                         struct MHD_Connection *connection,
    105                         struct TMH_HandlerContext *hc,
    106                         bool auth_override,
    107                         bool require_old_password,
    108                         enum TEH_TanChannelSet tcs,
    109                         struct GNUNET_TIME_Relative login_token_duration)
    110 {
    111   struct TALER_MERCHANTDB_InstanceAuthSettings ias;
    112   struct TALER_MERCHANTDB_LoginTokenP login_token;
    113   struct GNUNET_TIME_Timestamp token_creation_time;
    114   struct GNUNET_TIME_Timestamp token_expiration_time;
    115   const char *auth_pw = NULL;
    116   const char *old_password = NULL;
    117   json_t *jauth = hc->request_body;
    118   bool issue_login_token
    119     = ! GNUNET_TIME_relative_is_zero (login_token_duration);
    120 
    121   if (issue_login_token)
    122   {
    123     GNUNET_CRYPTO_random_block (&login_token,
    124                                 sizeof (login_token));
    125     token_creation_time = GNUNET_TIME_timestamp_get ();
    126     token_expiration_time
    127       = GNUNET_TIME_relative_to_timestamp (login_token_duration);
    128   }
    129 
    130   if (require_old_password)
    131   {
    132     json_t *jold_password = json_object_get (jauth,
    133                                              "old_password");
    134 
    135     if (NULL != jold_password)
    136     {
    137       old_password = json_string_value (jold_password);
    138       if (NULL == old_password)
    139       {
    140         GNUNET_break_op (0);
    141         return TALER_MHD_reply_with_error (
    142           connection,
    143           MHD_HTTP_BAD_REQUEST,
    144           TALER_EC_MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH,
    145           "old_password must be a string");
    146       }
    147     }
    148   }
    149 
    150   {
    151     enum GNUNET_GenericReturnValue ret;
    152 
    153     ret = TMH_check_auth_config (connection,
    154                                  jauth,
    155                                  &auth_pw);
    156     if (GNUNET_OK != ret)
    157       return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
    158   }
    159 
    160   if ( (0 != (tcs & TMH_TCS_SMS) &&
    161         ( (NULL == mi->settings.phone) ||
    162           (NULL == TMH_helper_sms) ||
    163           (! mi->settings.phone_validated) ) ) )
    164   {
    165     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    166                 "Cannot reset password: SMS factor not available\n");
    167     return TALER_MHD_reply_with_error (
    168       connection,
    169       MHD_HTTP_FORBIDDEN,
    170       TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
    171       "phone_number");
    172   }
    173   if ( (0 != (tcs & TMH_TCS_EMAIL) &&
    174         ( (NULL == mi->settings.email) ||
    175           (NULL == TMH_helper_email) ||
    176           (! mi->settings.email_validated) ) ) )
    177   {
    178     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    179                 "Cannot reset password: E-mail factor not available\n");
    180     return TALER_MHD_reply_with_error (
    181       connection,
    182       MHD_HTTP_FORBIDDEN,
    183       TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
    184       "email");
    185   }
    186   if (! auth_override)
    187   {
    188     enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; // fix -Wmaybe-uninitialized
    189 
    190     switch (tcs)
    191     {
    192     case TMH_TCS_NONE:
    193       ret = GNUNET_OK;
    194       break;
    195     case TMH_TCS_SMS:
    196       ret = TMH_mfa_challenges_do (hc,
    197                                    mi->settings.id,
    198                                    TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION,
    199                                    true,
    200                                    TALER_MERCHANT_MFA_CHANNEL_SMS,
    201                                    mi->settings.phone,
    202                                    TALER_MERCHANT_MFA_CHANNEL_NONE);
    203       break;
    204     case TMH_TCS_EMAIL:
    205       ret = TMH_mfa_challenges_do (hc,
    206                                    mi->settings.id,
    207                                    TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION,
    208                                    true,
    209                                    TALER_MERCHANT_MFA_CHANNEL_EMAIL,
    210                                    mi->settings.email,
    211                                    TALER_MERCHANT_MFA_CHANNEL_NONE);
    212       break;
    213     case TMH_TCS_EMAIL_AND_SMS:
    214       ret = TMH_mfa_challenges_do (hc,
    215                                    mi->settings.id,
    216                                    TALER_MERCHANT_MFA_CO_AUTH_CONFIGURATION,
    217                                    true,
    218                                    TALER_MERCHANT_MFA_CHANNEL_EMAIL,
    219                                    mi->settings.email,
    220                                    TALER_MERCHANT_MFA_CHANNEL_SMS,
    221                                    mi->settings.phone,
    222                                    TALER_MERCHANT_MFA_CHANNEL_NONE);
    223       break;
    224     }
    225     if (GNUNET_OK != ret)
    226     {
    227       return (GNUNET_NO == ret)
    228         ? MHD_YES
    229         : MHD_NO;
    230     }
    231   }
    232 
    233   if (NULL == auth_pw)
    234   {
    235     memset (&ias.auth_salt,
    236             0,
    237             sizeof (ias.auth_salt));
    238     memset (&ias.auth_hash,
    239             0,
    240             sizeof (ias.auth_hash));
    241   }
    242   else
    243   {
    244     TMH_compute_auth (auth_pw,
    245                       &ias.auth_salt,
    246                       &ias.auth_hash);
    247   }
    248 
    249   /* Store the new auth information in the database */
    250   {
    251     enum GNUNET_DB_QueryStatus qs;
    252 
    253     for (unsigned int i = 0; i<MAX_RETRIES; i++)
    254     {
    255       if (GNUNET_OK !=
    256           TALER_MERCHANTDB_start (TMH_db,
    257                                   "post /instances/$ID/auth"))
    258       {
    259         return TALER_MHD_reply_with_error (connection,
    260                                            MHD_HTTP_INTERNAL_SERVER_ERROR,
    261                                            TALER_EC_GENERIC_DB_START_FAILED,
    262                                            NULL);
    263       }
    264 
    265       /* Make the authentication update a serializable operation.
    266          We first check that the authentication information
    267          that the caller's request authenticated with
    268          is still up to date.
    269          Otherwise, we've detected a conflicting update
    270          to the authentication. */
    271       {
    272         struct TALER_MERCHANTDB_InstanceAuthSettings db_ias;
    273         enum TALER_ErrorCode ec;
    274 
    275         qs = TALER_MERCHANTDB_get_instance_auth (TMH_db,
    276                                                  mi->settings.id,
    277                                                  &db_ias);
    278 
    279         switch (qs)
    280         {
    281         case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    282           /* Instance got purged. */
    283           TALER_MERCHANTDB_rollback (TMH_db);
    284           return TALER_MHD_reply_with_error (connection,
    285                                              MHD_HTTP_NOT_FOUND,
    286                                              TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
    287                                              NULL);
    288         case GNUNET_DB_STATUS_SOFT_ERROR:
    289           TALER_MERCHANTDB_rollback (TMH_db);
    290           goto retry;
    291         case GNUNET_DB_STATUS_HARD_ERROR:
    292           TALER_MERCHANTDB_rollback (TMH_db);
    293           return TALER_MHD_reply_with_error (connection,
    294                                              MHD_HTTP_INTERNAL_SERVER_ERROR,
    295                                              TALER_EC_GENERIC_DB_FETCH_FAILED,
    296                                              NULL);
    297         case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    298           /* Success! */
    299           break;
    300         }
    301 
    302         if (! auth_override)
    303         {
    304           // FIXME are we sure what the scope here is?
    305           ec = TMH_check_token (hc->auth_token,
    306                                 mi->settings.id,
    307                                 &hc->auth_scope);
    308           if (TALER_EC_NONE != ec)
    309           {
    310             TALER_MERCHANTDB_rollback (TMH_db);
    311             GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    312                         "Refusing auth change: `%s'\n",
    313                         TALER_ErrorCode_get_hint (ec));
    314             return TALER_MHD_reply_with_error (connection,
    315                                                MHD_HTTP_UNAUTHORIZED,
    316                                                TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED,
    317                                                NULL);
    318           }
    319         }
    320         if (require_old_password &&
    321             (GNUNET_OK !=
    322              TMH_check_auth (old_password,
    323                              &db_ias.auth_salt,
    324                              &db_ias.auth_hash)))
    325         {
    326           TALER_MERCHANTDB_rollback (TMH_db);
    327           return TALER_MHD_reply_with_error (
    328             connection,
    329             MHD_HTTP_UNAUTHORIZED,
    330             TALER_EC_MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_OLD_PASSWORD,
    331             NULL);
    332         }
    333       }
    334 
    335       qs = TALER_MERCHANTDB_update_instance_auth (TMH_db,
    336                                                   mi->settings.id,
    337                                                   &ias);
    338       if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    339       {
    340         GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    341         TALER_MERCHANTDB_rollback (TMH_db);
    342         if (GNUNET_DB_STATUS_HARD_ERROR == qs)
    343         {
    344           return TALER_MHD_reply_with_error (connection,
    345                                              MHD_HTTP_INTERNAL_SERVER_ERROR,
    346                                              TALER_EC_GENERIC_DB_FETCH_FAILED,
    347                                              NULL);
    348         }
    349         goto retry;
    350       }
    351       if (issue_login_token)
    352       {
    353         qs = TALER_MERCHANTDB_insert_login_token (
    354           TMH_db,
    355           mi->settings.id,
    356           &login_token,
    357           token_creation_time,
    358           token_expiration_time,
    359           TMH_AS_REFRESHABLE | TMH_AS_SPA,
    360           "login token from password reset");
    361         switch (qs)
    362         {
    363         case GNUNET_DB_STATUS_SOFT_ERROR:
    364           TALER_MERCHANTDB_rollback (TMH_db);
    365           goto retry;
    366         case GNUNET_DB_STATUS_HARD_ERROR:
    367         case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    368           GNUNET_break (0);
    369           TALER_MERCHANTDB_rollback (TMH_db);
    370           return TALER_MHD_reply_with_error (
    371             connection,
    372             MHD_HTTP_INTERNAL_SERVER_ERROR,
    373             TALER_EC_GENERIC_DB_STORE_FAILED,
    374             "insert_login_token");
    375         case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    376           break;
    377         }
    378       }
    379       qs = TALER_MERCHANTDB_commit (TMH_db);
    380       if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    381         qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
    382 retry:
    383       if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
    384         break; /* success! -- or hard failure */
    385     } /* for .. MAX_RETRIES */
    386     if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    387     {
    388       return TALER_MHD_reply_with_error (connection,
    389                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
    390                                          TALER_EC_GENERIC_DB_COMMIT_FAILED,
    391                                          NULL);
    392     }
    393     /* Finally, also update our running process */
    394     mi->auth = ias;
    395   }
    396   TMH_reload_instances (mi->settings.id);
    397   if (issue_login_token)
    398     return reply_with_login_token (connection,
    399                                    &login_token,
    400                                    token_expiration_time);
    401   return TALER_MHD_reply_static (connection,
    402                                  MHD_HTTP_NO_CONTENT,
    403                                  NULL,
    404                                  NULL,
    405                                  0);
    406 }
    407 
    408 
    409 enum MHD_Result
    410 TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh,
    411                                     struct MHD_Connection *connection,
    412                                     struct TMH_HandlerContext *hc)
    413 {
    414   struct TMH_MerchantInstance *mi = hc->instance;
    415 
    416   return post_instances_ID_auth (mi,
    417                                  connection,
    418                                  hc,
    419                                  false,
    420                                  true,
    421                                  TMH_TCS_NONE,
    422                                  GNUNET_TIME_UNIT_ZERO);
    423 }
    424 
    425 
    426 enum MHD_Result
    427 TMH_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh,
    428                                    struct MHD_Connection *connection,
    429                                    struct TMH_HandlerContext *hc)
    430 {
    431   struct TMH_MerchantInstance *mi = hc->instance;
    432   struct GNUNET_TIME_Relative token_duration = GNUNET_TIME_UNIT_ZERO;
    433   struct GNUNET_JSON_Specification spec[] = {
    434     GNUNET_JSON_spec_mark_optional (
    435       GNUNET_JSON_spec_relative_time ("token_duration",
    436                                       &token_duration),
    437       NULL),
    438     GNUNET_JSON_spec_end ()
    439   };
    440 
    441   {
    442     enum GNUNET_GenericReturnValue res;
    443 
    444     res = TALER_MHD_parse_json_data (connection,
    445                                      hc->request_body,
    446                                      spec);
    447     if (GNUNET_OK != res)
    448       return (GNUNET_NO == res) ? MHD_YES : MHD_NO;
    449   }
    450   GNUNET_JSON_parse_free (spec);
    451 
    452   if (0 == strcmp ("admin",
    453                    mi->settings.id))
    454   {
    455     GNUNET_break_op (0);
    456     return TALER_MHD_reply_with_error (
    457       connection,
    458       MHD_HTTP_FORBIDDEN,
    459       TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
    460       "not allowed for 'admin' account");
    461   }
    462   if (TMH_TCS_NONE == TEH_mandatory_tan_channels)
    463   {
    464     /* This endpoint changes the instance password *without* requiring
    465        the current password; the only thing standing between an
    466        anonymous client and a full account takeover is the MFA
    467        challenge.  If no TAN channel is mandatory, we have no second
    468        factor to require and thus must refuse the request. */
    469     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    470                 "Refusing password reset: no mandatory TAN channel configured\n");
    471     return TALER_MHD_reply_with_error (
    472       connection,
    473       MHD_HTTP_FORBIDDEN,
    474       TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
    475       "MANDATORY_TAN_CHANNELS");
    476   }
    477   return post_instances_ID_auth (mi,
    478                                  connection,
    479                                  hc,
    480                                  false,
    481                                  false,
    482                                  TEH_mandatory_tan_channels,
    483                                  token_duration);
    484 }
    485 
    486 
    487 enum MHD_Result
    488 TMH_private_post_instances_default_ID_auth (
    489   const struct TMH_RequestHandler *rh,
    490   struct MHD_Connection *connection,
    491   struct TMH_HandlerContext *hc)
    492 {
    493   struct TMH_MerchantInstance *mi;
    494   enum MHD_Result ret;
    495 
    496   mi = TMH_lookup_instance (hc->infix);
    497   if (NULL == mi)
    498   {
    499     return TALER_MHD_reply_with_error (
    500       connection,
    501       MHD_HTTP_NOT_FOUND,
    502       TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
    503       hc->infix);
    504   }
    505   ret = post_instances_ID_auth (mi,
    506                                 connection,
    507                                 hc,
    508                                 true,
    509                                 false,
    510                                 TMH_TCS_NONE,
    511                                 GNUNET_TIME_UNIT_ZERO);
    512   return ret;
    513 }
    514 
    515 
    516 /* end of taler-merchant-httpd_post-management-instances-INSTANCE-auth.c */