challenger

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

commit 604a14d9204d00dd246951523ebfc24f4f3a7422
parent 0ed94333a35a9f22c6b18c9cf96a0d9bde404ff6
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu,  6 Aug 2026 14:58:09 +0200

RFC 6750 requires 401 for both malformed and unknown tokens

Diffstat:
Msrc/challenger/challenger-httpd_info.c | 53+++++++++++++++++++++++++++++++++++++----------------
Msrc/challenger/meson.build | 1+
Asrc/challenger/test-challenger-auth-errors.sh | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 207 insertions(+), 16 deletions(-)

diff --git a/src/challenger/challenger-httpd_info.c b/src/challenger/challenger-httpd_info.c @@ -38,6 +38,29 @@ */ #define MAX_RETRIES 3 + +/** + * Reply to @a connection that the bearer token is not (or no longer) + * valid, as per RFC 6750 Section 3.1. Used both for tokens that do not + * even decode and for tokens that are simply unknown, so that the reply + * does not first reveal whether a guessed token is well-formed and only + * then whether it exists. + * + * @param connection connection to reply on + * @return MHD result code + */ +static enum MHD_Result +reply_invalid_token (struct MHD_Connection *connection) +{ + return CH_reply_with_oauth_error ( + connection, + MHD_HTTP_UNAUTHORIZED, + "invalid_token", + TALER_EC_CHALLENGER_GRANT_UNKNOWN, + NULL); +} + + enum MHD_Result CH_handler_info (struct CH_HandlerContext *hc, const char *upload_data, @@ -55,10 +78,12 @@ CH_handler_info (struct CH_HandlerContext *hc, if (NULL == auth) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (hc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_GENERIC_PARAMETER_MISSING, - MHD_HTTP_HEADER_AUTHORIZATION); + return CH_reply_with_oauth_error ( + hc->connection, + MHD_HTTP_UNAUTHORIZED, + "invalid_request", + TALER_EC_GENERIC_PARAMETER_MISSING, + MHD_HTTP_HEADER_AUTHORIZATION); } /* RFC 7235: the auth-scheme token ("Bearer") is case-insensitive. */ if (0 != strncasecmp (auth, @@ -66,10 +91,12 @@ CH_handler_info (struct CH_HandlerContext *hc, strlen (BEARER_PREFIX))) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (hc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - MHD_HTTP_HEADER_AUTHORIZATION); + return CH_reply_with_oauth_error ( + hc->connection, + MHD_HTTP_UNAUTHORIZED, + "invalid_request", + TALER_EC_GENERIC_PARAMETER_MALFORMED, + MHD_HTTP_HEADER_AUTHORIZATION); } token = auth + strlen (BEARER_PREFIX); @@ -80,10 +107,7 @@ CH_handler_info (struct CH_HandlerContext *hc, sizeof (grant))) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error (hc->connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - MHD_HTTP_HEADER_AUTHORIZATION); + return reply_invalid_token (hc->connection); } /* Check token is valid */ @@ -116,10 +140,7 @@ CH_handler_info (struct CH_HandlerContext *hc, TALER_EC_GENERIC_DB_FETCH_FAILED, "get_token"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - return TALER_MHD_reply_with_error (hc->connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_CHALLENGER_GRANT_UNKNOWN, - "get_token"); + return reply_invalid_token (hc->connection); case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } diff --git a/src/challenger/meson.build b/src/challenger/meson.build @@ -19,6 +19,7 @@ check_SCRIPTS = [ 'test-challenger-pinfail', 'test-challenger-resend', 'test-challenger-exhaustion', + 'test-challenger-auth-errors', ] test_helper_cat = configure_file(input: 'cat.sh', output: 'cat.sh', copy: true) diff --git a/src/challenger/test-challenger-auth-errors.sh b/src/challenger/test-challenger-auth-errors.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# This file is in the public domain. +# +# Regression test for finding 13: 'GET /info' must implement the RFC 6750 +# bearer-token error contract. +# +# A missing 'Authorization' header, a token that does not decode and a +# well-formed but unknown token were answered 403, 403 and 404 +# respectively, never 401, and never with a 'WWW-Authenticate' header -- +# so a conforming OAuth client cannot tell "refresh your token" from +# "resource gone", and the 403/404 split reveals whether a guessed token +# is well-formed before revealing whether it exists. +# +# All of these must now be 401 with a 'WWW-Authenticate: Bearer error=...' +# header, and the malformed and the unknown token must be answered +# identically. + +set -eu + +# Exit, with status code "skip" (no 'real' failure) +function exit_skip() { + echo " SKIP: $1" + exit 77 +} + +# Exit, with error message (hard failure) +function exit_fail() { + echo " FAIL: $@" + exit 1 +} + +# Cleanup to run whenever we exit +function cleanup() +{ + for n in $(jobs -p) + do + kill $n 2> /dev/null || true + done + rm -f "$LAST_RESPONSE" "$LAST_HEADERS" + wait +} + +LAST_RESPONSE=$(mktemp responseXXXXXX.log) +LAST_HEADERS=$(mktemp headersXXXXXX.log) + +# Install cleanup handler (except for kill -9) +trap cleanup EXIT + +export PATH="$PATH:." + +echo -n "Testing for jq" +jq -h > /dev/null || exit_skip "jq required" +echo " FOUND" +echo -n "Testing for curl" +curl -h > /dev/null || exit_skip "curl required" +echo " FOUND" +echo -n "Testing for wget" +wget -h > /dev/null || exit_skip "wget required" +echo " FOUND" +echo -n "Testing for challenger-httpd ..." +challenger-httpd -h > /dev/null || exit_skip "challenger-httpd required" +echo " FOUND" + +CONF="test-challenger.conf" +BURL="http://localhost:9967" +REDIRECT_URI="http://client.example.com/" +CLIENT_SECRET="secret-token:secret" +CLIENT_ID=1 + +echo -n "Initialize challenger database ..." +challenger-dbinit -r -c "${CONF}" &> dbinit.log +echo " OK" + +echo -n "Add challenger client ..." +challenger-admin -c "${CONF}" -a "${CLIENT_SECRET}" "${REDIRECT_URI}" &> admin.log +echo " OK" + +echo -n "Start challenger-httpd ..." +challenger-httpd -L INFO -c "${CONF}" &> httpd.log & + +# Wait for challenger to be available +for n in $(seq 1 50) +do + echo -n "." + sleep 0.2 + OK=0 + wget --tries=1 --timeout=1 "${BURL}/config" -o /dev/null -O /dev/null >/dev/null || continue + OK=1 + break +done +if [ 1 != $OK ] +then + exit_skip "Failed to launch challenger service" +fi +echo " OK" + +# Check that the last response carried a 'WWW-Authenticate: Bearer' header +# naming the given OAuth error. +function check_www_authenticate() { + if ! grep -qi "^WWW-Authenticate:" "$LAST_HEADERS" + then + exit_fail "missing WWW-Authenticate header: $(cat $LAST_HEADERS)" + fi + if ! grep -qi "^WWW-Authenticate:.*Bearer.*error=\"$1\"" "$LAST_HEADERS" + then + exit_fail "expected WWW-Authenticate 'Bearer error=\"$1\"'. Got: $(grep -i '^WWW-Authenticate:' $LAST_HEADERS)" + fi +} + +echo -n "/info without an Authorization header ..." +STATUS=$(curl "${BURL}/info" \ + -D "$LAST_HEADERS" \ + -w "%{http_code}" -s -o $LAST_RESPONSE) +if [ "$STATUS" != "401" ] +then + exit_fail "Expected 401 Unauthorized. Got: $STATUS" $(cat $LAST_RESPONSE) +fi +check_www_authenticate "invalid_request" +echo " OK" + +echo -n "/info with a non-Bearer Authorization header ..." +STATUS=$(curl "${BURL}/info" \ + -H "Authorization: Basic dXNlcjpwYXNz" \ + -D "$LAST_HEADERS" \ + -w "%{http_code}" -s -o $LAST_RESPONSE) +if [ "$STATUS" != "401" ] +then + exit_fail "Expected 401 Unauthorized. Got: $STATUS" $(cat $LAST_RESPONSE) +fi +check_www_authenticate "invalid_request" +echo " OK" + +echo -n "/info with a token that does not decode ..." +STATUS=$(curl "${BURL}/info" \ + -H "Authorization: Bearer not-a-base32-token" \ + -D "$LAST_HEADERS" \ + -w "%{http_code}" -s -o $LAST_RESPONSE) +if [ "$STATUS" != "401" ] +then + exit_fail "Expected 401 Unauthorized. Got: $STATUS" $(cat $LAST_RESPONSE) +fi +check_www_authenticate "invalid_token" +MALFORMED_STATUS="$STATUS" +MALFORMED_BODY=$(cat "$LAST_RESPONSE") +echo " OK" + +echo -n "/info with a well-formed but unknown token ..." +# 32 zero bytes in GNUnet's base32 alphabet +UNKNOWN_TOKEN=$(printf '0%.0s' $(seq 1 52)) +STATUS=$(curl "${BURL}/info" \ + -H "Authorization: Bearer ${UNKNOWN_TOKEN}" \ + -D "$LAST_HEADERS" \ + -w "%{http_code}" -s -o $LAST_RESPONSE) +if [ "$STATUS" != "401" ] +then + exit_fail "Expected 401 Unauthorized. Got: $STATUS" $(cat $LAST_RESPONSE) +fi +check_www_authenticate "invalid_token" +echo " OK" + +echo -n "Malformed and unknown tokens must be indistinguishable ..." +if [ "$STATUS" != "$MALFORMED_STATUS" ] || \ + [ "$(cat $LAST_RESPONSE)" != "$MALFORMED_BODY" ] +then + exit_fail "a malformed token is answered differently from an unknown one: '$MALFORMED_BODY' vs '$(cat $LAST_RESPONSE)'" +fi +echo " OK" + +exit 0