challenger

OAuth 2.0-based authentication service that validates user can receive messages at a certain address
Log | Files | Refs | Submodules | README | LICENSE

commit 6e9a98b056705a7bc31321af085f92ae022a2516
parent e49e33a13df92c6a1d6f119775baa31778163531
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun, 30 Mar 2025 14:14:45 +0200

allow sending of custom messages for address validation

Diffstat:
Mconfigure.ac | 12++++++++++++
Msrc/challenger/Makefile.am | 5+++++
Msrc/challenger/challenger-httpd.c | 130++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/challenger/challenger-httpd.h | 16++++++++++++++++
Msrc/challenger/challenger-httpd_challenge.c | 59++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/challenger/challenger.conf | 8+++++++-
6 files changed, 223 insertions(+), 7 deletions(-)

diff --git a/configure.ac b/configure.ac @@ -132,6 +132,18 @@ AS_IF([test $libtalermhd != 1], *** https://taler.net *** ]])]) +libtalertemplating=0 +AC_CHECK_HEADERS([taler/taler_templating_lib.h], + [AC_CHECK_LIB([talertemplating], [TALER_TEMPLATING_fill2], libtalertemplating=1)]) +AM_CONDITIONAL(HAVE_TALERTEMPLATING, test x$libtalertemplating = x1) +AS_IF([test $libtalertemplating != 1], + [AC_MSG_ERROR([[ +*** +*** You need libtalertemplating >= 0.14.6 to build this program. +*** This library is part of the GNU Taler exchange, available at +*** https://taler.net +*** ]])]) + # check for libmicrohttpd microhttpd=0 diff --git a/src/challenger/Makefile.am b/src/challenger/Makefile.am @@ -6,6 +6,11 @@ if USE_COVERAGE XLIB = -lgcov endif +tmpldatadir = $(prefix)/share/challenger/templates/ + +dist_tmpldata_DATA = \ + default-challenge-message.txt + pkgcfgdir = $(prefix)/share/challenger/config.d/ pkgcfg_DATA = \ diff --git a/src/challenger/challenger-httpd.c b/src/challenger/challenger-httpd.c @@ -1,6 +1,6 @@ /* This file is part of Challenger - (C) 2023, 2024 Taler Systems SA + (C) 2023, 2024, 2025 Taler Systems SA Challenger is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -79,6 +79,22 @@ static struct MHD_Daemon *mhd; struct CHALLENGER_DatabasePlugin *CH_db; /** + * (external) base URL of this service. + */ +char *CH_base_url; + +/** + * Mustach template for the letter to send. + * WARNING: not 0-terminated! Allocated via mmap(), free with munmap()! + */ +void *CH_message_template; + +/** + * Number of bytes in #CH_message_template + */ +size_t CH_message_template_len; + +/** * How long is an individual validation request valid? */ struct GNUNET_TIME_Relative CH_validation_duration; @@ -361,6 +377,17 @@ do_shutdown (void *cls) GNUNET_SCHEDULER_cancel (mhd_task); mhd_task = NULL; } + if ( (NULL != CH_message_template) && + (MAP_FAILED != CH_message_template) ) + { + if (0 != + munmap (CH_message_template, + CH_message_template_len)) + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "munmap"); + CH_message_template = NULL; + CH_message_template_len = 0; + } if (NULL != CH_ctx) { GNUNET_CURL_fini (CH_ctx); @@ -588,6 +615,8 @@ run (void *cls, GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "CHALLENGER", "VALIDATION_DURATION"); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_OK != @@ -599,6 +628,8 @@ run (void *cls, GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "CHALLENGER", "VALIDATION_EXPIRATION"); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_OK != @@ -610,6 +641,8 @@ run (void *cls, GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "CHALLENGER", "AUTH_COMMAND"); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); return; } if (GNUNET_OK != @@ -621,8 +654,101 @@ run (void *cls, GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "CHALLENGER", "ADDRESS_TYPE"); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "CHALLENGER", + "BASE_URL", + &CH_base_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "CHALLENGER", + "BASE_URL"); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); return; } + if ( (! TALER_url_valid_charset (CH_base_url)) || + (! TALER_is_web_url (CH_base_url) ) || + (0 == strlen (CH_base_url)) || + ('/' != CH_base_url[strlen (CH_base_url) - 1]) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "CHALLENGER", + "BASE_URL", + "invalid URL"); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } + { + char *tmpl_file; + int fd; + struct stat s; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_filename (config, + "CHALLENGER", + "MESSAGE_TEMPLATE_FILE", + &tmpl_file)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING, + "CHALLENGER", + "MESSAGE_TEMPLATE_FILE"); + } + else + { + fd = open (tmpl_file, + O_RDONLY); + GNUNET_free (tmpl_file); + if (-1 == fd) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "CHALLENGER", + "MESSAGE_TEMPLATE_FILE", + strerror (errno)); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } + if (0 != fstat (fd, + &s)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "CHALLENGER", + "MESSAGE_TEMPLATE_FILE", + strerror (errno)); + GNUNET_free (tmpl_file); + GNUNET_break (0 == close (fd)); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } + CH_message_template_len = (size_t) s.st_size; + CH_message_template = mmap (NULL, + CH_message_template_len, + PROT_READ, + MAP_SHARED, + fd, + 0); + GNUNET_break (0 == close (fd)); + if (MAP_FAILED == CH_message_template) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "CHALLENGER", + "MESSAGE_TEMPLATE_FILE", + strerror (errno)); + GNUNET_free (tmpl_file); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); + return; + } + } + } + { char *restrictions; @@ -644,6 +770,8 @@ run (void *cls, "CHALLENGER", "ADDRESS_RESTRICTIONS", err.text); + global_ret = EXIT_NOTCONFIGURED; + GNUNET_SCHEDULER_shutdown (); return; } } diff --git a/src/challenger/challenger-httpd.h b/src/challenger/challenger-httpd.h @@ -147,6 +147,22 @@ extern char *CH_auth_command; extern char *CH_address_type; /** + * (external) base URL of this service. + */ +extern char *CH_base_url; + +/** + * Mustach template for the letter to send. + * WARNING: not 0-terminated! Allocated via mmap(), free with munmap()! + */ +extern void *CH_message_template; + +/** + * Number of bytes in #CH_message_template + */ +extern size_t CH_message_template_len; + +/** * How long is an individual validation request valid? */ extern struct GNUNET_TIME_Relative CH_validation_duration; diff --git a/src/challenger/challenger-httpd_challenge.c b/src/challenger/challenger-httpd_challenge.c @@ -293,7 +293,8 @@ send_tan (struct ChallengeContext *bc) { struct GNUNET_DISK_PipeHandle *p; struct GNUNET_DISK_FileHandle *pipe_stdin; - char *msg; + void *msg; + size_t msg_len; p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); if (NULL == p) @@ -352,13 +353,60 @@ send_tan (struct ChallengeContext *bc) GNUNET_assert (NULL != pipe_stdin); GNUNET_break (GNUNET_OK == GNUNET_DISK_pipe_close (p)); + if (0 != CH_message_template_len) + { + json_t *root; + int mret; + char *my_url; + + GNUNET_asprintf (&my_url, + "%schallenge/%s", + CH_base_url, + bc->hc->path); + root = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("challenger_url", + my_url), + GNUNET_JSON_pack_object_steal ("address", + bc->address), + GNUNET_JSON_pack_int64 ("pin", + bc->tan)); + GNUNET_free (my_url); + mret = TALER_TEMPLATING_fill2 (CH_message_template, + CH_message_template_len, + root, + &msg, + &msg_len); + json_decref (root); + if (0 != mret) + { + MHD_RESULT mres; - GNUNET_asprintf (&msg, - "PIN: %u", - (unsigned int) bc->tan); + GNUNET_break (0); + mres = reply_error (bc, + "internal-error", + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_TO_EXPAND_TEMPLATE, + NULL); + GNUNET_DISK_file_close (pipe_stdin); + bc->status = (MHD_YES == mres) + ? GNUNET_NO + : GNUNET_SYSERR; + return; + } + } + else + { + char *cmsg; + + GNUNET_asprintf (&cmsg, + "PIN: %u", + (unsigned int) bc->tan); + msg_len = strlen (msg); + msg = cmsg; + } { const char *off = msg; - size_t left = strlen (off); + size_t left = msg_len; while (0 != left) { @@ -377,6 +425,7 @@ send_tan (struct ChallengeContext *bc) MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_CHALLENGER_HELPER_EXEC_FAILED, "write"); + GNUNET_DISK_file_close (pipe_stdin); GNUNET_free (msg); bc->status = (MHD_YES == mres) ? GNUNET_NO diff --git a/src/challenger/challenger.conf b/src/challenger/challenger.conf @@ -30,6 +30,12 @@ VALIDATION_DURATION = 1d # How long is an validation valid? VALIDATION_EXPIRATION = 365d +# Base URL of our service. Must end with '/'. +#BASE_URL = https://challenger.DOMAIN/ + +# Name of a file with the message to send with the challenge. +MESSAGE_TEMPLATE_FILE = ${DATADIR}templates/default-challenge-message.txt + # Which external command should be used to transmit challenges? # Example commands are challenger-send-{sms,email,post}.sh # AUTH_COMMAND = @@ -39,5 +45,5 @@ VALIDATION_EXPIRATION = 365d # A template of the form 'enter-$ADDRESS_TYPE-form' must # exist and the field names must be supported by the # AUTH_COMMAND. -# +# # ADDRESS_TYPE =