diff options
Diffstat (limited to 'src/kyclogic')
-rw-r--r-- | src/kyclogic/Makefile.am | 16 | ||||
-rw-r--r-- | src/kyclogic/kyclogic-kycaid.conf | 6 | ||||
-rw-r--r-- | src/kyclogic/kyclogic-oauth2.conf | 14 | ||||
-rw-r--r-- | src/kyclogic/kyclogic-persona.conf | 23 | ||||
-rw-r--r-- | src/kyclogic/kyclogic_api.c | 230 | ||||
-rw-r--r-- | src/kyclogic/plugin_kyclogic_kycaid.c | 366 | ||||
-rw-r--r-- | src/kyclogic/plugin_kyclogic_oauth2.c | 997 | ||||
-rw-r--r-- | src/kyclogic/plugin_kyclogic_persona.c | 734 | ||||
-rw-r--r-- | src/kyclogic/plugin_kyclogic_template.c | 3 | ||||
-rwxr-xr-x | src/kyclogic/taler-exchange-kyc-kycaid-converter.sh | 90 | ||||
-rwxr-xr-x | src/kyclogic/taler-exchange-kyc-oauth2-challenger.sh | 27 | ||||
-rwxr-xr-x | src/kyclogic/taler-exchange-kyc-oauth2-nda.sh | 30 | ||||
-rwxr-xr-x | src/kyclogic/taler-exchange-kyc-oauth2-test-converter.sh | 31 | ||||
-rwxr-xr-x | src/kyclogic/taler-exchange-kyc-persona-converter.sh | 57 | ||||
-rw-r--r-- | src/kyclogic/taler-exchange-kyc-tester.c | 168 |
15 files changed, 2052 insertions, 740 deletions
diff --git a/src/kyclogic/Makefile.am b/src/kyclogic/Makefile.am index a88c3c49b..0281553fc 100644 --- a/src/kyclogic/Makefile.am +++ b/src/kyclogic/Makefile.am @@ -14,8 +14,16 @@ pkgcfg_DATA = \ kyclogic-oauth2.conf \ kyclogic-persona.conf +bin_SCRIPTS = \ + taler-exchange-kyc-kycaid-converter.sh \ + taler-exchange-kyc-persona-converter.sh \ + taler-exchange-kyc-oauth2-test-converter.sh \ + taler-exchange-kyc-oauth2-challenger.sh \ + taler-exchange-kyc-oauth2-nda.sh + EXTRA_DIST = \ $(pkgcfg_DATA) \ + $(bin_SCRIPTS) \ sample.conf lib_LTLIBRARIES = \ @@ -25,6 +33,7 @@ libtalerkyclogic_la_SOURCES = \ kyclogic_api.c libtalerkyclogic_la_LIBADD = \ $(top_builddir)/src/util/libtalerutil.la \ + -ljansson \ -lgnunetutil \ $(XLIB) libtalerkyclogic_la_LDFLAGS = \ @@ -69,6 +78,8 @@ libtaler_plugin_kyclogic_template_la_LIBADD = \ $(LTLIBINTL) libtaler_plugin_kyclogic_template_la_LDFLAGS = \ $(TALER_PLUGIN_LDFLAGS) \ + -lgnunetcurl \ + -lgnunetutil \ $(XLIB) libtaler_plugin_kyclogic_oauth2_la_SOURCES = \ @@ -77,12 +88,14 @@ libtaler_plugin_kyclogic_oauth2_la_LIBADD = \ $(LTLIBINTL) libtaler_plugin_kyclogic_oauth2_la_LDFLAGS = \ $(TALER_PLUGIN_LDFLAGS) \ + $(top_builddir)/src/templating/libtalertemplating.la \ $(top_builddir)/src/mhd/libtalermhd.la \ $(top_builddir)/src/json/libtalerjson.la \ $(top_builddir)/src/util/libtalerutil.la \ -lgnunetcurl \ -lgnunetjson \ -lgnunetutil \ + -lmicrohttpd \ -ljansson \ -lcurl \ $(XLIB) @@ -93,6 +106,7 @@ libtaler_plugin_kyclogic_kycaid_la_LIBADD = \ $(LTLIBINTL) libtaler_plugin_kyclogic_kycaid_la_LDFLAGS = \ $(TALER_PLUGIN_LDFLAGS) \ + $(top_builddir)/src/templating/libtalertemplating.la \ $(top_builddir)/src/mhd/libtalermhd.la \ $(top_builddir)/src/json/libtalerjson.la \ $(top_builddir)/src/curl/libtalercurl.la \ @@ -100,6 +114,7 @@ libtaler_plugin_kyclogic_kycaid_la_LDFLAGS = \ -lgnunetcurl \ -lgnunetjson \ -lgnunetutil \ + -lmicrohttpd \ -ljansson \ -lcurl \ $(XLIB) @@ -121,6 +136,7 @@ libtaler_plugin_kyclogic_persona_la_LDFLAGS = \ -lgnunetcurl \ -lgnunetjson \ -lgnunetutil \ + -lmicrohttpd \ -ljansson \ -lcurl \ $(XLIB) diff --git a/src/kyclogic/kyclogic-kycaid.conf b/src/kyclogic/kyclogic-kycaid.conf index 3cfb0e790..753fb689d 100644 --- a/src/kyclogic/kyclogic-kycaid.conf +++ b/src/kyclogic/kyclogic-kycaid.conf @@ -12,11 +12,15 @@ PROVIDED_CHECKS = EXAMPLE_DO_NOT_USE # How long is the KYC check valid? KYC_KYCAID_VALIDITY = forever +# Program that converts Persona KYC data into the +# GNU Taler format. +KYC_KYCAID_CONVERTER_HELPER = taler-exchange-kyc-kycaid-converter.sh + # Authentication token to use. KYC_KYCAID_AUTH_TOKEN = XXX # Form to use. KYC_KYCAID_FORM_ID = XXX -# Authentication token to use. +# URL to go to after the process is complete. KYC_KYCAID_POST_URL = https://example.com/ diff --git a/src/kyclogic/kyclogic-oauth2.conf b/src/kyclogic/kyclogic-oauth2.conf index 7ccf81c0d..57e1fc13a 100644 --- a/src/kyclogic/kyclogic-oauth2.conf +++ b/src/kyclogic/kyclogic-oauth2.conf @@ -13,11 +13,11 @@ PROVIDED_CHECKS = EXAMPLE_DO_NOT_USE KYC_OAUTH2_VALIDITY = forever # URL where we initiate the user's login process -KYC_OAUTH2_LOGIN_URL = http://kyc.example.com/login +KYC_OAUTH2_AUTHORIZE_URL = https://kyc.example.com/authorize # URL where we send the user's authentication information -KYC_OAUTH2_AUTH_URL = http://kyc.example.com/auth +KYC_OAUTH2_TOKEN_URL = https://kyc.example.com/token # URL of the user info access point. -KYC_OAUTH2_INFO_URL = http://kyc.example.com/info +KYC_OAUTH2_INFO_URL = https://kyc.example.com/info # Where does the client get redirected upon completion? KYC_OAUTH2_POST_URL = http://example.com/thank-you @@ -25,3 +25,11 @@ KYC_OAUTH2_POST_URL = http://example.com/thank-you # For authentication to the OAuth2.0 service KYC_OAUTH2_CLIENT_ID = testcase KYC_OAUTH2_CLIENT_SECRET = password + +# Mustach template that converts OAuth2.0 data about the user +# into GNU Taler standardized attribute data. +# +# This is just an example, you need to pick the right converter +# for the provider! +# +KYC_OAUTH2_CONVERTER_HELPER = taler-exchange-kyc-oauth2-converter.sh diff --git a/src/kyclogic/kyclogic-persona.conf b/src/kyclogic/kyclogic-persona.conf index de90238c7..2d52a9ee0 100644 --- a/src/kyclogic/kyclogic-persona.conf +++ b/src/kyclogic/kyclogic-persona.conf @@ -6,7 +6,10 @@ [kyclogic-persona] -# Optional authorization token for the webhook +# Optional authorization token for the webhook. +# This must be the same for all uses of the +# Persona provider, and is thus not in a +# template-specific section. #WEBHOOK_AUTH_TOKEN = wbhsec_698b5a19-c790-47f6-b396-deb572ec82f9 @@ -18,16 +21,24 @@ USER_TYPE = INDIVIDUAL PROVIDED_CHECKS = EXAMPLE_DO_NOT_USE # How long is the KYC check valid? -PERSONA_VALIDITY = forever +KYC_PERSONA_VALIDITY = forever # Which subdomain is used for our API? -PERSONA_SUBDOMAIN = taler +KYC_PERSONA_SUBDOMAIN = taler # Authentication token to use. -PERSONA_AUTH_TOKEN = persona_sandbox_42 +KYC_PERSONA_AUTH_TOKEN = persona_sandbox_42 + +# Program that converts Persona KYC data into the +# GNU Taler format. +KYC_PERSONA_CONVERTER_HELPER = taler-exchange-kyc-persona-converter.sh # Form to use. -PERSONA_TEMPLATE_ID = itempl_Uj6Xxxxx +KYC_PERSONA_TEMPLATE_ID = itempl_Uj6Xxxxx # Where do we redirect to after KYC finished successfully. -KYC_POST_URL = https://taler.net/ +KYC_PERSONA_POST_URL = https://taler.net/ + +# Salt to give to requests for idempotency. +# Optional. +# KYC_PERSONA_SALT = salt
\ No newline at end of file diff --git a/src/kyclogic/kyclogic_api.c b/src/kyclogic/kyclogic_api.c index 3954ae4ce..186799dbb 100644 --- a/src/kyclogic/kyclogic_api.c +++ b/src/kyclogic/kyclogic_api.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022-2023 Taler Systems SA TALER 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 @@ -22,6 +22,12 @@ #include "taler_kyclogic_lib.h" /** + * Name of the KYC check that may never be passed. Useful if some + * operations/amounts are categorically forbidden. + */ +#define KYC_CHECK_IMPOSSIBLE "impossible" + +/** * Information about a KYC provider. */ struct TALER_KYCLOGIC_KycProvider; @@ -180,6 +186,7 @@ TALER_KYCLOGIC_kyc_trigger_from_string (const char *trigger_s, enum TALER_KYCLOGIC_KycTriggerEvent out; } map [] = { { "withdraw", TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW }, + { "age-withdraw", TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW }, { "deposit", TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT }, { "merge", TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE }, { "balance", TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE }, @@ -208,6 +215,8 @@ TALER_KYCLOGIC_kyc_trigger2s (enum TALER_KYCLOGIC_KycTriggerEvent trigger) { case TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW: return "withdraw"; + case TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW: + return "age-withdraw"; case TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT: return "deposit"; case TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE: @@ -265,6 +274,38 @@ TALER_KYCLOGIC_kyc_user_type2s (enum TALER_KYCLOGIC_KycUserType ut) } +enum GNUNET_GenericReturnValue +TALER_KYCLOGIC_check_satisfiable ( + const char *check_name) +{ + for (unsigned int i = 0; i<num_kyc_checks; i++) + if (0 == strcmp (check_name, + kyc_checks[i]->name)) + return GNUNET_OK; + if (0 == strcmp (check_name, + KYC_CHECK_IMPOSSIBLE)) + return GNUNET_NO; + return GNUNET_SYSERR; +} + + +json_t * +TALER_KYCLOGIC_get_satisfiable () +{ + json_t *requirements; + + requirements = json_array (); + GNUNET_assert (NULL != requirements); + for (unsigned int i = 0; i<num_kyc_checks; i++) + GNUNET_assert ( + 0 == + json_array_append_new ( + requirements, + json_string (kyc_checks[i]->name))); + return requirements; +} + + /** * Load KYC logic plugin. * @@ -331,9 +372,8 @@ add_check (const char *check) /** - * Parse list of checks from @a checks and build an - * array of aliases into the global checks array - * in @a provided_checks. + * Parse list of checks from @a checks and build an array of aliases into the + * global checks array in @a provided_checks. * * @param[in,out] checks list of checks; clobbered * @param[out] p_checks where to put array of aliases @@ -585,6 +625,29 @@ add_trigger (const struct GNUNET_CONFIGURATION_Handle *cfg, GNUNET_array_append (kyc_triggers, num_kyc_triggers, kt); + for (unsigned int i = 0; i<kt->num_checks; i++) + { + const struct TALER_KYCLOGIC_KycCheck *ck = kt->required_checks[i]; + + if (0 != ck->num_providers) + continue; + if (0 == strcmp (ck->name, + KYC_CHECK_IMPOSSIBLE)) + continue; + { + char *msg; + + GNUNET_asprintf (&msg, + "Required check `%s' cannot be satisfied: not provided by any provider", + ck->name); + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + section, + "REQUIRED_CHECKS", + msg); + GNUNET_free (msg); + } + return GNUNET_SYSERR; + } } return GNUNET_OK; } @@ -614,8 +677,8 @@ struct SectionContext * @param section name of the section */ static void -handle_section (void *cls, - const char *section) +handle_provider_section (void *cls, + const char *section) { struct SectionContext *sc = cls; @@ -629,6 +692,21 @@ handle_section (void *cls, sc->result = false; return; } +} + + +/** + * Function to iterate over configuration sections. + * + * @param cls a `struct SectionContext *` + * @param section name of the section + */ +static void +handle_trigger_section (void *cls, + const char *section) +{ + struct SectionContext *sc = cls; + if (0 == strncasecmp (section, "kyc-legitimization-", strlen ("kyc-legitimization-"))) @@ -680,7 +758,10 @@ TALER_KYCLOGIC_kyc_init (const struct GNUNET_CONFIGURATION_Handle *cfg) }; GNUNET_CONFIGURATION_iterate_sections (cfg, - &handle_section, + &handle_provider_section, + &sc); + GNUNET_CONFIGURATION_iterate_sections (cfg, + &handle_trigger_section, &sc); if (! sc.result) { @@ -699,10 +780,11 @@ TALER_KYCLOGIC_kyc_init (const struct GNUNET_CONFIGURATION_Handle *cfg) TALER_KYCLOGIC_kyc_done (); return GNUNET_SYSERR; } - qsort (kyc_triggers, - num_kyc_triggers, - sizeof (struct TALER_KYCLOGIC_KycTrigger *), - &sort_by_timeframe); + if (0 != num_kyc_triggers) + qsort (kyc_triggers, + num_kyc_triggers, + sizeof (struct TALER_KYCLOGIC_KycTrigger *), + &sort_by_timeframe); return GNUNET_OK; } @@ -996,13 +1078,14 @@ remove_satisfied (void *cls, } -const char * +enum GNUNET_DB_QueryStatus TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, const struct TALER_PaytoHashP *h_payto, TALER_KYCLOGIC_KycSatisfiedIterator ki, void *ki_cls, TALER_KYCLOGIC_KycAmountIterator ai, - void *ai_cls) + void *ai_cls, + char **required) { struct TALER_KYCLOGIC_KycCheck *needed[num_kyc_checks]; unsigned int needed_cnt = 0; @@ -1035,7 +1118,10 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, &ttc); } if (0 == needed_cnt) - return NULL; + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } timeframe = GNUNET_TIME_UNIT_ZERO; for (unsigned int i = 0; i<num_kyc_triggers; i++) { @@ -1062,7 +1148,10 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, &ttc); } if (0 == needed_cnt) - return NULL; + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } { struct RemoveContext rc = { .needed = needed, @@ -1076,10 +1165,17 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, h_payto, &remove_satisfied, &rc); - GNUNET_break (qs >= 0); // FIXME: handle DB failure more nicely? + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } } if (0 == needed_cnt) - return NULL; + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } { struct RemoveContext rc = { .needed = needed, @@ -1093,10 +1189,17 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, h_payto, &remove_satisfied, &rc); - GNUNET_break (qs >= 0); // FIXME: handle DB failure more nicely? + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + return qs; + } } if (0 == needed_cnt) - return NULL; + { + *required = NULL; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } ret = NULL; for (unsigned int k = 0; k<needed_cnt; k++) { @@ -1117,7 +1220,8 @@ TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, GNUNET_free (tmp); } } - return ret; + *required = ret; + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } @@ -1144,26 +1248,31 @@ TALER_KYCLOGIC_kyc_get_details ( } -bool -TALER_KYCLOGIC_check_satisfied (const char *requirements, +enum GNUNET_DB_QueryStatus +TALER_KYCLOGIC_check_satisfied (char **requirements, const struct TALER_PaytoHashP *h_payto, json_t **kyc_details, TALER_KYCLOGIC_KycSatisfiedIterator ki, - void *ki_cls) + void *ki_cls, + bool *satisfied) { struct TALER_KYCLOGIC_KycCheck *needed[num_kyc_checks]; unsigned int needed_cnt = 0; if (NULL == requirements) - return true; { - char *req = GNUNET_strdup (requirements); + *satisfied = true; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + } + { + char *req = *requirements; for (const char *tok = strtok (req, " "); NULL != tok; tok = strtok (NULL, " ")) needed[needed_cnt++] = add_check (tok); GNUNET_free (req); + *requirements = NULL; } { @@ -1182,7 +1291,12 @@ TALER_KYCLOGIC_check_satisfied (const char *requirements, h_payto, &remove_satisfied, &rc); - GNUNET_break (qs >= 0); // FIXME: handle DB failure more nicely? + if (qs < 0) + { + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs); + *satisfied = false; + return qs; + } if (0 != needed_cnt) { json_decref (rc.kyc_details); @@ -1193,7 +1307,34 @@ TALER_KYCLOGIC_check_satisfied (const char *requirements, *kyc_details = rc.kyc_details; } } - return (0 == needed_cnt); + *satisfied = (0 == needed_cnt); + + { + char *res = NULL; + + for (unsigned int i = 0; i<needed_cnt; i++) + { + const struct TALER_KYCLOGIC_KycCheck *need = needed[i]; + + if (NULL == res) + { + res = GNUNET_strdup (need->name); + } + else + { + char *tmp; + + GNUNET_asprintf (&tmp, + "%s %s", + res, + need->name); + GNUNET_free (res); + res = tmp; + } + } + *requirements = res; + } + return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; } @@ -1339,17 +1480,32 @@ TALER_KYCLOGIC_kyc_iterate_thresholds ( } -enum TALER_ErrorCode -TALER_KYCLOGIC_user_to_attributes (const char *provider_section, - const char *provider_user_id, - const char *legitimization_id, - struct GNUNET_TIME_Timestamp *attr_expiration, - json_t **attrs) +void +TALER_KYCLOGIC_lookup_checks (const char *section_name, + unsigned int *num_checks, + char ***provided_checks) { - GNUNET_break (0); // FIXME: not yet implemented!!! - *attrs = json_object (); - *attr_expiration = GNUNET_TIME_UNIT_ZERO_TS; - return TALER_EC_NONE; + *num_checks = 0; + *provided_checks = NULL; + for (unsigned int i = 0; i<num_kyc_providers; i++) + { + struct TALER_KYCLOGIC_KycProvider *kp = kyc_providers[i]; + + if (0 != + strcasecmp (section_name, + kp->provider_section_name)) + continue; + *num_checks = kp->num_checks; + if (0 != kp->num_checks) + { + char **pc = GNUNET_new_array (kp->num_checks, + char *); + for (unsigned int i = 0; i<kp->num_checks; i++) + pc[i] = GNUNET_strdup (kp->provided_checks[i]->name); + *provided_checks = pc; + } + return; + } } diff --git a/src/kyclogic/plugin_kyclogic_kycaid.c b/src/kyclogic/plugin_kyclogic_kycaid.c index c08948f7b..243ff7c34 100644 --- a/src/kyclogic/plugin_kyclogic_kycaid.c +++ b/src/kyclogic/plugin_kyclogic_kycaid.c @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022--2024 Taler Systems SA Taler 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 @@ -19,10 +19,13 @@ * @author Christian Grothoff */ #include "platform.h" +#include "taler_attributes.h" +#include "taler_kyclogic_lib.h" #include "taler_kyclogic_plugin.h" #include "taler_mhd_lib.h" #include "taler_curl_lib.h" #include "taler_json_lib.h" +#include "taler_templating_lib.h" #include <regex.h> #include "taler_util.h" @@ -86,6 +89,12 @@ struct TALER_KYCLOGIC_ProviderDetails char *form_id; /** + * Helper binary to convert attributes returned by + * KYCAID into our internal format. + */ + char *conversion_helper; + + /** * Validity time for a successful KYC process. */ struct GNUNET_TIME_Relative validity; @@ -214,6 +223,12 @@ struct TALER_KYCLOGIC_WebhookHandle struct PluginState *ps; /** + * Handle to helper process to extract attributes + * we care about. + */ + struct TALER_JSON_ExternalConversion *econ; + + /** * Our configuration details. */ const struct TALER_KYCLOGIC_ProviderDetails *pd; @@ -224,6 +239,11 @@ struct TALER_KYCLOGIC_WebhookHandle struct MHD_Connection *connection; /** + * JSON response we got back, or NULL for none. + */ + json_t *json_response; + + /** * Verification ID from the service. */ char *verification_id; @@ -260,6 +280,11 @@ struct TALER_KYCLOGIC_WebhookHandle uint64_t process_row; /** + * HTTP response code we got from KYCAID. + */ + unsigned int kycaid_response_code; + + /** * HTTP response code to return asynchronously. */ unsigned int response_code; @@ -275,6 +300,7 @@ static void kycaid_unload_configuration (struct TALER_KYCLOGIC_ProviderDetails *pd) { curl_slist_free_all (pd->slist); + GNUNET_free (pd->conversion_helper); GNUNET_free (pd->auth_token); GNUNET_free (pd->form_id); GNUNET_free (pd->section); @@ -335,6 +361,18 @@ kycaid_load_configuration (void *cls, kycaid_unload_configuration (pd); return NULL; } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_KYCAID_CONVERTER_HELPER", + &pd->conversion_helper)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_KYCAID_CONVERTER_HELPER"); + kycaid_unload_configuration (pd); + return NULL; + } { char *auth; @@ -392,11 +430,14 @@ handle_initiate_finished (void *cls, { const char *verification_id; const char *form_url; + const char *form_id; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("verification_id", &verification_id), GNUNET_JSON_spec_string ("form_url", &form_url), + GNUNET_JSON_spec_string ("form_id", + &form_id), GNUNET_JSON_spec_end () }; @@ -418,6 +459,10 @@ handle_initiate_finished (void *cls, "type"))); break; } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Started new verification `%s' using form %s\n", + verification_id, + form_id); ih->cb (ih->cb_cls, TALER_EC_NONE, form_url, @@ -586,6 +631,7 @@ kycaid_initiate (void *cls, json_decref (body); return NULL; } + json_decref (body); ih->job = GNUNET_CURL_job_add2 (ps->curl_ctx, eh, ih->ctx.headers, @@ -624,15 +670,30 @@ proof_reply (void *cls) { struct TALER_KYCLOGIC_ProofHandle *ph = cls; struct MHD_Response *resp; + enum GNUNET_GenericReturnValue ret; + json_t *body; + unsigned int http_status; - resp = TALER_MHD_make_error (TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - "there is no '/kyc-proof' for kycaid"); + http_status = MHD_HTTP_BAD_REQUEST; + body = GNUNET_JSON_PACK ( + TALER_JSON_pack_ec (TALER_EC_GENERIC_ENDPOINT_UNKNOWN)); + GNUNET_assert (NULL != body); + ret = TALER_TEMPLATING_build (ph->connection, + &http_status, + "kycaid-invalid-request", + NULL, + NULL, + body, + &resp); + json_decref (body); + GNUNET_break (GNUNET_SYSERR != ret); ph->cb (ph->cb_cls, TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, NULL, /* user id */ NULL, /* provider legi ID */ GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ - MHD_HTTP_BAD_REQUEST, + NULL, /* attributes */ + http_status, resp); } @@ -643,7 +704,6 @@ proof_reply (void *cls) * * @param cls the @e cls of this struct with the plugin-specific state * @param pd provider configuration details - * @param url_path rest of the URL after `/kyc-webhook/` * @param connection MHD connection object (for HTTP headers) * @param account_id which account to trigger process for * @param process_row row in the legitimization processes table the legitimization is for @@ -656,7 +716,6 @@ proof_reply (void *cls) static struct TALER_KYCLOGIC_ProofHandle * kycaid_proof (void *cls, const struct TALER_KYCLOGIC_ProviderDetails *pd, - const char *const url_path[], struct MHD_Connection *connection, const struct TALER_PaytoHashP *account_id, uint64_t process_row, @@ -693,11 +752,21 @@ kycaid_webhook_cancel (struct TALER_KYCLOGIC_WebhookHandle *wh) GNUNET_SCHEDULER_cancel (wh->task); wh->task = NULL; } + if (NULL != wh->econ) + { + TALER_JSON_external_conversion_stop (wh->econ); + wh->econ = NULL; + } if (NULL != wh->job) { GNUNET_CURL_job_cancel (wh->job); wh->job = NULL; } + if (NULL != wh->json_response) + { + json_decref (wh->json_response); + wh->json_response = NULL; + } GNUNET_free (wh->verification_id); GNUNET_free (wh->applicant_id); GNUNET_free (wh->url); @@ -711,11 +780,12 @@ kycaid_webhook_cancel (struct TALER_KYCLOGIC_WebhookHandle *wh) * @param verifications JSON object with failure details */ static void -log_failure (json_t *verifications) +log_failure (const json_t *verifications) { - json_t *member; + const json_t *member; const char *name; - json_object_foreach (verifications, name, member) + + json_object_foreach ((json_t *) verifications, name, member) { bool iverified; const char *comment; @@ -749,8 +819,101 @@ log_failure (json_t *verifications) /** + * Type of a callback that receives a JSON @a result. + * + * @param cls closure our `struct TALER_KYCLOGIC_WebhookHandle *` + * @param status_type how did the process die + * @param code termination status code from the process + * @param result converted attribute data, NULL on failure + */ +static void +webhook_conversion_cb (void *cls, + enum GNUNET_OS_ProcessStatusType status_type, + unsigned long code, + const json_t *result) +{ + struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + struct GNUNET_TIME_Absolute expiration; + struct MHD_Response *resp; + + wh->econ = NULL; + if ( (0 == code) && + (NULL == result) ) + { + /* No result, but *our helper* was OK => bad input */ + GNUNET_break_op (0); + json_dumpf (wh->json_response, + stderr, + JSON_INDENT (2)); + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("kycaid_http_status", + wh->kycaid_response_code), + GNUNET_JSON_pack_object_incref ("kycaid_body", + (json_t *) wh->json_response)); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_BAD_GATEWAY, + resp); + kycaid_webhook_cancel (wh); + return; + } + if (NULL == result) + { + /* Failure in our helper */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Helper exited with status code %d\n", + (int) code); + json_dumpf (wh->json_response, + stderr, + JSON_INDENT (2)); + resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("kycaid_http_status", + wh->kycaid_response_code), + GNUNET_JSON_pack_object_incref ("kycaid_body", + (json_t *) wh->json_response)); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_BAD_GATEWAY, + resp); + kycaid_webhook_cancel (wh); + return; + } + expiration = GNUNET_TIME_relative_to_absolute (wh->pd->validity); + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + wh->cb (wh->cb_cls, + wh->process_row, + &wh->h_payto, + wh->pd->section, + wh->applicant_id, + wh->verification_id, + TALER_KYCLOGIC_STATUS_SUCCESS, + expiration, + result, + MHD_HTTP_NO_CONTENT, + resp); + kycaid_webhook_cancel (wh); +} + + +/** * Function called when we're done processing the - * HTTP "/verifications/{verification_id}" request. + * HTTP "/applicants/{verification_id}" request. * * @param cls the `struct TALER_KYCLOGIC_WebhookHandle` * @param response_code HTTP response code, 0 on error @@ -766,91 +929,78 @@ handle_webhook_finished (void *cls, struct MHD_Response *resp; wh->job = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Webhook returned with HTTP status %u\n", + (unsigned int) response_code); + wh->kycaid_response_code = response_code; + wh->json_response = json_incref ((json_t *) j); switch (response_code) { case MHD_HTTP_OK: { - const char *applicant_id; - const char *verification_id; - const char *status; - bool verified; - json_t *verifications; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("applicant_id", - &applicant_id), - GNUNET_JSON_spec_string ("verification_id", - &verification_id), - GNUNET_JSON_spec_string ("status", - &status), /* completed, pending, ... */ - GNUNET_JSON_spec_bool ("verified", - &verified), - GNUNET_JSON_spec_json ("verifications", - &verifications), - GNUNET_JSON_spec_end () - }; - struct GNUNET_TIME_Absolute expiration; - - if (GNUNET_OK != - GNUNET_JSON_parse (j, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - json_dumpf (j, - stderr, - JSON_INDENT (2)); - resp = TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("kycaid_http_status", - response_code), - GNUNET_JSON_pack_object_incref ("kycaid_body", - (json_t *) j)); - wh->cb (wh->cb_cls, - wh->process_row, - &wh->h_payto, - wh->pd->section, - wh->applicant_id, - wh->verification_id, - TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, - GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ - MHD_HTTP_BAD_GATEWAY, - resp); - break; - } - if (! verified) - { - log_failure (verifications); - } - resp = MHD_create_response_from_buffer (0, - "", - MHD_RESPMEM_PERSISTENT); - if (verified) + const char *profile_status; + + profile_status = json_string_value ( + json_object_get ( + j, + "profile_status")); + if (0 != strcasecmp ("valid", + profile_status)) { - expiration = GNUNET_TIME_relative_to_absolute (wh->pd->validity); + enum TALER_KYCLOGIC_KycStatus ks; + + ks = (0 == strcasecmp ("pending", + profile_status)) + ? TALER_KYCLOGIC_STATUS_PENDING + : TALER_KYCLOGIC_STATUS_USER_ABORTED; + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); wh->cb (wh->cb_cls, wh->process_row, &wh->h_payto, wh->pd->section, wh->applicant_id, wh->verification_id, - TALER_KYCLOGIC_STATUS_SUCCESS, - expiration, + ks, + GNUNET_TIME_UNIT_ZERO_ABS, + NULL, MHD_HTTP_NO_CONTENT, resp); + break; } - else + wh->econ + = TALER_JSON_external_conversion_start ( + j, + &webhook_conversion_cb, + wh, + wh->pd->conversion_helper, + wh->pd->conversion_helper, + "-a", + wh->pd->auth_token, + NULL); + if (NULL == wh->econ) { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start KYCAID conversion helper `%s'\n", + wh->pd->conversion_helper); + resp = TALER_MHD_make_error ( + TALER_EC_EXCHANGE_GENERIC_KYC_CONVERTER_FAILED, + NULL); wh->cb (wh->cb_cls, wh->process_row, &wh->h_payto, wh->pd->section, wh->applicant_id, wh->verification_id, - TALER_KYCLOGIC_STATUS_USER_ABORTED, - GNUNET_TIME_UNIT_ZERO_ABS, - MHD_HTTP_NO_CONTENT, + TALER_KYCLOGIC_STATUS_INTERNAL_ERROR, + GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, + MHD_HTTP_INTERNAL_SERVER_ERROR, resp); + break; } - GNUNET_JSON_parse_free (spec); + return; } break; case MHD_HTTP_BAD_REQUEST: @@ -873,6 +1023,7 @@ handle_webhook_finished (void *cls, wh->verification_id, TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, MHD_HTTP_INTERNAL_SERVER_ERROR, resp); break; @@ -894,6 +1045,7 @@ handle_webhook_finished (void *cls, wh->verification_id, TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, MHD_HTTP_NETWORK_AUTHENTICATION_REQUIRED, resp); break; @@ -911,6 +1063,7 @@ handle_webhook_finished (void *cls, wh->verification_id, TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, MHD_HTTP_GATEWAY_TIMEOUT, resp); break; @@ -934,6 +1087,7 @@ handle_webhook_finished (void *cls, wh->verification_id, TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, MHD_HTTP_BAD_GATEWAY, resp); break; @@ -951,6 +1105,7 @@ handle_webhook_finished (void *cls, wh->verification_id, TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, MHD_HTTP_SERVICE_UNAVAILABLE, resp); break; @@ -968,6 +1123,7 @@ handle_webhook_finished (void *cls, wh->verification_id, TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, MHD_HTTP_BAD_GATEWAY, resp); break; @@ -991,6 +1147,7 @@ handle_webhook_finished (void *cls, wh->verification_id, TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, MHD_HTTP_BAD_GATEWAY, resp); break; @@ -1009,6 +1166,7 @@ async_webhook_reply (void *cls) { struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + wh->task = NULL; wh->cb (wh->cb_cls, wh->process_row, (0 == wh->process_row) @@ -1019,6 +1177,7 @@ async_webhook_reply (void *cls) wh->verification_id, /* provider legi ID */ TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, wh->response_code, wh->resp); kycaid_webhook_cancel (wh); @@ -1061,11 +1220,13 @@ kycaid_webhook (void *cls, CURL *eh; const char *request_id; const char *type; - const char *verification_id; + const char *verification_id; /* = provider_legitimization_id */ const char *applicant_id; - const char *status; - bool verified; - json_t *verifications; + const char *form_id; + const char *status = NULL; + bool verified = false; + bool no_verified = true; + const json_t *verifications = NULL; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("request_id", &request_id), @@ -1075,12 +1236,20 @@ kycaid_webhook (void *cls, &verification_id), GNUNET_JSON_spec_string ("applicant_id", &applicant_id), - GNUNET_JSON_spec_string ("status", - &status), - GNUNET_JSON_spec_bool ("verified", - &verified), - GNUNET_JSON_spec_json ("verifications", - &verifications), + GNUNET_JSON_spec_string ("form_id", + &form_id), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("status", + &status), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_bool ("verified", + &verified), + &no_verified), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("verifications", + &verifications), + NULL), GNUNET_JSON_spec_end () }; enum GNUNET_DB_QueryStatus qs; @@ -1091,7 +1260,16 @@ kycaid_webhook (void *cls, wh->ps = ps; wh->pd = pd; wh->connection = connection; - + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "KYCAID webhook of `%s' triggered with %s\n", + pd->section, + http_method); +#if 1 + if (NULL != body) + json_dumpf (body, + stderr, + JSON_INDENT (2)); +#endif if (NULL == pd) { GNUNET_break_op (0); @@ -1136,37 +1314,41 @@ kycaid_webhook (void *cls, wh->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, wh); - GNUNET_JSON_parse_free (spec); return wh; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Received webhook for unknown verification ID `%s'\n", - verification_id); + "Received webhook for unknown verification ID `%s' and section `%s'\n", + verification_id, + pd->section); wh->resp = TALER_MHD_make_error ( TALER_EC_EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN, verification_id); wh->response_code = MHD_HTTP_NOT_FOUND; wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, wh); - GNUNET_JSON_parse_free (spec); return wh; } wh->verification_id = GNUNET_strdup (verification_id); wh->applicant_id = GNUNET_strdup (applicant_id); - if (! verified) + if ( (0 != strcasecmp (type, + "VERIFICATION_COMPLETED")) || + (no_verified) || + (! verified) ) { /* We don't need to re-confirm the failure by asking the API again. */ log_failure (verifications); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Webhook called with non-completion status: %s\n", + type); wh->response_code = MHD_HTTP_NO_CONTENT; wh->resp = MHD_create_response_from_buffer (0, "", MHD_RESPMEM_PERSISTENT); wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, wh); - GNUNET_JSON_parse_free (spec); return wh; } @@ -1180,13 +1362,12 @@ kycaid_webhook (void *cls, wh->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; wh->task = GNUNET_SCHEDULER_add_now (&async_webhook_reply, wh); - GNUNET_JSON_parse_free (spec); return wh; } GNUNET_asprintf (&wh->url, - "https://api.kycaid.com/verifications/%s", - verification_id); + "https://api.kycaid.com/applicants/%s", + applicant_id); GNUNET_break (CURLE_OK == curl_easy_setopt (eh, CURLOPT_VERBOSE, @@ -1204,7 +1385,6 @@ kycaid_webhook (void *cls, pd->slist, &handle_webhook_finished, wh); - GNUNET_JSON_parse_free (spec); return wh; } diff --git a/src/kyclogic/plugin_kyclogic_oauth2.c b/src/kyclogic/plugin_kyclogic_oauth2.c index 91c936bbc..3a1f50bcf 100644 --- a/src/kyclogic/plugin_kyclogic_oauth2.c +++ b/src/kyclogic/plugin_kyclogic_oauth2.c @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022-2024 Taler Systems SA Taler 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 @@ -21,6 +21,7 @@ #include "platform.h" #include "taler_kyclogic_plugin.h" #include "taler_mhd_lib.h" +#include "taler_templating_lib.h" #include "taler_json_lib.h" #include <regex.h> #include "taler_util.h" @@ -74,15 +75,21 @@ struct TALER_KYCLOGIC_ProviderDetails char *section; /** + * URL of the Challenger ``/setup`` endpoint for + * approving address validations. NULL if not used. + */ + char *setup_url; + + /** * URL of the OAuth2.0 endpoint for KYC checks. - * (token/auth) */ - char *auth_url; + char *authorize_url; /** * URL of the OAuth2.0 endpoint for KYC checks. + * (token/auth) */ - char *login_url; + char *token_url; /** * URL of the user info access endpoint. @@ -106,10 +113,22 @@ struct TALER_KYCLOGIC_ProviderDetails char *post_kyc_redirect_url; /** + * Name of the program we use to convert outputs + * from Persona into our JSON inputs. + */ + char *conversion_binary; + + /** * Validity time for a successful KYC process. */ struct GNUNET_TIME_Relative validity; + /** + * Set to true if we are operating in DEBUG + * mode and may return private details in HTML + * responses to make diagnostics easier. + */ + bool debug_mode; }; @@ -141,6 +160,11 @@ struct TALER_KYCLOGIC_InitiateHandle struct GNUNET_SCHEDULER_Task *task; /** + * Handle for the OAuth 2.0 setup request. + */ + struct GNUNET_CURL_Job *job; + + /** * Continuation to call. */ TALER_KYCLOGIC_InitiateCallback cb; @@ -170,6 +194,12 @@ struct TALER_KYCLOGIC_ProofHandle struct MHD_Connection *connection; /** + * Handle to an external process that converts the + * Persona response to our internal format. + */ + struct TALER_JSON_ExternalConversion *ec; + + /** * Hash of the payto URI that this is about. */ struct TALER_PaytoHashP h_payto; @@ -195,6 +225,11 @@ struct TALER_KYCLOGIC_ProofHandle char *post_body; /** + * KYC attributes returned about the user by the OAuth 2.0 server. + */ + json_t *attributes; + + /** * Response to return. */ struct MHD_Response *response; @@ -271,12 +306,14 @@ static void oauth2_unload_configuration (struct TALER_KYCLOGIC_ProviderDetails *pd) { GNUNET_free (pd->section); - GNUNET_free (pd->auth_url); - GNUNET_free (pd->login_url); + GNUNET_free (pd->token_url); + GNUNET_free (pd->setup_url); + GNUNET_free (pd->authorize_url); GNUNET_free (pd->info_url); GNUNET_free (pd->client_id); GNUNET_free (pd->client_secret); GNUNET_free (pd->post_kyc_redirect_url); + GNUNET_free (pd->conversion_binary); GNUNET_free (pd); } @@ -311,15 +348,30 @@ oauth2_load_configuration (void *cls, oauth2_unload_configuration (pd); return NULL; } + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_OAUTH2_CLIENT_ID", + &s)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_CLIENT_ID"); + oauth2_unload_configuration (pd); + return NULL; + } + pd->client_id = s; + if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (ps->cfg, provider_section_name, - "KYC_OAUTH2_AUTH_URL", + "KYC_OAUTH2_TOKEN_URL", &s)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "KYC_OAUTH2_AUTH_URL"); + "KYC_OAUTH2_TOKEN_URL"); oauth2_unload_configuration (pd); return NULL; } @@ -333,23 +385,23 @@ oauth2_load_configuration (void *cls, { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "KYC_OAUTH2_AUTH_URL", + "KYC_OAUTH2_TOKEN_URL", "not a valid URL"); GNUNET_free (s); oauth2_unload_configuration (pd); return NULL; } - pd->auth_url = s; + pd->token_url = s; if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (ps->cfg, provider_section_name, - "KYC_OAUTH2_LOGIN_URL", + "KYC_OAUTH2_AUTHORIZE_URL", &s)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "KYC_OAUTH2_LOGIN_URL"); + "KYC_OAUTH2_AUTHORIZE_URL"); oauth2_unload_configuration (pd); return NULL; } @@ -363,13 +415,42 @@ oauth2_load_configuration (void *cls, { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "KYC_OAUTH2_LOGIN_URL", + "KYC_OAUTH2_AUTHORIZE_URL", "not a valid URL"); oauth2_unload_configuration (pd); GNUNET_free (s); return NULL; } - pd->login_url = s; + if (NULL != strchr (s, '#')) + { + const char *extra = strchr (s, '#'); + const char *slash = strrchr (s, '/'); + + if ( (0 != strcmp (extra, + "#setup")) || + (NULL == slash) ) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_OAUTH2_AUTHORIZE_URL", + "not a valid authorze URL (bad fragment)"); + oauth2_unload_configuration (pd); + GNUNET_free (s); + return NULL; + } + pd->authorize_url = GNUNET_strndup (s, + extra - s); + GNUNET_asprintf (&pd->setup_url, + "%.*s/setup/%s", + (int) (slash - s), + s, + pd->client_id); + GNUNET_free (s); + } + else + { + pd->authorize_url = s; + } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (ps->cfg, @@ -404,44 +485,48 @@ oauth2_load_configuration (void *cls, if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (ps->cfg, provider_section_name, - "KYC_OAUTH2_CLIENT_ID", + "KYC_OAUTH2_CLIENT_SECRET", &s)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "KYC_OAUTH2_CLIENT_ID"); + "KYC_OAUTH2_CLIENT_SECRET"); oauth2_unload_configuration (pd); return NULL; } - pd->client_id = s; + pd->client_secret = s; if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (ps->cfg, provider_section_name, - "KYC_OAUTH2_CLIENT_SECRET", + "KYC_OAUTH2_POST_URL", &s)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "KYC_OAUTH2_CLIENT_SECRET"); + "KYC_OAUTH2_POST_URL"); oauth2_unload_configuration (pd); return NULL; } - pd->client_secret = s; + pd->post_kyc_redirect_url = s; if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (ps->cfg, provider_section_name, - "KYC_OAUTH2_POST_URL", - &s)) + "KYC_OAUTH2_CONVERTER_HELPER", + &pd->conversion_binary)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "KYC_OAUTH2_POST_URL"); + "KYC_OAUTH2_CONVERTER_HELPER"); oauth2_unload_configuration (pd); return NULL; } - pd->post_kyc_redirect_url = s; + if (GNUNET_OK == + GNUNET_CONFIGURATION_get_value_yesno (ps->cfg, + provider_section_name, + "KYC_OAUTH2_DEBUG_MODE")) + pd->debug_mode = true; return pd; } @@ -452,41 +537,47 @@ oauth2_load_configuration (void *cls, * how to begin the OAuth2.0 checking process to * the client. * - * @param cls a `struct TALER_KYCLOGIC_InitiateHandle *` + * @param ih process to redirect for + * @param authorize_url authorization URL to use */ static void -initiate_task (void *cls) +initiate_with_url (struct TALER_KYCLOGIC_InitiateHandle *ih, + const char *authorize_url) { - struct TALER_KYCLOGIC_InitiateHandle *ih = cls; + const struct TALER_KYCLOGIC_ProviderDetails *pd = ih->pd; struct PluginState *ps = pd->ps; char *hps; char *url; - char *redirect_uri; - char *redirect_uri_encoded; char legi_s[42]; - ih->task = NULL; GNUNET_snprintf (legi_s, sizeof (legi_s), "%llu", (unsigned long long) ih->legitimization_uuid); hps = GNUNET_STRINGS_data_to_string_alloc (&ih->h_payto, sizeof (ih->h_payto)); - GNUNET_asprintf (&redirect_uri, - "%s/kyc-proof/%s/%s/%s", - ps->exchange_base_url, - hps, - pd->section, - legi_s); - redirect_uri_encoded = TALER_urlencode (redirect_uri); - GNUNET_free (redirect_uri); - GNUNET_asprintf (&url, - "%s?client_id=%s&redirect_uri=%s", - pd->login_url, - pd->client_id, - redirect_uri_encoded); - GNUNET_free (redirect_uri_encoded); + { + char *redirect_uri_encoded; + + { + char *redirect_uri; + + GNUNET_asprintf (&redirect_uri, + "%skyc-proof/%s", + ps->exchange_base_url, + pd->section); + redirect_uri_encoded = TALER_urlencode (redirect_uri); + GNUNET_free (redirect_uri); + } + GNUNET_asprintf (&url, + "%s?response_type=code&client_id=%s&redirect_uri=%s&state=%s", + authorize_url, + pd->client_id, + redirect_uri_encoded, + hps); + GNUNET_free (redirect_uri_encoded); + } ih->cb (ih->cb_cls, TALER_EC_NONE, url, @@ -500,6 +591,169 @@ initiate_task (void *cls) /** + * After we are done with the CURL interaction we + * need to update our database state with the information + * retrieved. + * + * @param cls a `struct TALER_KYCLOGIC_InitiateHandle *` + * @param response_code HTTP response code from server, 0 on hard error + * @param response in JSON, NULL if response was not in JSON format + */ +static void +handle_curl_setup_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_KYCLOGIC_InitiateHandle *ih = cls; + const struct TALER_KYCLOGIC_ProviderDetails *pd = ih->pd; + const json_t *j = response; + + ih->job = NULL; + switch (response_code) + { + case 0: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "/setup URL failed to return HTTP response\n"); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, + NULL, + NULL, + NULL, + "/setup request to OAuth 2.0 backend returned no response"); + GNUNET_free (ih); + return; + case MHD_HTTP_OK: + { + const char *nonce; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("nonce", + &nonce), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *emsg; + unsigned int line; + char *url; + + res = GNUNET_JSON_parse (j, + spec, + &emsg, + &line); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, + NULL, + NULL, + NULL, + "Unexpected response from KYC gateway: setup must return a nonce"); + GNUNET_free (ih); + return; + } + GNUNET_asprintf (&url, + "%s/%s", + pd->authorize_url, + nonce); + initiate_with_url (ih, + url); + GNUNET_free (url); + return; + } + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "/setup URL returned HTTP status %u\n", + (unsigned int) response_code); + ih->cb (ih->cb_cls, + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, + NULL, + NULL, + NULL, + "/setup request to OAuth 2.0 backend returned unexpected HTTP status code"); + GNUNET_free (ih); + return; + } +} + + +/** + * Logic to asynchronously return the response for how to begin the OAuth2.0 + * checking process to the client. May first request a dynamic URL via + * ``/setup`` if configured to use a client-authenticated setup process. + * + * @param cls a `struct TALER_KYCLOGIC_InitiateHandle *` + */ +static void +initiate_task (void *cls) +{ + struct TALER_KYCLOGIC_InitiateHandle *ih = cls; + const struct TALER_KYCLOGIC_ProviderDetails *pd = ih->pd; + struct PluginState *ps = pd->ps; + char *hdr; + struct curl_slist *slist; + CURL *eh; + + ih->task = NULL; + if (NULL == pd->setup_url) + { + initiate_with_url (ih, + pd->authorize_url); + return; + } + eh = curl_easy_init (); + if (NULL == eh) + { + GNUNET_break (0); + ih->cb (ih->cb_cls, + TALER_EC_GENERIC_ALLOCATION_FAILURE, + NULL, + NULL, + NULL, + "curl_easy_init() failed"); + GNUNET_free (ih); + return; + } + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_URL, + pd->setup_url)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POST, + 1)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_POSTFIELDS, + "")); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_FOLLOWLOCATION, + 1L)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (eh, + CURLOPT_MAXREDIRS, + 5L)); + GNUNET_asprintf (&hdr, + "%s: Bearer %s", + MHD_HTTP_HEADER_AUTHORIZATION, + pd->client_secret); + slist = curl_slist_append (NULL, + hdr); + ih->job = GNUNET_CURL_job_add2 (ps->curl_ctx, + eh, + slist, + &handle_curl_setup_finished, + ih); + curl_slist_free_all (slist); + GNUNET_free (hdr); +} + + +/** * Initiate KYC check. * * @param cls the @e cls of this struct with the plugin-specific state @@ -546,11 +800,52 @@ oauth2_initiate_cancel (struct TALER_KYCLOGIC_InitiateHandle *ih) GNUNET_SCHEDULER_cancel (ih->task); ih->task = NULL; } + if (NULL != ih->job) + { + GNUNET_CURL_job_cancel (ih->job); + ih->job = NULL; + } GNUNET_free (ih); } /** + * Cancel KYC proof. + * + * @param[in] ph handle of operation to cancel + */ +static void +oauth2_proof_cancel (struct TALER_KYCLOGIC_ProofHandle *ph) +{ + if (NULL != ph->ec) + { + TALER_JSON_external_conversion_stop (ph->ec); + ph->ec = NULL; + } + if (NULL != ph->task) + { + GNUNET_SCHEDULER_cancel (ph->task); + ph->task = NULL; + } + if (NULL != ph->job) + { + GNUNET_CURL_job_cancel (ph->job); + ph->job = NULL; + } + if (NULL != ph->response) + { + MHD_destroy_response (ph->response); + ph->response = NULL; + } + GNUNET_free (ph->provider_user_id); + if (NULL != ph->attributes) + json_decref (ph->attributes); + GNUNET_free (ph->post_body); + GNUNET_free (ph); +} + + +/** * Function called to asynchronously return the final * result to the callback. * @@ -567,10 +862,11 @@ return_proof_response (void *cls) ph->provider_user_id, ph->provider_legitimization_id, GNUNET_TIME_relative_to_absolute (ph->pd->validity), + ph->attributes, ph->http_status, ph->response); - GNUNET_free (ph->provider_user_id); - GNUNET_free (ph); + ph->response = NULL; /*Ownership passed to 'ph->cb'!*/ + oauth2_proof_cancel (ph); } @@ -585,18 +881,18 @@ static void handle_proof_error (struct TALER_KYCLOGIC_ProofHandle *ph, const json_t *j) { - const char *msg; - const char *desc; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("error", - &msg), - GNUNET_JSON_spec_string ("error_description", - &desc), - GNUNET_JSON_spec_end () - }; + enum GNUNET_GenericReturnValue res; { - enum GNUNET_GenericReturnValue res; + const char *msg; + const char *desc; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("error", + &msg), + GNUNET_JSON_spec_string ("error_description", + &desc), + GNUNET_JSON_spec_end () + }; const char *emsg; unsigned int line; @@ -604,94 +900,113 @@ handle_proof_error (struct TALER_KYCLOGIC_ProofHandle *ph, spec, &emsg, &line); - if (GNUNET_OK != res) - { - GNUNET_break_op (0); - ph->status = TALER_KYCLOGIC_STATUS_PROVIDER_FAILED; - ph->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); - ph->http_status - = MHD_HTTP_BAD_GATEWAY; - return; - } } - /* case TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_AUTHORZATION_FAILED, - we MAY want to in the future look at the requested content type - and possibly respond in JSON if indicated. */ + + if (GNUNET_OK != res) { - char *reply; - - GNUNET_asprintf (&reply, - "<html><head><title>%s</title></head><body><h1>%s</h1><p>%s</p></body></html>", - msg, - msg, - desc); - ph->status = TALER_KYCLOGIC_STATUS_USER_ABORTED; - ph->response - = MHD_create_response_from_buffer (strlen (reply), - reply, - MHD_RESPMEM_MUST_COPY); - GNUNET_assert (NULL != ph->response); - GNUNET_free (reply); + json_t *body; + + GNUNET_break_op (0); + ph->status = TALER_KYCLOGIC_STATUS_PROVIDER_FAILED; + ph->http_status + = MHD_HTTP_BAD_GATEWAY; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("server_response", + (json_t *) j)), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_assert (NULL != body); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-authorization-failure-malformed", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + return; } ph->status = TALER_KYCLOGIC_STATUS_USER_ABORTED; ph->http_status = MHD_HTTP_FORBIDDEN; + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-authorization-failure", + NULL, + NULL, + j, + &ph->response)); } /** - * The request for @a ph succeeded (presumably). - * Call continuation with the result. + * Type of a callback that receives a JSON @a result. * - * @param[in,out] ph request that succeeded - * @param j reply from the server + * @param cls closure with a `struct TALER_KYCLOGIC_ProofHandle *` + * @param status_type how did the process die + * @param code termination status code from the process + * @param attr result some JSON result, NULL if we failed to get an JSON output */ static void -parse_proof_success_reply (struct TALER_KYCLOGIC_ProofHandle *ph, - const json_t *j) +converted_proof_cb (void *cls, + enum GNUNET_OS_ProcessStatusType status_type, + unsigned long code, + const json_t *attr) { - const char *state; - json_t *data; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("status", - &state), - GNUNET_JSON_spec_json ("data", - &data), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - const char *emsg; - unsigned int line; + struct TALER_KYCLOGIC_ProofHandle *ph = cls; + const struct TALER_KYCLOGIC_ProviderDetails *pd = ph->pd; - res = GNUNET_JSON_parse (j, - spec, - &emsg, - &line); - if (GNUNET_OK != res) + ph->ec = NULL; + if ( (NULL == attr) || + (0 != code) ) { + json_t *body; + char *msg; + GNUNET_break_op (0); - json_dumpf (j, - stderr, - JSON_INDENT (2)); ph->status = TALER_KYCLOGIC_STATUS_PROVIDER_FAILED; - ph->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); - ph->http_status - = MHD_HTTP_BAD_GATEWAY; - return; - } - if (0 != strcasecmp (state, - "success")) - { - GNUNET_break_op (0); - handle_proof_error (ph, - j); + ph->http_status = MHD_HTTP_BAD_GATEWAY; + if (0 != code) + GNUNET_asprintf (&msg, + "Attribute converter exited with status %ld", + code); + else + msg = GNUNET_strdup ( + "Attribute converter response was not in JSON format"); + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("converter", + pd->conversion_binary), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("attributes", + (json_t *) attr)), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_string ("message", + msg), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_free (msg); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-conversion-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); return; } + { const char *id; struct GNUNET_JSON_Specification ispec[] = { @@ -699,39 +1014,123 @@ parse_proof_success_reply (struct TALER_KYCLOGIC_ProofHandle *ph, &id), GNUNET_JSON_spec_end () }; + enum GNUNET_GenericReturnValue res; + const char *emsg; + unsigned int line; - res = GNUNET_JSON_parse (data, + res = GNUNET_JSON_parse (attr, ispec, &emsg, &line); if (GNUNET_OK != res) { + json_t *body; + GNUNET_break_op (0); - json_dumpf (data, - stderr, - JSON_INDENT (2)); ph->status = TALER_KYCLOGIC_STATUS_PROVIDER_FAILED; - ph->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); - ph->http_status - = MHD_HTTP_BAD_GATEWAY; + ph->http_status = MHD_HTTP_BAD_GATEWAY; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("converter", + pd->conversion_binary), + GNUNET_JSON_pack_string ("message", + "Unexpected response from KYC attribute converter: returned JSON data must contain 'id' field"), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_object_incref ("attributes", + (json_t *) attr), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-conversion-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); return; } - ph->status = TALER_KYCLOGIC_STATUS_SUCCESS; - ph->response = MHD_create_response_from_buffer (0, - "", - MHD_RESPMEM_PERSISTENT); - GNUNET_assert (NULL != ph->response); - GNUNET_break (MHD_YES == - MHD_add_response_header ( - ph->response, - MHD_HTTP_HEADER_LOCATION, - ph->pd->post_kyc_redirect_url)); - ph->http_status = MHD_HTTP_SEE_OTHER; ph->provider_user_id = GNUNET_strdup (id); } + ph->status = TALER_KYCLOGIC_STATUS_SUCCESS; + ph->response = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + GNUNET_assert (NULL != ph->response); + GNUNET_break (MHD_YES == + MHD_add_response_header ( + ph->response, + MHD_HTTP_HEADER_LOCATION, + ph->pd->post_kyc_redirect_url)); + ph->http_status = MHD_HTTP_SEE_OTHER; + ph->attributes = json_incref ((json_t *) attr); + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); +} + + +/** + * The request for @a ph succeeded (presumably). + * Call continuation with the result. + * + * @param[in,out] ph request that succeeded + * @param j reply from the server + */ +static void +parse_proof_success_reply (struct TALER_KYCLOGIC_ProofHandle *ph, + const json_t *j) +{ + const struct TALER_KYCLOGIC_ProviderDetails *pd = ph->pd; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Calling converter `%s' with JSON\n", + pd->conversion_binary); + json_dumpf (j, + stderr, + JSON_INDENT (2)); + ph->ec = TALER_JSON_external_conversion_start ( + j, + &converted_proof_cb, + ph, + pd->conversion_binary, + pd->conversion_binary, + NULL); + if (NULL != ph->ec) + return; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start KYCAID conversion helper `%s'\n", + pd->conversion_binary); + ph->status = TALER_KYCLOGIC_STATUS_INTERNAL_ERROR; + ph->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; + { + json_t *body; + + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("converter", + pd->conversion_binary), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_string ("message", + "Failed to launch KYC conversion helper process."), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_GENERIC_KYC_CONVERTER_FAILED)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-conversion-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + } + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); } @@ -755,10 +1154,34 @@ handle_curl_proof_finished (void *cls, ph->job = NULL; switch (response_code) { + case 0: + { + json_t *body; + + ph->status = TALER_KYCLOGIC_STATUS_PROVIDER_FAILED; + ph->http_status = MHD_HTTP_BAD_GATEWAY; + + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("message", + "No response from KYC gateway"), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-provider-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + } + break; case MHD_HTTP_OK: parse_proof_success_reply (ph, j); - break; + return; default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "OAuth2.0 info URL returned HTTP status %u\n", @@ -797,15 +1220,21 @@ handle_curl_login_finished (void *cls, const char *token_type; uint64_t expires_in_s; const char *refresh_token; + bool no_expires; + bool no_refresh; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("access_token", &access_token), GNUNET_JSON_spec_string ("token_type", &token_type), - GNUNET_JSON_spec_uint64 ("expires_in", - &expires_in_s), - GNUNET_JSON_spec_string ("refresh_token", - &refresh_token), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("expires_in", + &expires_in_s), + &no_expires), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("refresh_token", + &refresh_token), + &no_refresh), GNUNET_JSON_spec_end () }; CURL *eh; @@ -821,26 +1250,59 @@ handle_curl_login_finished (void *cls, &line); if (GNUNET_OK != res) { + json_t *body; + GNUNET_break_op (0); - ph->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected response from KYC gateway"); ph->http_status = MHD_HTTP_BAD_GATEWAY; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("server_response", + (json_t *) j), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_string ("message", + "Unexpected response from KYC gateway: required fields missing or malformed"), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-provider-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); break; } } if (0 != strcasecmp (token_type, "bearer")) { + json_t *body; + GNUNET_break_op (0); - ph->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Unexpected token type in response from KYC gateway"); - ph->http_status - = MHD_HTTP_BAD_GATEWAY; + ph->http_status = MHD_HTTP_BAD_GATEWAY; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("server_response", + (json_t *) j), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_string ("message", + "Unexpected 'token_type' in response from KYC gateway: 'bearer' token required"), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-provider-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); break; } @@ -855,28 +1317,34 @@ handle_curl_login_finished (void *cls, (NULL != strchr (access_token, ';')) ) { + json_t *body; + GNUNET_break_op (0); - ph->response - = TALER_MHD_make_error ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE, - "Illegal character in access token"); - ph->http_status - = MHD_HTTP_BAD_GATEWAY; + ph->http_status = MHD_HTTP_BAD_GATEWAY; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_object_incref ("server_response", + (json_t *) j), + GNUNET_JSON_pack_bool ("debug", + ph->pd->debug_mode), + GNUNET_JSON_pack_string ("message", + "Illegal character in access token"), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-provider-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); break; } eh = curl_easy_init (); - if (NULL == eh) - { - GNUNET_break_op (0); - ph->response - = TALER_MHD_make_error ( - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "curl_easy_init"); - ph->http_status - = MHD_HTTP_INTERNAL_SERVER_ERROR; - break; - } + GNUNET_assert (NULL != eh); GNUNET_assert (CURLE_OK == curl_easy_setopt (eh, CURLOPT_URL, @@ -918,7 +1386,6 @@ handle_curl_login_finished (void *cls, * * @param cls the @e cls of this struct with the plugin-specific state * @param pd provider configuration details - * @param url_path rest of the URL after `/kyc-webhook/` * @param connection MHD connection object (for HTTP headers) * @param account_id which account to trigger process for * @param process_row row in the legitimization processes table the legitimization is for @@ -931,7 +1398,6 @@ handle_curl_login_finished (void *cls, static struct TALER_KYCLOGIC_ProofHandle * oauth2_proof (void *cls, const struct TALER_KYCLOGIC_ProviderDetails *pd, - const char *const url_path[], struct MHD_Connection *connection, const struct TALER_PaytoHashP *account_id, uint64_t process_row, @@ -944,7 +1410,6 @@ oauth2_proof (void *cls, struct TALER_KYCLOGIC_ProofHandle *ph; const char *code; - (void) url_path; GNUNET_break (NULL == provider_user_id); ph = GNUNET_new (struct TALER_KYCLOGIC_ProofHandle); GNUNET_snprintf (ph->provider_legitimization_id, @@ -959,6 +1424,7 @@ oauth2_proof (void *cls, GNUNET_free (ph); return NULL; } + ph->pd = pd; ph->connection = connection; ph->h_payto = *account_id; @@ -969,62 +1435,126 @@ oauth2_proof (void *cls, "code"); if (NULL == code) { - GNUNET_break_op (0); - ph->status = TALER_KYCLOGIC_STATUS_USER_PENDING; - ph->http_status = MHD_HTTP_BAD_REQUEST; - ph->response = TALER_MHD_make_error ( - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "code"); + const char *err; + const char *desc; + const char *euri; + json_t *body; + + err = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "error"); + if (NULL == err) + { + GNUNET_break_op (0); + ph->status = TALER_KYCLOGIC_STATUS_USER_PENDING; + ph->http_status = MHD_HTTP_BAD_REQUEST; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("message", + "'code' parameter malformed"), + TALER_JSON_pack_ec ( + TALER_EC_GENERIC_PARAMETER_MALFORMED)); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-bad-request", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); + ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, + ph); + return ph; + } + desc = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "error_description"); + euri = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "error_uri"); + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "OAuth2 process %llu failed with error `%s'\n", + (unsigned long long) process_row, + err); + if (0 == strcmp (err, + "server_error")) + ph->status = TALER_KYCLOGIC_STATUS_PROVIDER_FAILED; + else if (0 == strcmp (err, + "unauthorized_client")) + ph->status = TALER_KYCLOGIC_STATUS_FAILED; + else if (0 == strcmp (err, + "temporarily_unavailable")) + ph->status = TALER_KYCLOGIC_STATUS_PENDING; + else + ph->status = TALER_KYCLOGIC_STATUS_INTERNAL_ERROR; + ph->http_status = MHD_HTTP_FORBIDDEN; + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("error", + err), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("error_details", + desc)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("error_uri", + euri))); + GNUNET_break ( + GNUNET_SYSERR != + TALER_TEMPLATING_build (ph->connection, + &ph->http_status, + "oauth2-authentication-failure", + NULL, + NULL, + body, + &ph->response)); + json_decref (body); ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, ph); return ph; - } - ph->eh = curl_easy_init (); - if (NULL == ph->eh) - { - GNUNET_break (0); - ph->status = TALER_KYCLOGIC_STATUS_USER_PENDING; - ph->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR; - ph->response = TALER_MHD_make_error ( - TALER_EC_GENERIC_ALLOCATION_FAILURE, - "curl_easy_init"); - ph->task = GNUNET_SCHEDULER_add_now (&return_proof_response, - ph); - return ph; } + ph->eh = curl_easy_init (); + GNUNET_assert (NULL != ph->eh); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Requesting OAuth 2.0 data via HTTP POST `%s'\n", + pd->token_url); GNUNET_assert (CURLE_OK == curl_easy_setopt (ph->eh, CURLOPT_URL, - pd->auth_url)); + pd->token_url)); + GNUNET_assert (CURLE_OK == + curl_easy_setopt (ph->eh, + CURLOPT_VERBOSE, + 1)); GNUNET_assert (CURLE_OK == curl_easy_setopt (ph->eh, CURLOPT_POST, 1)); { char *client_id; - char *redirect_uri; char *client_secret; char *authorization_code; + char *redirect_uri_encoded; + char *hps; + hps = GNUNET_STRINGS_data_to_string_alloc (&ph->h_payto, + sizeof (ph->h_payto)); + { + char *redirect_uri; + + GNUNET_asprintf (&redirect_uri, + "%skyc-proof/%s", + ps->exchange_base_url, + pd->section); + redirect_uri_encoded = TALER_urlencode (redirect_uri); + GNUNET_free (redirect_uri); + } + GNUNET_assert (NULL != redirect_uri_encoded); client_id = curl_easy_escape (ph->eh, pd->client_id, 0); GNUNET_assert (NULL != client_id); - { - char *request_uri; - - GNUNET_asprintf (&request_uri, - "%s?client_id=%s", - pd->login_url, - pd->client_id); - redirect_uri = curl_easy_escape (ph->eh, - request_uri, - 0); - GNUNET_free (request_uri); - } - GNUNET_assert (NULL != redirect_uri); client_secret = curl_easy_escape (ph->eh, pd->client_secret, 0); @@ -1034,14 +1564,16 @@ oauth2_proof (void *cls, 0); GNUNET_assert (NULL != authorization_code); GNUNET_asprintf (&ph->post_body, - "client_id=%s&redirect_uri=%s&client_secret=%s&code=%s&grant_type=authorization_code", + "client_id=%s&redirect_uri=%s&state=%s&client_secret=%s&code=%s&grant_type=authorization_code", client_id, - redirect_uri, + redirect_uri_encoded, + hps, client_secret, authorization_code); curl_free (authorization_code); curl_free (client_secret); - curl_free (redirect_uri); + GNUNET_free (redirect_uri_encoded); + GNUNET_free (hps); curl_free (client_id); } GNUNET_assert (CURLE_OK == @@ -1068,34 +1600,6 @@ oauth2_proof (void *cls, /** - * Cancel KYC proof. - * - * @param[in] ph handle of operation to cancel - */ -static void -oauth2_proof_cancel (struct TALER_KYCLOGIC_ProofHandle *ph) -{ - if (NULL != ph->task) - { - GNUNET_SCHEDULER_cancel (ph->task); - ph->task = NULL; - } - if (NULL != ph->job) - { - GNUNET_CURL_job_cancel (ph->job); - ph->job = NULL; - } - if (NULL != ph->response) - { - MHD_destroy_response (ph->response); - ph->response = NULL; - } - GNUNET_free (ph->post_body); - GNUNET_free (ph); -} - - -/** * Function to asynchronously return the 404 not found * page for the webhook. * @@ -1119,6 +1623,7 @@ wh_return_not_found (void *cls) NULL, TALER_KYCLOGIC_STATUS_KEEP, GNUNET_TIME_UNIT_ZERO_ABS, + NULL, MHD_HTTP_NOT_FOUND, response); GNUNET_free (wh); diff --git a/src/kyclogic/plugin_kyclogic_persona.c b/src/kyclogic/plugin_kyclogic_persona.c index 651388c99..c68b7f881 100644 --- a/src/kyclogic/plugin_kyclogic_persona.c +++ b/src/kyclogic/plugin_kyclogic_persona.c @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022, 2023 Taler Systems SA Taler 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 @@ -19,6 +19,7 @@ * @author Christian Grothoff */ #include "platform.h" +#include "taler_attributes.h" #include "taler_kyclogic_plugin.h" #include "taler_mhd_lib.h" #include "taler_curl_lib.h" @@ -62,9 +63,10 @@ struct PluginState struct GNUNET_CURL_RescheduleContext *curl_rc; /** - * Authorization token to use when receiving webhooks from the Persona service. Optional. Note that - * webhooks are *global* and not per template. - */ + * Authorization token to use when receiving webhooks from the Persona + * service. Optional. Note that webhooks are *global* and not per + * template. + */ char *webhook_token; @@ -110,6 +112,12 @@ struct TALER_KYCLOGIC_ProviderDetails char *subdomain; /** + * Name of the program we use to convert outputs + * from Persona into our JSON inputs. + */ + char *conversion_binary; + + /** * Where to redirect the client upon completion. */ char *post_kyc_redirect_url; @@ -229,6 +237,12 @@ struct TALER_KYCLOGIC_ProofHandle char *url; /** + * Handle to an external process that converts the + * Persona response to our internal format. + */ + struct TALER_JSON_ExternalConversion *ec; + + /** * Hash of the payto:// URI we are checking the KYC for. */ struct TALER_PaytoHashP h_payto; @@ -245,6 +259,11 @@ struct TALER_KYCLOGIC_ProofHandle char *provider_user_id; /** + * Account ID from the service. + */ + char *account_id; + + /** * Inquiry ID at the provider. */ char *inquiry_id; @@ -293,6 +312,11 @@ struct TALER_KYCLOGIC_WebhookHandle char *inquiry_id; /** + * Account ID from the service. + */ + char *account_id; + + /** * URL of the cURL request. */ char *url; @@ -314,6 +338,12 @@ struct TALER_KYCLOGIC_WebhookHandle const char *template_id; /** + * Handle to an external process that converts the + * Persona response to our internal format. + */ + struct TALER_JSON_ExternalConversion *ec; + + /** * Our account ID. */ struct TALER_PaytoHashP h_payto; @@ -342,6 +372,7 @@ persona_unload_configuration (struct TALER_KYCLOGIC_ProviderDetails *pd) GNUNET_free (pd->auth_token); GNUNET_free (pd->template_id); GNUNET_free (pd->subdomain); + GNUNET_free (pd->conversion_binary); GNUNET_free (pd->salt); GNUNET_free (pd->section); GNUNET_free (pd->post_kyc_redirect_url); @@ -369,31 +400,31 @@ persona_load_configuration (void *cls, if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_time (ps->cfg, provider_section_name, - "PERSONA_VALIDITY", + "KYC_PERSONA_VALIDITY", &pd->validity)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "PERSONA_VALIDITY"); + "KYC_PERSONA_VALIDITY"); persona_unload_configuration (pd); return NULL; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (ps->cfg, provider_section_name, - "PERSONA_AUTH_TOKEN", + "KYC_PERSONA_AUTH_TOKEN", &pd->auth_token)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "PERSONA_AUTH_TOKEN"); + "KYC_PERSONA_AUTH_TOKEN"); persona_unload_configuration (pd); return NULL; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (ps->cfg, provider_section_name, - "SALT", + "KYC_PERSONA_SALT", &pd->salt)) { uint32_t salt[8]; @@ -407,36 +438,48 @@ persona_load_configuration (void *cls, if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (ps->cfg, provider_section_name, - "PERSONA_SUBDOMAIN", + "KYC_PERSONA_SUBDOMAIN", &pd->subdomain)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "PERSONA_SUBDOMAIN"); + "KYC_PERSONA_SUBDOMAIN"); + persona_unload_configuration (pd); + return NULL; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (ps->cfg, + provider_section_name, + "KYC_PERSONA_CONVERTER_HELPER", + &pd->conversion_binary)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + provider_section_name, + "KYC_PERSONA_CONVERTER_HELPER"); persona_unload_configuration (pd); return NULL; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (ps->cfg, provider_section_name, - "KYC_POST_URL", + "KYC_PERSONA_POST_URL", &pd->post_kyc_redirect_url)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "KYC_POST_URL"); + "KYC_PERSONA_POST_URL"); persona_unload_configuration (pd); return NULL; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (ps->cfg, provider_section_name, - "PERSONA_TEMPLATE_ID", + "KYC_PERSONA_TEMPLATE_ID", &pd->template_id)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, provider_section_name, - "PERSONA_TEMPLATE_ID"); + "KYC_PERSONA_TEMPLATE_ID"); persona_unload_configuration (pd); return NULL; } @@ -747,13 +790,14 @@ persona_initiate (void *cls, (unsigned long long) ih->legitimization_uuid); payto_s = GNUNET_STRINGS_data_to_string_alloc (&ih->h_payto, sizeof (ih->h_payto)); - /* NOTE: check here that exchange_base_url ends - with a '/'? */ + GNUNET_break ('/' == + pd->ps->exchange_base_url[strlen ( + pd->ps->exchange_base_url) - 1]); GNUNET_asprintf (&proof_url, - "%skyc-proof/%s/%s", + "%skyc-proof/%s?state=%s", pd->ps->exchange_base_url, - payto_s, - pd->section); + pd->section, + payto_s); body = GNUNET_JSON_PACK ( GNUNET_JSON_pack_object_steal ( "data", @@ -835,8 +879,14 @@ persona_proof_cancel (struct TALER_KYCLOGIC_ProofHandle *ph) GNUNET_CURL_job_cancel (ph->job); ph->job = NULL; } + if (NULL != ph->ec) + { + TALER_JSON_external_conversion_stop (ph->ec); + ph->ec = NULL; + } GNUNET_free (ph->url); GNUNET_free (ph->provider_user_id); + GNUNET_free (ph->account_id); GNUNET_free (ph->inquiry_id); GNUNET_free (ph); } @@ -865,12 +915,9 @@ proof_generic_reply (struct TALER_KYCLOGIC_ProofHandle *ph, { struct MHD_Response *resp; enum GNUNET_GenericReturnValue ret; - struct GNUNET_TIME_Absolute expiration; - if (TALER_KYCLOGIC_STATUS_SUCCESS == status) - expiration = GNUNET_TIME_relative_to_absolute (ph->pd->validity); - else - expiration = GNUNET_TIME_UNIT_ZERO_ABS; + /* This API is not usable for successful replies */ + GNUNET_assert (TALER_KYCLOGIC_STATUS_SUCCESS != status); ret = TALER_TEMPLATING_build (ph->connection, &http_status, template, @@ -888,7 +935,8 @@ proof_generic_reply (struct TALER_KYCLOGIC_ProofHandle *ph, status, account_id, inquiry_id, - expiration, + GNUNET_TIME_UNIT_ZERO_ABS, + NULL, http_status, resp); } @@ -922,8 +970,132 @@ proof_reply_error (struct TALER_KYCLOGIC_ProofHandle *ph, /** + * Return a response for the @a ph request indicating a + * protocol violation by the Persona server. + * + * @param[in,out] ph request we are processing + * @param response_code HTTP status returned by Persona + * @param inquiry_id ID of the inquiry this is about + * @param detail where the response was wrong + * @param data full response data to output + */ +static void +return_invalid_response (struct TALER_KYCLOGIC_ProofHandle *ph, + unsigned int response_code, + const char *inquiry_id, + const char *detail, + const json_t *data) +{ + proof_reply_error ( + ph, + inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-invalid-response", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + GNUNET_JSON_pack_string ("persona_inquiry_id", + inquiry_id), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), + GNUNET_JSON_pack_string ("detail", + detail), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); +} + + +/** + * Start the external conversion helper. + * + * @param pd configuration details + * @param attr attributes to give to the helper + * @param cb function to call with the result + * @param cb_cls closure for @a cb + * @return handle for the helper + */ +static struct TALER_JSON_ExternalConversion * +start_conversion (const struct TALER_KYCLOGIC_ProviderDetails *pd, + const json_t *attr, + TALER_JSON_JsonCallback cb, + void *cb_cls) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Calling converter `%s' with JSON\n", + pd->conversion_binary); + json_dumpf (attr, + stderr, + JSON_INDENT (2)); + return TALER_JSON_external_conversion_start ( + attr, + cb, + cb_cls, + pd->conversion_binary, + pd->conversion_binary, + "-a", + pd->auth_token, + NULL + ); +} + + +/** + * Type of a callback that receives a JSON @a result. + * + * @param cls closure with a `struct TALER_KYCLOGIC_ProofHandle *` + * @param status_type how did the process die + * @param code termination status code from the process + * @param attr result some JSON result, NULL if we failed to get an JSON output + */ +static void +proof_post_conversion_cb (void *cls, + enum GNUNET_OS_ProcessStatusType status_type, + unsigned long code, + const json_t *attr) +{ + struct TALER_KYCLOGIC_ProofHandle *ph = cls; + struct MHD_Response *resp; + struct GNUNET_TIME_Absolute expiration; + + ph->ec = NULL; + if ( (NULL == attr) || + (0 != code) ) + { + GNUNET_break_op (0); + return_invalid_response (ph, + MHD_HTTP_OK, + ph->inquiry_id, + "converter", + NULL); + persona_proof_cancel (ph); + return; + } + expiration = GNUNET_TIME_relative_to_absolute (ph->pd->validity); + resp = MHD_create_response_from_buffer (0, + "", + MHD_RESPMEM_PERSISTENT); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + MHD_HTTP_HEADER_LOCATION, + ph->pd->post_kyc_redirect_url)); + TALER_MHD_add_global_headers (resp); + ph->cb (ph->cb_cls, + TALER_KYCLOGIC_STATUS_SUCCESS, + ph->account_id, + ph->inquiry_id, + expiration, + attr, + MHD_HTTP_SEE_OTHER, + resp); + persona_proof_cancel (ph); +} + + +/** * Function called when we're done processing the - * HTTP "/api/v1/verifications/{verification-id}" request. + * HTTP "/api/v1/inquiries/{inquiry-id}" request. * * @param cls the `struct TALER_KYCLOGIC_InitiateHandle` * @param response_code HTTP response code, 0 on error @@ -947,14 +1119,17 @@ handle_proof_finished (void *cls, const char *inquiry_id; const char *account_id; const char *type = NULL; - json_t *attributes; + const json_t *attributes; + const json_t *relationships; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("type", &type), GNUNET_JSON_spec_string ("id", &inquiry_id), - GNUNET_JSON_spec_json ("attributes", - &attributes), + GNUNET_JSON_spec_object_const ("attributes", + &attributes), + GNUNET_JSON_spec_object_const ("relationships", + &relationships), GNUNET_JSON_spec_end () }; @@ -967,25 +1142,11 @@ handle_proof_finished (void *cls, "inquiry")) ) { GNUNET_break_op (0); - json_dumpf (j, - stderr, - JSON_INDENT (2)); - proof_reply_error (ph, - inquiry_id, - MHD_HTTP_BAD_GATEWAY, - "persona-logic-failure", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_string ("persona_inquiry_id", - inquiry_id), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), - GNUNET_JSON_pack_string ("detail", - "data"), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) data)))); + return_invalid_response (ph, + response_code, + inquiry_id, + "data", + data); break; } @@ -996,10 +1157,10 @@ handle_proof_finished (void *cls, struct GNUNET_JSON_Specification ispec[] = { GNUNET_JSON_spec_string ("status", &status), - GNUNET_JSON_spec_string ("reference_id", + GNUNET_JSON_spec_string ("reference-id", &reference_id), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("expired_at", + GNUNET_JSON_spec_string ("expired-at", &expired_at), NULL), GNUNET_JSON_spec_end () @@ -1011,27 +1172,11 @@ handle_proof_finished (void *cls, NULL, NULL)) { GNUNET_break_op (0); - json_dumpf (j, - stderr, - JSON_INDENT (2)); - proof_reply_error (ph, - inquiry_id, - MHD_HTTP_BAD_GATEWAY, - "persona-invalid-response", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_string ("persona_inquiry_id", - inquiry_id), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), - GNUNET_JSON_pack_string ("detail", - "data-attributes"), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) data)))); - GNUNET_JSON_parse_free (ispec); - GNUNET_JSON_parse_free (spec); + return_invalid_response (ph, + response_code, + inquiry_id, + "data-attributes", + data); break; } { @@ -1045,25 +1190,11 @@ handle_proof_finished (void *cls, (idr != ph->process_row) ) { GNUNET_break_op (0); - proof_reply_error (ph, - inquiry_id, - MHD_HTTP_BAD_GATEWAY, - "persona-invalid-response", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_string ("persona_inquiry_id", - inquiry_id), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), - GNUNET_JSON_pack_string ("detail", - "data-attributes-reference_id"), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) - data)))); - GNUNET_JSON_parse_free (ispec); - GNUNET_JSON_parse_free (spec); + return_invalid_response (ph, + response_code, + inquiry_id, + "data-attributes-reference_id", + data); break; } } @@ -1072,25 +1203,11 @@ handle_proof_finished (void *cls, ph->inquiry_id)) { GNUNET_break_op (0); - proof_reply_error (ph, - inquiry_id, - MHD_HTTP_BAD_GATEWAY, - "persona-invalid-response", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_string ("persona_inquiry_id", - inquiry_id), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), - GNUNET_JSON_pack_string ("detail", - "data-id"), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) - data)))); - GNUNET_JSON_parse_free (ispec); - GNUNET_JSON_parse_free (spec); + return_invalid_response (ph, + response_code, + inquiry_id, + "data-id", + data); break; } @@ -1098,9 +1215,7 @@ handle_proof_finished (void *cls, json_object_get ( json_object_get ( json_object_get ( - json_object_get ( - data, - "relationships"), + relationships, "account"), "data"), "id")); @@ -1108,77 +1223,56 @@ handle_proof_finished (void *cls, if (0 != strcmp (status, "completed")) { - proof_generic_reply (ph, - TALER_KYCLOGIC_STATUS_FAILED, - account_id, - inquiry_id, - MHD_HTTP_OK, - "persona-kyc-failed", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_string ("persona_inquiry_id", - inquiry_id), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) - data)))); - GNUNET_JSON_parse_free (ispec); - GNUNET_JSON_parse_free (spec); + proof_generic_reply ( + ph, + TALER_KYCLOGIC_STATUS_FAILED, + account_id, + inquiry_id, + MHD_HTTP_OK, + "persona-kyc-failed", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + GNUNET_JSON_pack_string ("persona_inquiry_id", + inquiry_id), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); break; } if (NULL == account_id) { GNUNET_break_op (0); - json_dumpf (data, - stderr, - JSON_INDENT (2)); - proof_reply_error (ph, - inquiry_id, - MHD_HTTP_BAD_GATEWAY, - "persona-invalid-response", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - GNUNET_JSON_pack_string ("persona_inquiry_id", - inquiry_id), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), - GNUNET_JSON_pack_string ("detail", - "data-relationships-account-data-id"), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) - data)))); + return_invalid_response (ph, + response_code, + inquiry_id, + "data-relationships-account-data-id", + data); break; } - + ph->account_id = GNUNET_strdup (account_id); + ph->ec = start_conversion (ph->pd, + j, + &proof_post_conversion_cb, + ph); + if (NULL == ph->ec) { - struct MHD_Response *resp; - struct GNUNET_TIME_Absolute expiration; - - expiration = GNUNET_TIME_relative_to_absolute (ph->pd->validity); - resp = MHD_create_response_from_buffer (0, - "", - MHD_RESPMEM_PERSISTENT); - GNUNET_break (MHD_YES == - MHD_add_response_header (resp, - MHD_HTTP_HEADER_LOCATION, - ph->pd->post_kyc_redirect_url)); - TALER_MHD_add_global_headers (resp); - ph->cb (ph->cb_cls, - TALER_KYCLOGIC_STATUS_SUCCESS, - account_id, - inquiry_id, - expiration, - MHD_HTTP_SEE_OTHER, - resp); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start Persona conversion helper\n"); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-logic-failure", + GNUNET_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_GENERIC_KYC_CONVERTER_FAILED))); + break; } - GNUNET_JSON_parse_free (ispec); } - GNUNET_JSON_parse_free (spec); - break; + return; /* continued in proof_post_conversion_cb */ } case MHD_HTTP_BAD_REQUEST: case MHD_HTTP_NOT_FOUND: @@ -1191,59 +1285,61 @@ handle_proof_finished (void *cls, json_dumpf (j, stderr, JSON_INDENT (2)); - proof_reply_error (ph, - ph->inquiry_id, - MHD_HTTP_BAD_GATEWAY, - "persona-logic-failure", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), - - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) - data)))); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-logic-failure", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), + + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); break; case MHD_HTTP_UNAUTHORIZED: /* These are failures of the exchange operator */ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Refused access with HTTP status code %u\n", (unsigned int) response_code); - proof_reply_error (ph, - ph->inquiry_id, - MHD_HTTP_INTERNAL_SERVER_ERROR, - "persona-exchange-unauthorized", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) - data)))); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-exchange-unauthorized", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); break; case MHD_HTTP_PAYMENT_REQUIRED: /* These are failures of the exchange operator */ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Refused access with HTTP status code %u\n", (unsigned int) response_code); - - proof_reply_error (ph, - ph->inquiry_id, - MHD_HTTP_INTERNAL_SERVER_ERROR, - "persona-exchange-unpaid", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) - data)))); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_SERVICE_UNAVAILABLE, + "persona-exchange-unpaid", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); break; case MHD_HTTP_REQUEST_TIMEOUT: /* These are networking issues */ @@ -1253,19 +1349,20 @@ handle_proof_finished (void *cls, json_dumpf (j, stderr, JSON_INDENT (2)); - proof_reply_error (ph, - ph->inquiry_id, - MHD_HTTP_GATEWAY_TIMEOUT, - "persona-network-timeout", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) - data)))); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_GATEWAY_TIMEOUT, + "persona-network-timeout", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); break; case MHD_HTTP_TOO_MANY_REQUESTS: /* This is a load issue */ @@ -1275,19 +1372,20 @@ handle_proof_finished (void *cls, json_dumpf (j, stderr, JSON_INDENT (2)); - proof_reply_error (ph, - ph->inquiry_id, - MHD_HTTP_SERVICE_UNAVAILABLE, - "persona-load-failure", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) - data)))); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_SERVICE_UNAVAILABLE, + "persona-load-failure", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); break; case MHD_HTTP_INTERNAL_SERVER_ERROR: /* This is an issue with Persona */ @@ -1297,19 +1395,20 @@ handle_proof_finished (void *cls, json_dumpf (j, stderr, JSON_INDENT (2)); - proof_reply_error (ph, - ph->inquiry_id, - MHD_HTTP_BAD_GATEWAY, - "persona-provider-failure", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_ERROR), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) - data)))); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-provider-failure", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_PROOF_BACKEND_ERROR), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); break; default: /* This is an issue with Persona */ @@ -1319,21 +1418,20 @@ handle_proof_finished (void *cls, json_dumpf (j, stderr, JSON_INDENT (2)); - proof_reply_error (ph, - ph->inquiry_id, - MHD_HTTP_BAD_GATEWAY, - "persona-invalid-response", - GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("persona_http_status", - response_code), - TALER_JSON_pack_ec ( - TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), - GNUNET_JSON_pack_string ("detail", - "data-relationships-account-data-id"), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("data", - (json_t *) - data)))); + proof_reply_error ( + ph, + ph->inquiry_id, + MHD_HTTP_BAD_GATEWAY, + "persona-invalid-response", + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("persona_http_status", + response_code), + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("data", + (json_t *) + data)))); break; } persona_proof_cancel (ph); @@ -1345,7 +1443,6 @@ handle_proof_finished (void *cls, * * @param cls the @e cls of this struct with the plugin-specific state * @param pd provider configuration details - * @param url_path rest of the URL after `/kyc-webhook/` * @param connection MHD connection object (for HTTP headers) * @param account_id which account to trigger process for * @param process_row row in the legitimization processes table the legitimization is for @@ -1358,7 +1455,6 @@ handle_proof_finished (void *cls, static struct TALER_KYCLOGIC_ProofHandle * persona_proof (void *cls, const struct TALER_KYCLOGIC_ProviderDetails *pd, - const char *const url_path[], struct MHD_Connection *connection, const struct TALER_PaytoHashP *account_id, uint64_t process_row, @@ -1432,6 +1528,12 @@ persona_webhook_cancel (struct TALER_KYCLOGIC_WebhookHandle *wh) GNUNET_CURL_job_cancel (wh->job); wh->job = NULL; } + if (NULL != wh->ec) + { + TALER_JSON_external_conversion_stop (wh->ec); + wh->ec = NULL; + } + GNUNET_free (wh->account_id); GNUNET_free (wh->inquiry_id); GNUNET_free (wh->url); GNUNET_free (wh); @@ -1445,6 +1547,7 @@ persona_webhook_cancel (struct TALER_KYCLOGIC_WebhookHandle *wh) * @param status status to return * @param account_id account to return * @param inquiry_id inquiry ID to supply + * @param attr KYC attribute data for the client * @param http_status HTTP status to use */ static void @@ -1452,6 +1555,7 @@ webhook_generic_reply (struct TALER_KYCLOGIC_WebhookHandle *wh, enum TALER_KYCLOGIC_KycStatus status, const char *account_id, const char *inquiry_id, + const json_t *attr, unsigned int http_status) { struct MHD_Response *resp; @@ -1468,11 +1572,12 @@ webhook_generic_reply (struct TALER_KYCLOGIC_WebhookHandle *wh, wh->cb (wh->cb_cls, wh->process_row, &wh->h_payto, - account_id, wh->pd->section, + account_id, inquiry_id, status, expiration, + attr, http_status, resp); } @@ -1494,13 +1599,40 @@ webhook_reply_error (struct TALER_KYCLOGIC_WebhookHandle *wh, TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, NULL, /* user id */ inquiry_id, + NULL, /* attributes */ http_status); } /** + * Type of a callback that receives a JSON @a result. + * + * @param cls closure with a `struct TALER_KYCLOGIC_WebhookHandle *` + * @param status_type how did the process die + * @param code termination status code from the process + * @param attr some JSON result, NULL if we failed to get an JSON output + */ +static void +webhook_post_conversion_cb (void *cls, + enum GNUNET_OS_ProcessStatusType status_type, + unsigned long code, + const json_t *attr) +{ + struct TALER_KYCLOGIC_WebhookHandle *wh = cls; + + wh->ec = NULL; + webhook_generic_reply (wh, + TALER_KYCLOGIC_STATUS_SUCCESS, + wh->account_id, + wh->inquiry_id, + attr, + MHD_HTTP_OK); +} + + +/** * Function called when we're done processing the - * HTTP "/verifications/{verification_id}" request. + * HTTP "/api/v1/inquiries/{inquiry_id}" request. * * @param cls the `struct TALER_KYCLOGIC_WebhookHandle` * @param response_code HTTP response code, 0 on error @@ -1524,14 +1656,17 @@ handle_webhook_finished (void *cls, const char *inquiry_id; const char *account_id; const char *type = NULL; - json_t *attributes; + const json_t *attributes; + const json_t *relationships; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("type", &type), GNUNET_JSON_spec_string ("id", &inquiry_id), - GNUNET_JSON_spec_json ("attributes", - &attributes), + GNUNET_JSON_spec_object_const ("attributes", + &attributes), + GNUNET_JSON_spec_object_const ("relationships", + &relationships), GNUNET_JSON_spec_end () }; @@ -1560,10 +1695,10 @@ handle_webhook_finished (void *cls, struct GNUNET_JSON_Specification ispec[] = { GNUNET_JSON_spec_string ("status", &status), - GNUNET_JSON_spec_string ("reference_id", + GNUNET_JSON_spec_string ("reference-id", &reference_id), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("expired_at", + GNUNET_JSON_spec_string ("expired-at", &expired_at), NULL), GNUNET_JSON_spec_end () @@ -1581,8 +1716,6 @@ handle_webhook_finished (void *cls, webhook_reply_error (wh, inquiry_id, MHD_HTTP_BAD_GATEWAY); - GNUNET_JSON_parse_free (ispec); - GNUNET_JSON_parse_free (spec); break; } { @@ -1599,8 +1732,6 @@ handle_webhook_finished (void *cls, webhook_reply_error (wh, inquiry_id, MHD_HTTP_BAD_GATEWAY); - GNUNET_JSON_parse_free (ispec); - GNUNET_JSON_parse_free (spec); break; } } @@ -1612,8 +1743,6 @@ handle_webhook_finished (void *cls, webhook_reply_error (wh, inquiry_id, MHD_HTTP_BAD_GATEWAY); - GNUNET_JSON_parse_free (ispec); - GNUNET_JSON_parse_free (spec); break; } @@ -1621,9 +1750,7 @@ handle_webhook_finished (void *cls, json_object_get ( json_object_get ( json_object_get ( - json_object_get ( - data, - "relationships"), + relationships, "account"), "data"), "id")); @@ -1635,9 +1762,8 @@ handle_webhook_finished (void *cls, TALER_KYCLOGIC_STATUS_FAILED, account_id, inquiry_id, + NULL, MHD_HTTP_OK); - GNUNET_JSON_parse_free (ispec); - GNUNET_JSON_parse_free (spec); break; } @@ -1652,16 +1778,22 @@ handle_webhook_finished (void *cls, MHD_HTTP_BAD_GATEWAY); break; } - - webhook_generic_reply (wh, - TALER_KYCLOGIC_STATUS_SUCCESS, - account_id, + wh->account_id = GNUNET_strdup (account_id); + wh->ec = start_conversion (wh->pd, + j, + &webhook_post_conversion_cb, + wh); + if (NULL == wh->ec) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to start Persona conversion helper\n"); + webhook_reply_error (wh, inquiry_id, - MHD_HTTP_OK); - GNUNET_JSON_parse_free (ispec); + MHD_HTTP_INTERNAL_SERVER_ERROR); + break; + } } - GNUNET_JSON_parse_free (spec); - break; + return; /* continued in webhook_post_conversion_cb */ } case MHD_HTTP_BAD_REQUEST: case MHD_HTTP_NOT_FOUND: @@ -1772,6 +1904,7 @@ async_webhook_reply (void *cls) wh->inquiry_id, /* provider legi ID */ TALER_KYCLOGIC_STATUS_PROVIDER_FAILED, GNUNET_TIME_UNIT_ZERO_ABS, /* expiration */ + NULL, wh->response_code, wh->resp); persona_webhook_cancel (wh); @@ -1853,13 +1986,13 @@ persona_webhook (void *cls, wh->ps = ps; wh->connection = connection; wh->pd = pd; - auth_header = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_AUTHORIZATION); if ( (NULL != ps->webhook_token) && - (0 != strcmp (ps->webhook_token, - auth_header)) ) + ( (NULL == auth_header) || + (0 != strcmp (ps->webhook_token, + auth_header)) ) ) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Invalid authorization header `%s' received for Persona webhook\n", @@ -1891,7 +2024,7 @@ persona_webhook (void *cls, "payload"), "data"), "relationships"), - "inquiry_template"), + "inquiry-template"), "data"), "id")); if (NULL == wh->template_id) @@ -1934,7 +2067,6 @@ persona_webhook (void *cls, return wh; } - persona_inquiry_id = json_string_value ( json_object_get ( diff --git a/src/kyclogic/plugin_kyclogic_template.c b/src/kyclogic/plugin_kyclogic_template.c index b4531117e..54f36e6f2 100644 --- a/src/kyclogic/plugin_kyclogic_template.c +++ b/src/kyclogic/plugin_kyclogic_template.c @@ -279,7 +279,6 @@ template_proof_cancel (struct TALER_KYCLOGIC_ProofHandle *ph) * * @param cls the @e cls of this struct with the plugin-specific state * @param pd provider configuration details - * @param url_path rest of the URL after `/kyc-webhook/` * @param connection MHD connection object (for HTTP headers) * @param account_id which account to trigger process for * @param process_row row in the legitimization processes table the legitimization is for @@ -292,7 +291,6 @@ template_proof_cancel (struct TALER_KYCLOGIC_ProofHandle *ph) static struct TALER_KYCLOGIC_ProofHandle * template_proof (void *cls, const struct TALER_KYCLOGIC_ProviderDetails *pd, - const char *const url_path[], struct MHD_Connection *connection, const struct TALER_PaytoHashP *account_id, uint64_t process_row, @@ -304,7 +302,6 @@ template_proof (void *cls, struct PluginState *ps = cls; struct TALER_KYCLOGIC_ProofHandle *ph; - (void) url_path; (void) account_id; (void) process_row; (void) provider_user_id; diff --git a/src/kyclogic/taler-exchange-kyc-kycaid-converter.sh b/src/kyclogic/taler-exchange-kyc-kycaid-converter.sh new file mode 100755 index 000000000..68a1b6a0d --- /dev/null +++ b/src/kyclogic/taler-exchange-kyc-kycaid-converter.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# This file is in the public domain. +# +# This code converts (some of) the JSON output from KYCAID into the GNU Taler +# specific KYC attribute data (again in JSON format). We may need to download +# and inline file data in the process, for authorization pass "-a" with the +# respective bearer token. +# + +# Die if anything goes wrong. +set -eu + +# Parse command-line options +while getopts ':a:' OPTION; do + case "$OPTION" in + a) + TOKEN="$OPTARG" + ;; + ?) + echo "Unrecognized command line option" + exit 1 + ;; + esac +done + +# First, extract everything from stdin. +J=$(jq '{"type":.type,"email":.email,"phone":.phone,"first_name":.first_name,"name-middle":.middle_name,"last_name":.last_name,"dob":.dob,"residence_country":.residence_country,"gender":.gender,"pep":.pep,"addresses":.addresses,"documents":.documents,"company_name":.company_name,"business_activity_id":.business_activity_id,"registration_country":.registration_country,"documents":.documents,"decline_reasons":.decline_reasons}') + +# TODO: +# log_failure (json_object_get (j, "decline_reasons")); + +TYPE=$(echo "$J" | jq -r '.type') + +N=0 +DOCS_RAW="" +DOCS_JSON="" +for ID in $(jq -r '.documents[]|select(.status=="valid")|.id') +do + TYPE=$(jq -r ".documents[]|select(.id==\"$ID\")|.type") + EXPIRY=$(jq -r ".documents[]|select(.id==\"$ID\")|.expiry_date") + DOCUMENT_FILE=$(mktemp -t tmp.XXXXXXXXXX) + # Authorization: Token $TOKEN + DOCUMENT_URL="https://api.kycaid.com/documents/$ID" + if [ -z "${TOKEN:-}" ] + then + wget -q --output-document=- "$DOCUMENT_URL" \ + | gnunet-base32 > ${DOCUMENT_FILE} + else + wget -q --output-document=- "$DOCUMENT_URL" \ + --header "Authorization: Token $TOKEN" \ + | gnunet-base32 > ${DOCUMENT_FILE} + fi + DOCS_RAW="$DOCS_RAW --rawfile photo$N \"${DOCUMENT_FILE}\"" + if [ "$N" = 0 ] + then + DOCS_JSON="{\"type\":\"$TYPE\",\"image\":\$photo$N}" + else + DOCS_JSON="{\"type\":\"$TYPE\",\"image\":\$photo$N},$DOCS_JSON" + fi + N=$(expr $N + 1) +done + + +if [ "PERSON" = "${TYPE}" ] +then + + # Next, combine some fields into larger values. + FULLNAME=$(echo "$J" | jq -r '[.first_name,.middle_name,.last_name]|join(" ")') +# STREET=$(echo $J | jq -r '[."street-1",."street-2"]|join(" ")') +# CITY=$(echo $J | jq -r '[.postcode,.city,."address-subdivision,.cc"]|join(" ")') + + # Combine into final result for individual. + echo "$J" \ + | jq \ + --arg full_name "${FULLNAME}" \ + '{$full_name,"birthdate":.dob,"pep":.pep,"phone":.phone,"email":.email,"residences":.residence_country}' \ + | jq \ + 'del(..|select(.==null))' + +else + # Combine into final result for business. + echo "$J" \ + | jq \ + $DOCS_RAW \ + "{\"company_name\":.company_name,\"phone\":.phone,\"email\":.email,\"registration_country\":.registration_country,\"documents\":[${DOCS_JSON}]}" \ + | jq \ + 'del(..|select(.==null))' +fi + +exit 0 diff --git a/src/kyclogic/taler-exchange-kyc-oauth2-challenger.sh b/src/kyclogic/taler-exchange-kyc-oauth2-challenger.sh new file mode 100755 index 000000000..729abc504 --- /dev/null +++ b/src/kyclogic/taler-exchange-kyc-oauth2-challenger.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# This file is in the public domain. +# +# This code converts (some of) the JSON output from +# Challenger into the GNU Taler +# specific KYC attribute data (again in JSON format). +# + +# Die if anything goes wrong. +set -eu + +# First, extract everything from stdin. +J=$(jq '{"id":.id,"email":.address,"type":.address_type,"expires":.address_expiration}') + +ADDRESS_TYPE=$(echo "$J" | jq -r '.type') +ROWID=$(echo "$J" | jq -r '.id') +if [ "$ADDRESS_TYPE" != "email" ] +then + return 1 +fi + +echo "$J" \ + | jq \ + --arg id "${ROWID}" \ + '{$id,"email":.email,"expires":.expires}' + +exit 0 diff --git a/src/kyclogic/taler-exchange-kyc-oauth2-nda.sh b/src/kyclogic/taler-exchange-kyc-oauth2-nda.sh new file mode 100755 index 000000000..5af785f19 --- /dev/null +++ b/src/kyclogic/taler-exchange-kyc-oauth2-nda.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# This file is in the public domain. +# +# This code converts (some of) the JSON output from NDA into the GNU Taler +# specific KYC attribute data (again in JSON format). +# + +# Die if anything goes wrong. +set -eu + +# First, extract everything from stdin. +J=$(jq '{"status":.status,"id":.data.id,"last":.data.last_name,"first":.data.first_name,"phone":.data.phone}') + +STATUS=$(echo "$J" | jq -r '.status') +if [ "$STATUS" != "success" ] +then + return 1 +fi + +# Next, combine some fields into larger values. +FULLNAME=$(echo "$J" | jq -r '[.first_name,.last_name]|join(" ")') + +echo "$J" \ + | jq \ + --arg full_name "${FULLNAME}" \ + '{$full_name,"phone":.phone,"id":.id}' \ + | jq \ + 'del(..|select(.==null))' + +exit 0 diff --git a/src/kyclogic/taler-exchange-kyc-oauth2-test-converter.sh b/src/kyclogic/taler-exchange-kyc-oauth2-test-converter.sh new file mode 100755 index 000000000..76f9f16c4 --- /dev/null +++ b/src/kyclogic/taler-exchange-kyc-oauth2-test-converter.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# This file is in the public domain. +# +# This code converts (some of) the JSON output from +# Challenger into the GNU Taler +# specific KYC attribute data (again in JSON format). +# + +# Die if anything goes wrong. +set -eu + + +# First, extract everything from stdin. +J=$(jq '{"id":.data.id,"first":.data.first_name,"last":.data.last_name,"birthdate":.data.birthdate,"status":.status}') + +# Next, combine some fields into larger values. +STATUS=$(echo "$J" | jq -r '.status') +if [ "$STATUS" != "success" ] +then + exit 1 +fi + +FULLNAME=$(echo "$J" | jq -r '[.first,.last]|join(" ")') + +echo $J \ + | jq \ + --arg full_name "${FULLNAME}" \ + '{$full_name,"birthdate":.birthdate,"id":.id}' \ + | jq \ + 'del(..|select(.==null))' +exit 0 diff --git a/src/kyclogic/taler-exchange-kyc-persona-converter.sh b/src/kyclogic/taler-exchange-kyc-persona-converter.sh new file mode 100755 index 000000000..13142d0e5 --- /dev/null +++ b/src/kyclogic/taler-exchange-kyc-persona-converter.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# This file is in the public domain. +# +# This code converts (some of) the JSON output from Persona into the GNU Taler +# specific KYC attribute data (again in JSON format). We may need to download +# and inline file data in the process, for authorization pass "-a" with the +# respective bearer token. +# + +# Die if anything goes wrong. +set -eu + +# Parse command-line options +while getopts ':a:' OPTION; do + case "$OPTION" in + a) + TOKEN="$OPTARG" + ;; + ?) + echo "Unrecognized command line option" + exit 1 + ;; + esac +done + + +# First, extract everything from stdin. +J=$(jq '{"first":.data.attributes."name-first","middle":.data.attributes."name-middle","last":.data.attributes."name-last","cc":.data.attributes.fields."address-country-code".value,"birthdate":.data.attributes.birthdate,"city":.data.attributes."address-city","postcode":.data.attributes."address-postal-code","street-1":.data.attributes."address-street-1","street-2":.data.attributes."address-street-2","address-subdivision":.data.attributes."address-subdivision","identification-number":.data.attributes."identification-number","photo":.included[]|select(.type=="verification/government-id")|.attributes|select(.status=="passed")|."front-photo-url"}') + + +# Next, combine some fields into larger values. +FULLNAME=$(echo "$J" | jq -r '[.first,.middle,.last]|join(" ")') +STREET=$(echo $J | jq -r '[."street-1",."street-2"]|join(" ")') +CITY=$(echo $J | jq -r '[.postcode,.city,."address-subdivision,.cc"]|join(" ")') + +# Download and base32-encode the photo +PHOTO_URL=$(echo "$J" | jq -r '.photo') +PHOTO_FILE=$(mktemp -t tmp.XXXXXXXXXX) +if [ -z "${TOKEN:-}" ] +then + wget -q --output-document=- "$PHOTO_URL" | gnunet-base32 > ${PHOTO_FILE} +else + wget -q --output-document=- --header "Authorization: Bearer $TOKEN" "$PHOTO_URL" | gnunet-base32 > ${PHOTO_FILE} +fi + +# Combine into final result. +echo "$J" \ + | jq \ + --arg full_name "${FULLNAME}" \ + --arg street "${STREET}" \ + --arg city "${CITY}" \ + --rawfile photo "${PHOTO_FILE}" \ + '{$full_name,$street,$city,"birthdate":.birthdate,"residences":.cc,"identification_number":."identification-number",$photo}' \ + | jq \ + 'del(..|select(.==null))' + +exit 0 diff --git a/src/kyclogic/taler-exchange-kyc-tester.c b/src/kyclogic/taler-exchange-kyc-tester.c index 2aed8d961..c2efafd72 100644 --- a/src/kyclogic/taler-exchange-kyc-tester.c +++ b/src/kyclogic/taler-exchange-kyc-tester.c @@ -28,7 +28,7 @@ #include "taler_mhd_lib.h" #include "taler_json_lib.h" #include "taler_templating_lib.h" -#include "taler_crypto_lib.h" +#include "taler_util.h" #include "taler_kyclogic_lib.h" #include "taler_kyclogic_plugin.h" #include <gnunet/gnunet_mhd_compat.h> @@ -436,6 +436,7 @@ kyc_webhook_cleanup (void) * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown * @param status KYC status * @param expiration until when is the KYC check valid + * @param attributes user attributes returned by the provider * @param http_status HTTP status code of @a response * @param[in] response to return to the HTTP client */ @@ -449,6 +450,7 @@ webhook_finished_cb ( const char *provider_legitimization_id, enum TALER_KYCLOGIC_KycStatus status, struct GNUNET_TIME_Absolute expiration, + const json_t *attributes, unsigned int http_status, struct MHD_Response *response) { @@ -457,12 +459,31 @@ webhook_finished_cb ( (void) expiration; (void) provider_section; kwh->wh = NULL; - GNUNET_break (0 == GNUNET_memcmp (account_id, - &cmd_line_h_payto)); - GNUNET_break (0 == strcmp (provider_user_id, - cmd_provider_user_id)); - GNUNET_break (0 == strcmp (provider_legitimization_id, - cmd_provider_legitimization_id)); + if ( (NULL != account_id) && + (0 != GNUNET_memcmp (account_id, + &cmd_line_h_payto)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received webhook for unexpected account\n"); + } + if ( (NULL != provider_user_id) && + (NULL != cmd_provider_user_id) && + (0 != strcmp (provider_user_id, + cmd_provider_user_id)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received webhook for unexpected provider user ID (%s)\n", + provider_user_id); + } + if ( (NULL != provider_legitimization_id) && + (NULL != cmd_provider_legitimization_id) && + (0 != strcmp (provider_legitimization_id, + cmd_provider_legitimization_id)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received webhook for unexpected provider legitimization ID (%s)\n", + provider_legitimization_id); + } switch (status) { case TALER_KYCLOGIC_STATUS_SUCCESS: @@ -471,6 +492,12 @@ webhook_finished_cb ( "KYC successful for user `%s' (legi: %s)\n", provider_user_id, provider_legitimization_id); + GNUNET_break (NULL != attributes); + fprintf (stderr, + "Extracted attributes:\n"); + json_dumpf (attributes, + stderr, + JSON_INDENT (2)); break; default: GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -567,11 +594,12 @@ handler_kyc_webhook_generic ( rc->rh_ctx = kwh; rc->rh_cleaner = &clean_kwh; - if (GNUNET_OK != - TALER_KYCLOGIC_lookup_logic (args[0], - &kwh->plugin, - &kwh->pd, - &kwh->section_name)) + if ( (NULL == args[0]) || + (GNUNET_OK != + TALER_KYCLOGIC_lookup_logic (args[0], + &kwh->plugin, + &kwh->pd, + &kwh->section_name)) ) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "KYC logic `%s' unknown (check KYC provider configuration)\n", @@ -581,14 +609,6 @@ handler_kyc_webhook_generic ( TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, args[0]); } - if (0 != strcmp (args[0], - kwh->section_name)) - { - return TALER_MHD_reply_with_error (rc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "$PROVIDER_SECTION"); - } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Calling KYC provider specific webhook\n"); kwh->wh = kwh->plugin->webhook (kwh->plugin->cls, @@ -649,6 +669,8 @@ handler_kyc_webhook_get ( struct TEKT_RequestContext *rc, const char *const args[]) { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Webhook GET triggered\n"); return handler_kyc_webhook_generic (rc, MHD_HTTP_METHOD_GET, NULL, @@ -670,6 +692,8 @@ handler_kyc_webhook_post ( const json_t *root, const char *const args[]) { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Webhook POST triggered\n"); return handler_kyc_webhook_generic (rc, MHD_HTTP_METHOD_POST, root, @@ -688,6 +712,7 @@ handler_kyc_webhook_post ( * @param provider_user_id set to user ID at the provider, or NULL if not supported or unknown * @param provider_legitimization_id set to legitimization process ID at the provider, or NULL if not supported or unknown * @param expiration until when is the KYC check valid + * @param attributes attributes about the user * @param http_status HTTP status code of @a response * @param[in] response to return to the HTTP client */ @@ -698,6 +723,7 @@ proof_cb ( const char *provider_user_id, const char *provider_legitimization_id, struct GNUNET_TIME_Absolute expiration, + const json_t *attributes, unsigned int http_status, struct MHD_Response *response) { @@ -710,13 +736,26 @@ proof_cb ( status, http_status, provider_user_id); - MHD_resume_connection (rs->rc->connection); - TALER_MHD_daemon_trigger (); + if (TALER_KYCLOGIC_STATUS_SUCCESS == status) + { + GNUNET_break (NULL != attributes); + fprintf (stderr, + "Extracted attributes:\n"); + json_dumpf (attributes, + stderr, + JSON_INDENT (2)); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning response %p with status %u\n", + response, + http_status); rs->rc->response = response; rs->rc->http_status = http_status; GNUNET_CONTAINER_DLL_remove (rs_head, rs_tail, rs); + MHD_resume_connection (rs->rc->connection); + TALER_MHD_daemon_trigger (); GNUNET_free (rs); } @@ -727,32 +766,44 @@ proof_cb ( * * @param rc request context * @param args remaining URL arguments; - * args[0] is the 'h_payto', - * args[1] should be the logic plugin name + * args[0] should be the logic plugin name */ static MHD_RESULT handler_kyc_proof_get ( struct TEKT_RequestContext *rc, - const char *const args[]) + const char *const args[1]) { struct TALER_PaytoHashP h_payto; struct TALER_KYCLOGIC_ProviderDetails *pd; struct TALER_KYCLOGIC_Plugin *logic; struct ProofRequestState *rs; const char *section_name; + const char *h_paytos; - if ( (NULL == args[0]) || - (NULL == args[1]) ) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "GET /kyc-proof triggered\n"); + if (NULL == args[0]) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_NOT_FOUND, TALER_EC_GENERIC_ENDPOINT_UNKNOWN, - "'/$H_PAYTO/$LOGIC' required after '/kyc-proof'"); + "'/kyc-proof/$PROVIDER_SECTION?state=$H_PAYTO' required"); + } + h_paytos = MHD_lookup_connection_value (rc->connection, + MHD_GET_ARGUMENT_KIND, + "state"); + if (NULL == h_paytos) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MISSING, + "h_payto"); } if (GNUNET_OK != - GNUNET_STRINGS_string_to_data (args[0], - strlen (args[0]), + GNUNET_STRINGS_string_to_data (h_paytos, + strlen (h_paytos), &h_payto, sizeof (h_payto))) { @@ -774,18 +825,18 @@ handler_kyc_proof_get ( } if (GNUNET_OK != - TALER_KYCLOGIC_lookup_logic (args[1], + TALER_KYCLOGIC_lookup_logic (args[0], &logic, &pd, §ion_name)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Could not initiate KYC with provider `%s' (configuration error?)\n", - args[1]); + args[0]); return TALER_MHD_reply_with_error (rc->connection, MHD_HTTP_NOT_FOUND, TALER_EC_EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN, - args[1]); + args[0]); } rs = GNUNET_new (struct ProofRequestState); rs->rc = rc; @@ -796,7 +847,6 @@ handler_kyc_proof_get ( rs); rs->ph = logic->proof (logic->cls, pd, - &args[2], rc->connection, &h_payto, kyc_row_id, @@ -940,9 +990,9 @@ proceed_with_handler (struct TEKT_RequestContext *rc, /* Parse command-line arguments */ /* make a copy of 'url' because 'strtok_r()' will modify */ - memcpy (d, - url, - ulen); + GNUNET_memcpy (d, + url, + ulen); i = 0; args[i++] = strtok_r (d, "/", &sp); while ( (NULL != args[i - 1]) && @@ -1032,8 +1082,7 @@ handle_mhd_request (void *cls, .url = "kyc-proof", .method = MHD_HTTP_METHOD_GET, .handler.get = &handler_kyc_proof_get, - .nargs = 128, - .nargs_is_upper_bound = true + .nargs = 1 }, { .url = "kyc-webhook", @@ -1145,14 +1194,14 @@ handle_mhd_request (void *cls, } /* cache to avoid the loop next time */ rc->rh = rh; - /* run handler */ - return proceed_with_handler (rc, - url + tok_size + 1, - upload_data, - upload_data_size); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handler found for %s '%s'\n", + method, + url); + return MHD_YES; } - if (found) /* FIXME: this can never be true right now */ + if (found) { /* we found a matching address, but the method is wrong */ struct MHD_Response *reply; @@ -1330,11 +1379,30 @@ initiate_cb ( GNUNET_SCHEDULER_shutdown (); return; } - fprintf (stdout, - "Visit `%s' to begin KYC process (-u: '%s', -U: '%s')\n", - redirect_url, - provider_user_id, - provider_legitimization_id); + { + char *s; + + s = GNUNET_STRINGS_data_to_string_alloc (&cmd_line_h_payto, + sizeof (cmd_line_h_payto)); + if (NULL != provider_user_id) + { + fprintf (stdout, + "Visit `%s' to begin KYC process.\nAlso use: taler-exchange-kyc-tester -w -u '%s' -U '%s' -p %s\n", + redirect_url, + provider_user_id, + provider_legitimization_id, + s); + } + else + { + fprintf (stdout, + "Visit `%s' to begin KYC process.\nAlso use: taler-exchange-kyc-tester -w -U '%s' -p %s\n", + redirect_url, + provider_legitimization_id, + s); + } + GNUNET_free (s); + } GNUNET_free (cmd_provider_user_id); GNUNET_free (cmd_provider_legitimization_id); if (NULL != provider_user_id) |