Line data Source code
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 1 : 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 1 : token_data = GNUNET_STRINGS_data_to_string_alloc (token,
63 : sizeof (*token));
64 1 : GNUNET_asprintf (&access_token,
65 : RFC_8959_PREFIX "%s",
66 : token_data);
67 1 : GNUNET_free (token_data);
68 1 : 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 1 : GNUNET_free (access_token);
82 1 : 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 19 : 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 19 : const char *auth_pw = NULL;
118 19 : const char *old_password = NULL;
119 19 : json_t *jauth = hc->request_body;
120 19 : bool issue_login_token
121 19 : = ! GNUNET_TIME_relative_is_zero (login_token_duration);
122 :
123 19 : if (issue_login_token)
124 : {
125 3 : GNUNET_CRYPTO_random_block (&login_token,
126 : sizeof (login_token));
127 3 : token_creation_time = GNUNET_TIME_timestamp_get ();
128 : token_expiration_time
129 3 : = GNUNET_TIME_relative_to_timestamp (login_token_duration);
130 : }
131 :
132 19 : if (require_old_password)
133 : {
134 10 : json_t *jold_password = json_object_get (jauth,
135 : "old_password");
136 :
137 10 : if (NULL != jold_password)
138 : {
139 9 : old_password = json_string_value (jold_password);
140 9 : if (NULL == old_password)
141 : {
142 0 : GNUNET_break_op (0);
143 0 : 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 19 : ret = TMH_check_auth_config (connection,
156 : jauth,
157 : &auth_pw);
158 19 : if (GNUNET_OK != ret)
159 0 : return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
160 : }
161 :
162 : {
163 19 : enum TEH_TanChannelSet available_tcs = TMH_TCS_NONE;
164 46 : bool have_sms = (NULL != mi->settings.phone) &&
165 27 : (NULL != TMH_helper_sms) &&
166 8 : mi->settings.phone_validated;
167 46 : bool have_email = (NULL != mi->settings.email) &&
168 27 : (NULL != TMH_helper_email) &&
169 8 : mi->settings.email_validated;
170 :
171 19 : if (have_sms &&
172 8 : (0 != (tcs & TMH_TCS_SMS)))
173 8 : available_tcs |= TMH_TCS_SMS;
174 19 : if (have_email &&
175 8 : (0 != (tcs & TMH_TCS_EMAIL)))
176 8 : available_tcs |= TMH_TCS_EMAIL;
177 :
178 19 : if (mfa_combi_and &&
179 6 : (0 != (tcs & TMH_TCS_SMS)) &&
180 6 : (! have_sms))
181 : {
182 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
183 : "Cannot change authentication: SMS factor not available\n");
184 0 : return TALER_MHD_reply_with_error (
185 : connection,
186 : MHD_HTTP_FORBIDDEN,
187 : TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
188 : "phone_number");
189 : }
190 19 : if (mfa_combi_and &&
191 6 : (0 != (tcs & TMH_TCS_EMAIL)) &&
192 6 : (! have_email))
193 : {
194 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
195 : "Cannot change authentication: E-mail factor not available\n");
196 0 : return TALER_MHD_reply_with_error (
197 : connection,
198 : MHD_HTTP_FORBIDDEN,
199 : TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
200 : "email");
201 : }
202 19 : if ( (TMH_TCS_NONE != tcs) &&
203 : (TMH_TCS_NONE == available_tcs) )
204 : {
205 : const char *missing_factor;
206 :
207 0 : switch (tcs)
208 : {
209 0 : case TMH_TCS_SMS:
210 0 : missing_factor = "phone_number";
211 0 : break;
212 0 : case TMH_TCS_EMAIL:
213 0 : missing_factor = "email";
214 0 : break;
215 0 : case TMH_TCS_EMAIL_AND_SMS:
216 0 : missing_factor = "phone_number or email";
217 0 : break;
218 0 : case TMH_TCS_NONE:
219 0 : GNUNET_assert (0);
220 : missing_factor = NULL;
221 : break;
222 : }
223 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
224 : "Cannot change authentication: no MFA factor available\n");
225 0 : return TALER_MHD_reply_with_error (
226 : connection,
227 : MHD_HTTP_FORBIDDEN,
228 : TALER_EC_MERCHANT_GENERIC_MFA_MISSING,
229 : missing_factor);
230 : }
231 19 : tcs = available_tcs;
232 : }
233 19 : if (! auth_override)
234 : {
235 16 : enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR; // fix -Wmaybe-uninitialized
236 :
237 16 : switch (tcs)
238 : {
239 8 : case TMH_TCS_NONE:
240 8 : ret = GNUNET_OK;
241 8 : break;
242 0 : case TMH_TCS_SMS:
243 0 : ret = TMH_mfa_challenges_do (hc,
244 0 : 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 0 : break;
251 0 : case TMH_TCS_EMAIL:
252 0 : ret = TMH_mfa_challenges_do (hc,
253 0 : 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 0 : break;
260 8 : case TMH_TCS_EMAIL_AND_SMS:
261 8 : ret = TMH_mfa_challenges_do (hc,
262 8 : 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 8 : break;
271 : }
272 16 : if (GNUNET_OK != ret)
273 : {
274 : return (GNUNET_NO == ret)
275 : ? MHD_YES
276 5 : : MHD_NO;
277 : }
278 : }
279 :
280 14 : if (NULL == auth_pw)
281 : {
282 2 : memset (&ias.auth_salt,
283 : 0,
284 : sizeof (ias.auth_salt));
285 2 : memset (&ias.auth_hash,
286 : 0,
287 : sizeof (ias.auth_hash));
288 : }
289 : else
290 : {
291 12 : 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 14 : for (unsigned int i = 0; i<MAX_RETRIES; i++)
301 : {
302 14 : if (GNUNET_OK !=
303 14 : TALER_MERCHANTDB_start (TMH_db,
304 : "post /instances/$ID/auth"))
305 : {
306 0 : 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 14 : qs = TALER_MERCHANTDB_get_instance_auth (TMH_db,
323 14 : mi->settings.id,
324 : &db_ias);
325 :
326 14 : switch (qs)
327 : {
328 0 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
329 : /* Instance got purged. */
330 0 : TALER_MERCHANTDB_rollback (TMH_db);
331 2 : return TALER_MHD_reply_with_error (connection,
332 : MHD_HTTP_NOT_FOUND,
333 : TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
334 : NULL);
335 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
336 0 : TALER_MERCHANTDB_rollback (TMH_db);
337 0 : goto retry;
338 0 : case GNUNET_DB_STATUS_HARD_ERROR:
339 0 : TALER_MERCHANTDB_rollback (TMH_db);
340 0 : return TALER_MHD_reply_with_error (connection,
341 : MHD_HTTP_INTERNAL_SERVER_ERROR,
342 : TALER_EC_GENERIC_DB_FETCH_FAILED,
343 : NULL);
344 14 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
345 : /* Success! */
346 14 : break;
347 : }
348 :
349 14 : if (! auth_override)
350 : {
351 : // FIXME are we sure what the scope here is?
352 11 : ec = TMH_check_token (hc->auth_token,
353 11 : mi->settings.id,
354 : &hc->auth_scope);
355 11 : if (TALER_EC_NONE != ec)
356 : {
357 0 : TALER_MERCHANTDB_rollback (TMH_db);
358 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
359 : "Refusing auth change: `%s'\n",
360 : TALER_ErrorCode_get_hint (ec));
361 0 : return TALER_MHD_reply_with_error (connection,
362 : MHD_HTTP_UNAUTHORIZED,
363 : TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED,
364 : NULL);
365 : }
366 : }
367 23 : if (require_old_password &&
368 : (GNUNET_OK !=
369 9 : TMH_check_auth (old_password,
370 : &db_ias.auth_salt,
371 : &db_ias.auth_hash)))
372 : {
373 2 : TALER_MERCHANTDB_rollback (TMH_db);
374 2 : 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 12 : qs = TALER_MERCHANTDB_update_instance_auth (TMH_db,
383 12 : mi->settings.id,
384 : &ias);
385 12 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
386 : {
387 0 : GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
388 0 : TALER_MERCHANTDB_rollback (TMH_db);
389 0 : if (GNUNET_DB_STATUS_HARD_ERROR == qs)
390 : {
391 0 : return TALER_MHD_reply_with_error (connection,
392 : MHD_HTTP_INTERNAL_SERVER_ERROR,
393 : TALER_EC_GENERIC_DB_FETCH_FAILED,
394 : NULL);
395 : }
396 0 : goto retry;
397 : }
398 12 : if (issue_login_token)
399 : {
400 1 : qs = TALER_MERCHANTDB_insert_login_token (
401 : TMH_db,
402 1 : 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 1 : switch (qs)
409 : {
410 0 : case GNUNET_DB_STATUS_SOFT_ERROR:
411 0 : TALER_MERCHANTDB_rollback (TMH_db);
412 0 : goto retry;
413 0 : case GNUNET_DB_STATUS_HARD_ERROR:
414 : case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
415 0 : GNUNET_break (0);
416 0 : TALER_MERCHANTDB_rollback (TMH_db);
417 0 : 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 1 : case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
423 1 : break;
424 : }
425 : }
426 12 : qs = TALER_MERCHANTDB_commit (TMH_db);
427 12 : if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
428 12 : qs = GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
429 0 : retry:
430 12 : if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
431 12 : break; /* success! -- or hard failure */
432 : } /* for .. MAX_RETRIES */
433 12 : if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
434 : {
435 0 : 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 12 : mi->auth = ias;
442 : }
443 12 : TMH_reload_instances (mi->settings.id);
444 12 : if (issue_login_token)
445 1 : return reply_with_login_token (connection,
446 : &login_token,
447 : token_expiration_time);
448 11 : 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 10 : TMH_private_post_instances_ID_auth (const struct TMH_RequestHandler *rh,
458 : struct MHD_Connection *connection,
459 : struct TMH_HandlerContext *hc)
460 : {
461 10 : struct TMH_MerchantInstance *mi = hc->instance;
462 :
463 10 : return post_instances_ID_auth (mi,
464 : connection,
465 : hc,
466 : false,
467 : true,
468 10 : (GNUNET_YES == TMH_password_change_mfa)
469 : ? TEH_mandatory_tan_channels
470 : : TMH_TCS_NONE,
471 : false,
472 10 : GNUNET_TIME_UNIT_ZERO);
473 : }
474 :
475 :
476 : enum MHD_Result
477 6 : TMH_public_post_instances_ID_auth (const struct TMH_RequestHandler *rh,
478 : struct MHD_Connection *connection,
479 : struct TMH_HandlerContext *hc)
480 : {
481 6 : struct TMH_MerchantInstance *mi = hc->instance;
482 6 : struct GNUNET_TIME_Relative token_duration = GNUNET_TIME_UNIT_ZERO;
483 : struct GNUNET_JSON_Specification spec[] = {
484 6 : GNUNET_JSON_spec_mark_optional (
485 : GNUNET_JSON_spec_relative_time ("token_duration",
486 : &token_duration),
487 : NULL),
488 6 : GNUNET_JSON_spec_end ()
489 : };
490 :
491 : {
492 : enum GNUNET_GenericReturnValue res;
493 :
494 6 : res = TALER_MHD_parse_json_data (connection,
495 6 : hc->request_body,
496 : spec);
497 6 : if (GNUNET_OK != res)
498 0 : return (GNUNET_NO == res) ? MHD_YES : MHD_NO;
499 : }
500 6 : GNUNET_JSON_parse_free (spec);
501 :
502 6 : if (0 == strcmp ("admin",
503 6 : mi->settings.id))
504 : {
505 0 : GNUNET_break_op (0);
506 0 : 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 6 : 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 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
520 : "Refusing password reset: no mandatory TAN channel configured\n");
521 0 : 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 6 : 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 3 : 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 3 : mi = TMH_lookup_instance (hc->infix);
548 3 : if (NULL == mi)
549 : {
550 0 : return TALER_MHD_reply_with_error (
551 : connection,
552 : MHD_HTTP_NOT_FOUND,
553 : TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
554 0 : hc->infix);
555 : }
556 3 : ret = post_instances_ID_auth (mi,
557 : connection,
558 : hc,
559 : true,
560 : false,
561 : TMH_TCS_NONE,
562 : false,
563 3 : GNUNET_TIME_UNIT_ZERO);
564 3 : return ret;
565 : }
566 :
567 :
568 : /* end of taler-merchant-httpd_post-management-instances-INSTANCE-auth.c */
|