commit 1d39ca797e0276dcaae7eb8c202a652df3b34c03
parent d8938ddd91c4afa894d6a7669fb36ff53cfa1f7f
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Sat, 22 Mar 2025 21:33:28 +0100
add token endpoint basic auth; make other APIs require access tokens (as opposed to configurable secrets). WIP, most tests fail.
Diffstat:
6 files changed, 257 insertions(+), 149 deletions(-)
diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c
@@ -33,6 +33,7 @@
#include "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_get-orders-ID.h"
#include "taler-merchant-httpd_get-templates-ID.h"
+#include "taler-merchant-httpd_helper.h"
#include "taler-merchant-httpd_mhd.h"
#include "taler-merchant-httpd_private-delete-account-ID.h"
#include "taler-merchant-httpd_private-delete-categories-ID.h"
@@ -205,75 +206,6 @@ static const struct GNUNET_CONFIGURATION_Handle *cfg;
char *TMH_default_auth;
-/**
- * Check validity of login @a token for the given @a instance_id.
- *
- * @param token the login token given in the request
- * @param instance_id the instance the login is to be checked against
- * @param[out] as set to scope of the token if it is valid
- * @return TALER_EC_NONE on success
- */
-static enum TALER_ErrorCode
-TMH_check_token (const char *token,
- const char *instance_id,
- enum TMH_AuthScope *as)
-{
- enum TMH_AuthScope scope;
- struct GNUNET_TIME_Timestamp expiration;
- enum GNUNET_DB_QueryStatus qs;
- struct TALER_MERCHANTDB_LoginTokenP btoken;
-
- if (NULL == token)
- {
- *as = TMH_AS_NONE;
- return TALER_EC_NONE;
- }
- /* This was presumably checked before... */
- GNUNET_assert (0 == strncasecmp (token,
- RFC_8959_PREFIX,
- strlen (RFC_8959_PREFIX)));
- token += strlen (RFC_8959_PREFIX);
- if (GNUNET_OK !=
- GNUNET_STRINGS_string_to_data (token,
- strlen (token),
- &btoken,
- sizeof (btoken)))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Given authorization token `%s' is malformed\n",
- token);
- GNUNET_break_op (0);
- return TALER_EC_GENERIC_TOKEN_MALFORMED;
- }
- qs = TMH_db->select_login_token (TMH_db->cls,
- instance_id,
- &btoken,
- &expiration,
- &scope);
- if (qs < 0)
- {
- GNUNET_break (0);
- return TALER_EC_GENERIC_DB_FETCH_FAILED;
- }
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Authorization token `%s' unknown\n",
- token);
- return TALER_EC_GENERIC_TOKEN_UNKNOWN;
- }
- if (GNUNET_TIME_absolute_is_past (expiration.abs_time))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Authorization token `%s' expired\n",
- token);
- return TALER_EC_GENERIC_TOKEN_EXPIRED;
- }
- *as = scope;
- return TALER_EC_NONE;
-}
-
-
enum GNUNET_GenericReturnValue
TMH_check_auth (const char *token,
struct TALER_MerchantAuthenticationSaltP *salt,
@@ -312,6 +244,47 @@ TMH_check_auth (const char *token,
}
+enum GNUNET_GenericReturnValue
+TMH_check_auth_instance (const char *token,
+ struct TMH_MerchantInstance *instance)
+{
+ char *tmp;
+ const char *instance_name;
+ const char *password;
+ enum GNUNET_GenericReturnValue ret;
+
+ if (0 == GNUNET_STRINGS_base64_decode (token,
+ strlen (token),
+ (void**) &tmp))
+ {
+ return GNUNET_SYSERR;
+ }
+ instance_name = strtok (tmp, ":");
+ if (NULL == instance_name)
+ {
+ GNUNET_free (tmp);
+ return GNUNET_SYSERR;
+ }
+ password = strtok (NULL, ":");
+ if (NULL == password)
+ {
+ GNUNET_free (tmp);
+ return GNUNET_SYSERR;
+ }
+ if (0 != strncmp (instance_name, instance->settings.id,
+ strlen (instance->settings.id)))
+ {
+ GNUNET_free (tmp);
+ return GNUNET_SYSERR;
+ }
+ ret = TMH_check_auth (password,
+ &instance->auth.auth_salt,
+ &instance->auth.auth_hash);
+ GNUNET_free (tmp);
+ return ret;
+}
+
+
void
TMH_compute_auth (const char *token,
struct TALER_MerchantAuthenticationSaltP *salt,
@@ -655,24 +628,41 @@ spa_redirect (const struct TMH_RequestHandler *rh,
* or set to NULL if header value is invalid
*/
static void
-extract_token (const char **auth)
+extract_auth (const char **auth)
{
const char *bearer = "Bearer ";
+ const char *basic = "Basic ";
const char *tok = *auth;
+ int offset = 0;
+ int is_bearer = GNUNET_NO;
- if (0 != strncmp (tok,
+ if (0 == strncmp (tok,
bearer,
strlen (bearer)))
{
+ offset = strlen (bearer);
+ is_bearer = GNUNET_YES;
+ }
+ else if (0 == strncmp (tok,
+ basic,
+ strlen (basic)))
+ {
+ offset = strlen (basic);
+ }
+ else
+ {
*auth = NULL;
return;
}
- tok += strlen (bearer);
+
+
+ tok += offset;
while (' ' == *tok)
tok++;
- if (0 != strncasecmp (tok,
- RFC_8959_PREFIX,
- strlen (RFC_8959_PREFIX)))
+ if ((GNUNET_YES == is_bearer) &&
+ (0 != strncasecmp (tok,
+ RFC_8959_PREFIX,
+ strlen (RFC_8959_PREFIX))))
{
*auth = NULL;
return;
@@ -1895,13 +1885,15 @@ url_handler (void *cls,
{
const char *auth;
bool auth_ok;
+ bool is_basic_auth;
bool auth_malformed = false;
/* PATCHing an instance can alternatively be checked against
the default instance */
- auth = MHD_lookup_connection_value (connection,
- MHD_HEADER_KIND,
- MHD_HTTP_HEADER_AUTHORIZATION);
+ auth = MHD_lookup_connection_value (connection,
+ MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_AUTHORIZATION);
+
if (NULL != auth)
{
/* We _only_ complain about malformed auth headers if
@@ -1910,7 +1902,10 @@ url_handler (void *cls,
because some reverse proxy is already doing it, and
then that reverse proxy may forward malformed auth
headers to the backend. */
- extract_token (&auth);
+ is_basic_auth = (0 == strncmp (auth,
+ "Basic ",
+ strlen ("Basic ")));
+ extract_auth (&auth);
if (NULL == auth)
auth_malformed = true;
hc->auth_token = auth;
@@ -1919,27 +1914,46 @@ url_handler (void *cls,
/* If we have zero configured instances (not even ones that have been
purged) AND no override credentials, THEN we accept anything (no access
control), as we then also have no data to protect. */
+ // FIXME this must somehow carry over to tokens
auth_ok = ( (0 ==
GNUNET_CONTAINER_multihashmap_size (TMH_by_id_map)) &&
(NULL == TMH_default_auth) );
- /* Check against selected instance, if we have one */
- if (NULL != hc->instance)
- auth_ok |= (GNUNET_OK ==
- TMH_check_auth (auth,
- &hc->instance->auth.auth_salt,
- &hc->instance->auth.auth_hash));
- else /* Are the credentials provided OK for CLI override? */
- auth_ok |= (use_default &&
- (NULL != TMH_default_auth) &&
- (NULL != auth) &&
- (! auth_malformed) &&
- (0 == strcmp (auth,
- TMH_default_auth)) );
- if (auth_ok)
- {
- hc->auth_scope = TMH_AS_ALL;
+ if (is_basic_auth)
+ {
+ /* Handle token endpoint slightly differently: Only allow
+ * instance password (Basic auth) OR
+ * refresh token (Bearer auth) to retrieve access token.
+ * We need to handle authorization with Basic auth here first
+ * The only time we need to handle authentication like this is
+ * for the token endpoint!
+ */
+ if ((0 != strncmp (hc->rh->url_prefix, "/token", strlen ("/token"))) ||
+ (0 != strncmp (MHD_HTTP_METHOD_POST,
+ hc->rh->method,
+ strlen (MHD_HTTP_METHOD_POST))))
+ {
+ // FIXME this should never happen, but according to the comment below,
+ // We must not error out here for some reason that has to do with
+ // disabled authZ behind reverse proxy...
+ }
+ /* Check against selected instance, if we have one */
+ if (NULL != hc->instance)
+ auth_ok |= (GNUNET_OK ==
+ TMH_check_auth_instance (auth,
+ hc->instance));
+ else /* Are the credentials provided OK for CLI override? */
+ auth_ok |= (use_default &&
+ (NULL != TMH_default_auth) &&
+ (NULL != auth) &&
+ (! auth_malformed) &&
+ (0 == strcmp (auth,
+ TMH_default_auth)) );
+ if (auth_ok)
+ {
+ hc->auth_scope = TMH_AS_ALL;
+ }
}
- else
+ else /* Check bearer token */
{
if (NULL != hc->instance)
{
@@ -2274,18 +2288,6 @@ run (void *cls,
if ( (NULL != tok) &&
(NULL == TMH_default_auth) )
TMH_default_auth = GNUNET_strdup (tok);
- if ( (NULL != TMH_default_auth) &&
- (0 != strncmp (TMH_default_auth,
- RFC_8959_PREFIX,
- strlen (RFC_8959_PREFIX))) )
- {
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Authentication token does not start with `%s' prefix\n",
- RFC_8959_PREFIX);
- global_ret = EXIT_NOTCONFIGURED;
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
cfg = config;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Starting taler-merchant-httpd\n");
diff --git a/src/backend/taler-merchant-httpd_helper.c b/src/backend/taler-merchant-httpd_helper.c
@@ -529,6 +529,70 @@ TMH_setup_wire_account (
}
+enum TALER_ErrorCode
+TMH_check_token (const char *token,
+ const char *instance_id,
+ enum TMH_AuthScope *as)
+{
+ enum TMH_AuthScope scope;
+ struct GNUNET_TIME_Timestamp expiration;
+ enum GNUNET_DB_QueryStatus qs;
+ struct TALER_MERCHANTDB_LoginTokenP btoken;
+
+ if (NULL == token)
+ {
+ *as = TMH_AS_NONE;
+ return TALER_EC_NONE;
+ }
+ if (0 != strncasecmp (token,
+ RFC_8959_PREFIX,
+ strlen (RFC_8959_PREFIX)))
+ {
+ *as = TMH_AS_NONE;
+ return TALER_EC_NONE;
+ }
+ token += strlen (RFC_8959_PREFIX);
+ if (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (token,
+ strlen (token),
+ &btoken,
+ sizeof (btoken)))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Given authorization token `%s' is malformed\n",
+ token);
+ GNUNET_break_op (0);
+ return TALER_EC_GENERIC_TOKEN_MALFORMED;
+ }
+ qs = TMH_db->select_login_token (TMH_db->cls,
+ instance_id,
+ &btoken,
+ &expiration,
+ &scope);
+ if (qs < 0)
+ {
+ GNUNET_break (0);
+ return TALER_EC_GENERIC_DB_FETCH_FAILED;
+ }
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Authorization token `%s' unknown\n",
+ token);
+ return TALER_EC_GENERIC_TOKEN_UNKNOWN;
+ }
+ if (GNUNET_TIME_absolute_is_past (expiration.abs_time))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Authorization token `%s' expired\n",
+ token);
+ return TALER_EC_GENERIC_TOKEN_EXPIRED;
+ }
+ *as = scope;
+ return TALER_EC_NONE;
+}
+
+
enum GNUNET_GenericReturnValue
TMH_check_auth_config (struct MHD_Connection *connection,
const json_t *jauth,
@@ -549,7 +613,7 @@ TMH_check_auth_config (struct MHD_Connection *connection,
auth_wellformed = true;
}
else if (0 == strcmp (auth_method,
- "token"))
+ "token")) // FIXME "password"
{
*auth_token = json_string_value (json_object_get (jauth,
"token"));
@@ -559,12 +623,14 @@ TMH_check_auth_config (struct MHD_Connection *connection,
}
else
{
+ // FIXME prettify
+ /*
if (0 != strncasecmp (RFC_8959_PREFIX,
*auth_token,
strlen (RFC_8959_PREFIX)))
GNUNET_break_op (0);
- else
- auth_wellformed = true;
+ else*/
+ auth_wellformed = true;
}
}
diff --git a/src/backend/taler-merchant-httpd_helper.h b/src/backend/taler-merchant-httpd_helper.h
@@ -274,5 +274,17 @@ TMH_exchange_accounts_by_method (
const struct TALER_MasterPublicKeyP *master_pub,
const char *wire_method);
+/**
+ * Check validity of login @a token for the given @a instance_id.
+ *
+ * @param token the login token given in the request
+ * @param instance_id the instance the login is to be checked against
+ * @param[out] as set to scope of the token if it is valid
+ * @return TALER_EC_NONE on success
+ */
+enum TALER_ErrorCode
+TMH_check_token (const char *token,
+ const char *instance_id,
+ enum TMH_AuthScope *as);
#endif
diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c b/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c
@@ -49,7 +49,7 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi,
struct TMH_HandlerContext *hc)
{
struct TALER_MERCHANTDB_InstanceAuthSettings ias;
- const char *auth_token = NULL;
+ const char *auth_pw = NULL;
json_t *jauth = hc->request_body;
{
@@ -57,12 +57,12 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi,
ret = TMH_check_auth_config (connection,
jauth,
- &auth_token);
+ &auth_pw);
if (GNUNET_OK != ret)
return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
}
- if (NULL == auth_token)
+ if (NULL == auth_pw)
{
memset (&ias.auth_salt,
0,
@@ -73,7 +73,7 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi,
}
else
{
- TMH_compute_auth (auth_token,
+ TMH_compute_auth (auth_pw,
&ias.auth_salt,
&ias.auth_hash);
}
@@ -102,6 +102,7 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi,
to the authentication. */
{
struct TALER_MERCHANTDB_InstanceAuthSettings db_ias;
+ enum TALER_ErrorCode ec;
qs = TMH_db->lookup_instance_auth (TMH_db->cls,
mi->settings.id,
@@ -131,19 +132,23 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi,
}
if ( (NULL == TMH_default_auth) &&
- (! mi->auth_override) &&
- (GNUNET_OK !=
- TMH_check_auth (hc->auth_token,
- &db_ias.auth_salt,
- &db_ias.auth_hash)) )
+ (! mi->auth_override))
{
- TMH_db->rollback (TMH_db->cls);
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Refusing auth change: old token does not match\n");
- return TALER_MHD_reply_with_error (connection,
- MHD_HTTP_UNAUTHORIZED,
- TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED,
- NULL);
+ // FIXME are we sure what the scope here is?
+ ec = TMH_check_token (hc->auth_token,
+ mi->settings.id,
+ &hc->auth_scope);
+ if (TALER_EC_NONE != ec)
+ {
+ TMH_db->rollback (TMH_db->cls);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Refusing auth change: `%s'\n",
+ TALER_ErrorCode_get_hint (ec));
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_UNAUTHORIZED,
+ TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED,
+ NULL);
+ }
}
}
diff --git a/src/merchant-tools/taler-merchant-passwd.c b/src/merchant-tools/taler-merchant-passwd.c
@@ -65,16 +65,6 @@ run (void *cls,
global_ret = -1;
return;
}
- if (0 != strncmp (pw,
- RFC_8959_PREFIX,
- strlen (RFC_8959_PREFIX)))
- {
- fprintf (stderr,
- "Invalid password specified, does not begin with `%s'\n",
- RFC_8959_PREFIX);
- global_ret = 1;
- return;
- }
if (NULL == instance)
instance = GNUNET_strdup ("admin");
cfg = GNUNET_CONFIGURATION_dup (config);
diff --git a/src/testing/test_merchant_instance_auth.sh b/src/testing/test_merchant_instance_auth.sh
@@ -41,7 +41,7 @@ echo -n "Configuring 'admin' instance ..." >&2
STATUS=$(curl -H "Content-Type: application/json" -X POST \
http://localhost:9966/management/instances \
- -d '{"auth":{"method":"token","token":"secret-token:new_value"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \
+ -d '{"auth":{"method":"token","token":"new_pw"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \
-w "%{http_code}" -s -o /dev/null)
if [ "$STATUS" != "204" ]
@@ -49,8 +49,24 @@ then
exit_fail "Expected 204, instance created. got: $STATUS" >&2
fi
+BASIC_AUTH=$(echo -n default:new_pw | base64)
+
STATUS=$(curl -H "Content-Type: application/json" -X POST \
- -H 'Authorization: Bearer secret-token:new_value' \
+ -H "Authorization: Basic $BASIC_AUTH" \
+ http://localhost:9966/private/token \
+ -d '{"scope":"write"}' \
+ -w "%{http_code}" -s -o $LAST_RESPONSE)
+
+
+if [ "$STATUS" != "200" ]
+then
+ exit_fail "Expected 200 OK. Got: $STATUS"
+fi
+
+TOKEN=$(jq -e -r .token < $LAST_RESPONSE)
+
+STATUS=$(curl -H "Content-Type: application/json" -X POST \
+ -H "Authorization: Bearer $TOKEN" \
http://localhost:9966/private/accounts \
-d '{"payto_uri":"payto://x-taler-bank/localhost:8082/43?receiver-name=user43"}' \
-w "%{http_code}" -s -o /dev/null)
@@ -73,7 +89,7 @@ setup -c test_template.conf \
-u "exchange-account-2" \
-r "merchant-exchange-default"
-NEW_SECRET=secret-token:different_value
+NEW_SECRET=different_value
taler-merchant-exchangekeyupdate \
-c "${CONF}" \
@@ -110,11 +126,27 @@ then
exit_fail "Failed to (re)start merchant backend"
fi
+BASIC_AUTH=$(echo -n default:$NEW_SECRET | base64)
+
+STATUS=$(curl -H "Content-Type: application/json" -X POST \
+ -H "Authorization: Basic $BASIC_AUTH" \
+ http://localhost:9966/private/token \
+ -d '{"scope":"write"}' \
+ -w "%{http_code}" -s -o $LAST_RESPONSE)
+
+
+if [ "$STATUS" != "200" ]
+then
+ exit_fail "Expected 200 OK. Got: $STATUS"
+fi
+
+TOKEN=$(jq -e -r .token < $LAST_RESPONSE)
+
echo -n "Creating order to test auth is ok..." >&2
STATUS=$(curl -H "Content-Type: application/json" -X POST \
'http://localhost:9966/private/orders' \
- -H 'Authorization: Bearer '"$NEW_SECRET" \
+ -H 'Authorization: Bearer '"$TOKEN" \
-d '{"order":{"amount":"TESTKUDOS:1","summary":"payme"}}' \
-w "%{http_code}" -s -o "$LAST_RESPONSE")
@@ -125,10 +157,10 @@ then
fi
ORDER_ID=$(jq -e -r .order_id < "$LAST_RESPONSE")
-TOKEN=$(jq -e -r .token < "$LAST_RESPONSE")
+ORD_TOKEN=$(jq -e -r .token < "$LAST_RESPONSE")
STATUS=$(curl "http://localhost:9966/private/orders/${ORDER_ID}" \
- -H 'Authorization: Bearer '"$NEW_SECRET" \
+ -H 'Authorization: Bearer '"$TOKEN" \
-w "%{http_code}" -s -o "$LAST_RESPONSE")
if [ "$STATUS" != "200" ]
@@ -139,12 +171,12 @@ fi
PAY_URL=$(jq -e -r .taler_pay_uri < "$LAST_RESPONSE")
-echo "OK order ${ORDER_ID} with ${TOKEN} and ${PAY_URL}" >&2
+echo "OK order ${ORDER_ID} with ${ORD_TOKEN} and ${PAY_URL}" >&2
echo -n "Configuring 'second' instance ..." >&2
STATUS=$(curl -H "Content-Type: application/json" -X POST \
- -H 'Authorization: Bearer '"$NEW_SECRET" \
+ -H 'Authorization: Bearer '"$TOKEN" \
http://localhost:9966/management/instances \
-d '{"auth":{"method":"token","token":"secret-token:second"},"id":"second","name":"second","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 3600000000},"default_pay_delay":{"d_us": 3600000000}}' \
-w "%{http_code}" -s -o /dev/null)
@@ -159,23 +191,24 @@ echo "OK" >&2
echo -n "Updating 'second' instance token using the 'new_one' auth token..." >&2
STATUS=$(curl -H "Content-Type: application/json" -X POST \
- -H 'Authorization: Bearer '"$NEW_SECRET" \
+ -H 'Authorization: Bearer '"$TOKEN" \
http://localhost:9966/management/instances/second/auth \
- -d '{"method":"token","token":"secret-token:new_one"}' \
+ -d '{"method":"token","token":"new_one"}' \
-w "%{http_code}" -s -o /dev/null)
if [ "$STATUS" != "204" ]
then
exit_fail "Expected 204, instance auth token changed. got: $STATUS"
fi
-NEW_SECRET="secret-token:new_one"
+NEW_SECRET="new_one"
echo " OK" >&2
+BASIC_AUTH2=$(echo -n second:$NEW_SECRET | base64)
echo -n "Requesting login token..." >&2
STATUS=$(curl -H "Content-Type: application/json" -X POST \
- -H 'Authorization: Bearer '"$NEW_SECRET" \
+ -H 'Authorization: Basic '"$BASIC_AUTH2" \
http://localhost:9966/instances/second/private/token \
-d '{"scope":"readonly","refreshable":true}' \
-w "%{http_code}" -s -o "$LAST_RESPONSE")