merchant

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

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


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