{"thread":{"id":"65492","subject":"[PATCH 0/3] http: fix emptyAuth=auto for Negotiate/SPNEGO","startedAt":"2026-04-16T09:21:02Z","lastAt":"2026-04-30T10:54:41Z","messageCount":13,"participants":["Matthew John Cheetham via GitGitGadget","Junio C Hamano","Matthew John Cheetham"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"541716","messageId":"pull.2087.git.1776331259.gitgitgadget@gmail.com","threadId":"65492","inReplyTo":null,"subject":"[PATCH 0/3] http: fix emptyAuth=auto for Negotiate/SPNEGO","fromName":"Matthew John Cheetham via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-16T09:20:56Z","receivedAt":"2026-04-16T09:21:02Z","isPatch":true,"body":"When a server advertises Negotiate (SPNEGO) authentication alongside Basic,\nthe \"auto\" mode of http.emptyAuth should allow libcurl to attempt Kerberos\nauthentication using the system ticket cache before falling back to\ncredential_fill(). Currently this never happens due to an interaction\nbetween two older features.\n\nThe Negotiate-stripping logic from 4dbe66464b (remote-curl: fall back to\nBasic auth if Negotiate fails, 2015-01-08) removes CURLAUTH_GSSNEGOTIATE on\nthe first 401, before the auto-detection from 40a18fc77c (http: add an\n\"auto\" mode for http.emptyauth, 2017-02-25) gets a chance to see it as an\n\"exotic\" method. The result is that auto mode silently degrades to the same\nbehavior as emptyAuth=false for any server whose only non-Basic/Digest\nmethod is Negotiate, forcing Kerberos users to manually set\nhttp.emptyAuth=true to get seamless ticket-based authentication.\n\nThis series fixes the interaction by delaying the Negotiate stripping in\nauto mode by one round-trip, giving empty auth a chance to use the system\nKerberos ticket. If there is no valid ticket, Negotiate is stripped on the\nsecond 401 and we fall through to credential_fill() as before. The true and\nfalse modes are unchanged.\n\nPatch 1: Extract a http_reauth_prepare() helper from the three retry paths\nthat call credential_fill() on HTTP_REAUTH. Pure refactor, no behavior\nchange.\n\nPatch 2: Delay the GSSNEGOTIATE stripping in auto mode and teach\nhttp_reauth_prepare() to skip credential_fill() when empty auth should be\nattempted first.\n\nPatch 3: Add tests verifying that auto mode produces an extra round-trip\n(empty auth attempt) compared to false mode, using the existing\nnph-custom-auth.sh CGI infrastructure.\n\nThere is a trade-off in auto mode: when a server advertises Negotiate but\nthe client has no valid Kerberos ticket, there is one extra round-trip\ncompared to the current behavior. This matches the trade-off already\ndocumented in 40a18fc77c. Users who want to avoid it can set\nhttp.emptyAuth=false.\n\nNote: this patch series was taken early into Git for Windows for the\n2.54.0-rc2 release.\nhttps://github.com/git-for-windows/git/commit/8e94b65c003783d7d7b09d9fccdf06a1363e347c\n\nMatthew John Cheetham (3):\n  http: extract http_reauth_prepare() from retry paths\n  http: attempt Negotiate auth in http.emptyAuth=auto mode\n  t5563: add tests for http.emptyAuth with Negotiate\n\n http.c                      | 32 +++++++++++++++-\n http.h                      |  6 +++\n remote-curl.c               |  4 +-\n t/t5563-simple-http-auth.sh | 74 +++++++++++++++++++++++++++++++++++++\n 4 files changed, 112 insertions(+), 4 deletions(-)\n\n\nbase-commit: 2b39a27d40682c09ac1c031f099ee602061597cd\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2087%2Fmjcheetham%2Fspnego-fix-upstream-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2087/mjcheetham/spnego-fix-upstream-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2087\n-- \ngitgitgadget\n"},{"id":"541717","messageId":"49488cc7d44404b9af55859dd892427bc8ee9142.1776331259.git.gitgitgadget@gmail.com","threadId":"65492","inReplyTo":"pull.2087.git.1776331259.gitgitgadget@gmail.com","subject":"[PATCH 1/3] http: extract http_reauth_prepare() from retry paths","fromName":"Matthew John Cheetham via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-16T09:20:57Z","receivedAt":"2026-04-16T09:21:03Z","isPatch":true,"body":"From: Matthew John Cheetham <mjcheetham@outlook.com>\n\nAll three HTTP retry paths (http_request_recoverable, post_rpc,\nprobe_rpc) call credential_fill() directly when handling\nHTTP_REAUTH. Extract this into a helper function so that a\nsubsequent commit can add pre-fill logic (such as attempting\nempty-auth before prompting) in one place.\n\nNo functional change.\n\nSigned-off-by: Matthew John Cheetham <mjcheetham@outlook.com>\n---\n http.c        | 7 ++++++-\n http.h        | 6 ++++++\n remote-curl.c | 4 ++--\n 3 files changed, 14 insertions(+), 3 deletions(-)\n\ndiff --git a/http.c b/http.c\nindex d8d016891b..f208e0ad82 100644\n--- a/http.c\n+++ b/http.c\n@@ -665,6 +665,11 @@ static void init_curl_http_auth(CURL *result)\n \t}\n }\n \n+void http_reauth_prepare(int all_capabilities)\n+{\n+\tcredential_fill(the_repository, &http_auth, all_capabilities);\n+}\n+\n /* *var must be free-able */\n static void var_override(char **var, char *value)\n {\n@@ -2398,7 +2403,7 @@ static int http_request_recoverable(const char *url,\n \t\t\t\tsleep(retry_delay);\n \t\t\t}\n \t\t} else if (ret == HTTP_REAUTH) {\n-\t\t\tcredential_fill(the_repository, &http_auth, 1);\n+\t\t\thttp_reauth_prepare(1);\n \t\t}\n \n \t\tret = http_request(url, result, target, options);\ndiff --git a/http.h b/http.h\nindex f9ee888c3e..729c51904d 100644\n--- a/http.h\n+++ b/http.h\n@@ -76,6 +76,12 @@ extern int http_is_verbose;\n extern ssize_t http_post_buffer;\n extern struct credential http_auth;\n \n+/**\n+ * Prepare for an HTTP re-authentication retry. This fills credentials\n+ * via credential_fill() so the next request can include them.\n+ */\n+void http_reauth_prepare(int all_capabilities);\n+\n extern char curl_errorstr[CURL_ERROR_SIZE];\n \n enum http_follow_config {\ndiff --git a/remote-curl.c b/remote-curl.c\nindex aba60d5712..affdb880f7 100644\n--- a/remote-curl.c\n+++ b/remote-curl.c\n@@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece\n \t\tdo {\n \t\t\terr = probe_rpc(rpc, &results);\n \t\t\tif (err == HTTP_REAUTH)\n-\t\t\t\tcredential_fill(the_repository, &http_auth, 0);\n+\t\t\t\thttp_reauth_prepare(0);\n \t\t} while (err == HTTP_REAUTH);\n \t\tif (err != HTTP_OK)\n \t\t\treturn -1;\n@@ -1068,7 +1068,7 @@ retry:\n \trpc->any_written = 0;\n \terr = run_slot(slot, NULL);\n \tif (err == HTTP_REAUTH && !large_request) {\n-\t\tcredential_fill(the_repository, &http_auth, 0);\n+\t\thttp_reauth_prepare(0);\n \t\tcurl_slist_free_all(headers);\n \t\tgoto retry;\n \t}\n-- \ngitgitgadget\n\n"},{"id":"541718","messageId":"f175294459c9370ed79c8338d6008b69c2028f99.1776331259.git.gitgitgadget@gmail.com","threadId":"65492","inReplyTo":"pull.2087.git.1776331259.gitgitgadget@gmail.com","subject":"[PATCH 2/3] http: attempt Negotiate auth in http.emptyAuth=auto mode","fromName":"Matthew John Cheetham via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-16T09:20:58Z","receivedAt":"2026-04-16T09:21:04Z","isPatch":true,"body":"From: Matthew John Cheetham <mjcheetham@outlook.com>\n\nWhen a server advertises Negotiate (SPNEGO) authentication, the\n\"auto\" mode of http.emptyAuth should detect this as an \"exotic\"\nmethod and proactively send empty credentials, allowing libcurl to\nuse the system Kerberos ticket without prompting the user.\n\nHowever, two features interact to prevent this from working:\n\nThe Negotiate-stripping logic, introduced in 4dbe66464b\n(remote-curl: fall back to Basic auth if Negotiate fails,\n2015-01-08), removes CURLAUTH_GSSNEGOTIATE from the allowed\nmethods on the first 401 response. The empty-auth auto-detection,\nintroduced in 40a18fc77c (http: add an \"auto\" mode for\nhttp.emptyauth, 2017-02-25), then checks the remaining methods\nfor anything \"exotic\" -- but Negotiate has already been removed,\nso auto mode never activates for servers whose only non-Basic/Digest\nmethod is Negotiate (e.g., Apache with mod_auth_kerb offering\nBasic + Negotiate).\n\nFix this by delaying the Negotiate stripping in auto mode: on the\nfirst 401, keep Negotiate in the allowed methods so that auto mode\ncan detect it and retry with empty credentials. If that attempt\nfails (no valid Kerberos ticket), strip Negotiate on the second 401\nand fall through to credential_fill() as usual.\n\nTo support this, also teach http_reauth_prepare() to skip\ncredential_fill() when empty auth is about to be attempted, since\nfilling real credentials would bypass the empty-auth mechanism.\n\nThe true and false modes are unchanged: true sends empty credentials\non the very first request (before any 401), and false never sends\nthem.\n\nSigned-off-by: Matthew John Cheetham <mjcheetham@outlook.com>\n---\n http.c | 25 ++++++++++++++++++++++++-\n 1 file changed, 24 insertions(+), 1 deletion(-)\n\ndiff --git a/http.c b/http.c\nindex f208e0ad82..1c7ea32ef2 100644\n--- a/http.c\n+++ b/http.c\n@@ -138,6 +138,7 @@ static unsigned long empty_auth_useless =\n \tCURLAUTH_BASIC\n \t| CURLAUTH_DIGEST_IE\n \t| CURLAUTH_DIGEST;\n+static int empty_auth_try_negotiate;\n \n static struct curl_slist *pragma_header;\n static struct string_list extra_http_headers = STRING_LIST_INIT_DUP;\n@@ -667,6 +668,17 @@ static void init_curl_http_auth(CURL *result)\n \n void http_reauth_prepare(int all_capabilities)\n {\n+\t/*\n+\t * If we deferred stripping Negotiate to give empty auth a\n+\t * chance (auto mode), skip credential_fill on this retry so\n+\t * that init_curl_http_auth() sends empty credentials and\n+\t * libcurl can attempt Negotiate with the system ticket cache.\n+\t */\n+\tif (empty_auth_try_negotiate &&\n+\t    !http_auth.password && !http_auth.credential &&\n+\t    (http_auth_methods & CURLAUTH_GSSNEGOTIATE))\n+\t\treturn;\n+\n \tcredential_fill(the_repository, &http_auth, all_capabilities);\n }\n \n@@ -1895,7 +1907,18 @@ static int handle_curl_result(struct slot_results *results)\n \t\t\t\thttp_proactive_auth = PROACTIVE_AUTH_NONE;\n \t\t\treturn HTTP_NOAUTH;\n \t\t} else {\n-\t\t\thttp_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;\n+\t\t\tif (curl_empty_auth == -1 &&\n+\t\t\t    !empty_auth_try_negotiate &&\n+\t\t\t    (results->auth_avail & CURLAUTH_GSSNEGOTIATE)) {\n+\t\t\t\t/*\n+\t\t\t\t * In auto mode, give Negotiate a chance via\n+\t\t\t\t * empty auth before stripping it. If it fails,\n+\t\t\t\t * we will strip it on the next 401.\n+\t\t\t\t */\n+\t\t\t\tempty_auth_try_negotiate = 1;\n+\t\t\t} else {\n+\t\t\t\thttp_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;\n+\t\t\t}\n \t\t\tif (results->auth_avail) {\n \t\t\t\thttp_auth_methods &= results->auth_avail;\n \t\t\t\thttp_auth_methods_restricted = 1;\n-- \ngitgitgadget\n\n"},{"id":"541719","messageId":"650acab79ef5e45b6835b523a37cde184ad60e04.1776331259.git.gitgitgadget@gmail.com","threadId":"65492","inReplyTo":"pull.2087.git.1776331259.gitgitgadget@gmail.com","subject":"[PATCH 3/3] t5563: add tests for http.emptyAuth with Negotiate","fromName":"Matthew John Cheetham via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-16T09:20:59Z","receivedAt":"2026-04-16T09:21:06Z","isPatch":true,"body":"From: Matthew John Cheetham <mjcheetham@outlook.com>\n\nAdd tests exercising the interaction between http.emptyAuth and\nservers that advertise Negotiate (SPNEGO) authentication.\n\nVerify that auto mode gives Negotiate a chance via empty auth\n(resulting in two 401 responses before falling through to\ncredential_fill with Basic credentials), and that false mode\nstrips Negotiate immediately (only one 401 response).\n\nSigned-off-by: Matthew John Cheetham <mjcheetham@outlook.com>\n---\n t/t5563-simple-http-auth.sh | 74 +++++++++++++++++++++++++++++++++++++\n 1 file changed, 74 insertions(+)\n\ndiff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh\nindex 0063581615..a7d475dd68 100755\n--- a/t/t5563-simple-http-auth.sh\n+++ b/t/t5563-simple-http-auth.sh\n@@ -719,4 +719,78 @@ test_expect_success 'access using three-legged auth' '\n \tEOF\n '\n \n+test_lazy_prereq SPNEGO 'curl --version | grep -qi \"SPNEGO\\|GSS-API\\|Kerberos\\|negotiate\"'\n+\n+test_expect_success SPNEGO 'http.emptyAuth=auto attempts Negotiate before credential_fill' '\n+\ttest_when_finished \"per_test_cleanup\" &&\n+\n+\tset_credential_reply get <<-EOF &&\n+\tusername=alice\n+\tpassword=secret-passwd\n+\tEOF\n+\n+\t# Basic base64(alice:secret-passwd)\n+\tcat >\"$HTTPD_ROOT_PATH/custom-auth.valid\" <<-EOF &&\n+\tid=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==\n+\tEOF\n+\n+\tcat >\"$HTTPD_ROOT_PATH/custom-auth.challenge\" <<-EOF &&\n+\tid=1 status=200\n+\tid=default response=WWW-Authenticate: Negotiate\n+\tid=default response=WWW-Authenticate: Basic realm=\"example.com\"\n+\tEOF\n+\n+\ttest_config_global credential.helper test-helper &&\n+\tGIT_TRACE_CURL=\"$TRASH_DIRECTORY/trace-auto\" \\\n+\t\tgit -c http.emptyAuth=auto \\\n+\t\tls-remote \"$HTTPD_URL/custom_auth/repo.git\" &&\n+\n+\t# In auto mode with a Negotiate+Basic server, there should be\n+\t# three 401 responses: (1) initial no-auth request, (2) empty-auth\n+\t# retry where Negotiate fails (no Kerberos ticket), (3) libcurl\n+\t# internal Negotiate retry. The fourth attempt uses Basic\n+\t# credentials from credential_fill and succeeds.\n+\tgrep \"HTTP/[0-9.]* 401\" \"$TRASH_DIRECTORY/trace-auto\" >actual_401s &&\n+\ttest_line_count = 3 actual_401s &&\n+\n+\texpect_credential_query get <<-EOF\n+\tcapability[]=authtype\n+\tcapability[]=state\n+\tprotocol=http\n+\thost=$HTTPD_DEST\n+\twwwauth[]=Negotiate\n+\twwwauth[]=Basic realm=\"example.com\"\n+\tEOF\n+'\n+\n+test_expect_success SPNEGO 'http.emptyAuth=false skips Negotiate' '\n+\ttest_when_finished \"per_test_cleanup\" &&\n+\n+\tset_credential_reply get <<-EOF &&\n+\tusername=alice\n+\tpassword=secret-passwd\n+\tEOF\n+\n+\t# Basic base64(alice:secret-passwd)\n+\tcat >\"$HTTPD_ROOT_PATH/custom-auth.valid\" <<-EOF &&\n+\tid=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==\n+\tEOF\n+\n+\tcat >\"$HTTPD_ROOT_PATH/custom-auth.challenge\" <<-EOF &&\n+\tid=1 status=200\n+\tid=default response=WWW-Authenticate: Negotiate\n+\tid=default response=WWW-Authenticate: Basic realm=\"example.com\"\n+\tEOF\n+\n+\ttest_config_global credential.helper test-helper &&\n+\tGIT_TRACE_CURL=\"$TRASH_DIRECTORY/trace-false\" \\\n+\t\tgit -c http.emptyAuth=false \\\n+\t\tls-remote \"$HTTPD_URL/custom_auth/repo.git\" &&\n+\n+\t# With emptyAuth=false, Negotiate is stripped immediately and\n+\t# credential_fill is called right away. Only one 401 response.\n+\tgrep \"HTTP/[0-9.]* 401\" \"$TRASH_DIRECTORY/trace-false\" >actual_401s &&\n+\ttest_line_count = 1 actual_401s\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"541750","messageId":"xmqqecke3mgr.fsf@gitster.g","threadId":"65492","inReplyTo":"49488cc7d44404b9af55859dd892427bc8ee9142.1776331259.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 1/3] http: extract http_reauth_prepare() from retry paths","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-16T16:21:24Z","receivedAt":"2026-04-16T16:21:26Z","isPatch":true,"body":"\"Matthew John Cheetham via GitGitGadget\" <gitgitgadget@gmail.com>\nwrites:\n\n> From: Matthew John Cheetham <mjcheetham@outlook.com>\n>\n> All three HTTP retry paths (http_request_recoverable, post_rpc,\n> probe_rpc) call credential_fill() directly when handling\n> HTTP_REAUTH. Extract this into a helper function so that a\n> subsequent commit can add pre-fill logic (such as attempting\n> empty-auth before prompting) in one place.\n>\n> No functional change.\n>\n> Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>\n> ---\n>  http.c        | 7 ++++++-\n>  http.h        | 6 ++++++\n>  remote-curl.c | 4 ++--\n>  3 files changed, 14 insertions(+), 3 deletions(-)\n\nNeat.\n\n>\n> diff --git a/http.c b/http.c\n> index d8d016891b..f208e0ad82 100644\n> --- a/http.c\n> +++ b/http.c\n> @@ -665,6 +665,11 @@ static void init_curl_http_auth(CURL *result)\n>  \t}\n>  }\n>  \n> +void http_reauth_prepare(int all_capabilities)\n> +{\n> +\tcredential_fill(the_repository, &http_auth, all_capabilities);\n> +}\n> +\n>  /* *var must be free-able */\n>  static void var_override(char **var, char *value)\n>  {\n> @@ -2398,7 +2403,7 @@ static int http_request_recoverable(const char *url,\n>  \t\t\t\tsleep(retry_delay);\n>  \t\t\t}\n>  \t\t} else if (ret == HTTP_REAUTH) {\n> -\t\t\tcredential_fill(the_repository, &http_auth, 1);\n> +\t\t\thttp_reauth_prepare(1);\n>  \t\t}\n>  \n>  \t\tret = http_request(url, result, target, options);\n> diff --git a/http.h b/http.h\n> index f9ee888c3e..729c51904d 100644\n> --- a/http.h\n> +++ b/http.h\n> @@ -76,6 +76,12 @@ extern int http_is_verbose;\n>  extern ssize_t http_post_buffer;\n>  extern struct credential http_auth;\n>  \n> +/**\n> + * Prepare for an HTTP re-authentication retry. This fills credentials\n> + * via credential_fill() so the next request can include them.\n> + */\n> +void http_reauth_prepare(int all_capabilities);\n> +\n>  extern char curl_errorstr[CURL_ERROR_SIZE];\n>  \n>  enum http_follow_config {\n> diff --git a/remote-curl.c b/remote-curl.c\n> index aba60d5712..affdb880f7 100644\n> --- a/remote-curl.c\n> +++ b/remote-curl.c\n> @@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece\n>  \t\tdo {\n>  \t\t\terr = probe_rpc(rpc, &results);\n>  \t\t\tif (err == HTTP_REAUTH)\n> -\t\t\t\tcredential_fill(the_repository, &http_auth, 0);\n> +\t\t\t\thttp_reauth_prepare(0);\n>  \t\t} while (err == HTTP_REAUTH);\n>  \t\tif (err != HTTP_OK)\n>  \t\t\treturn -1;\n> @@ -1068,7 +1068,7 @@ retry:\n>  \trpc->any_written = 0;\n>  \terr = run_slot(slot, NULL);\n>  \tif (err == HTTP_REAUTH && !large_request) {\n> -\t\tcredential_fill(the_repository, &http_auth, 0);\n> +\t\thttp_reauth_prepare(0);\n>  \t\tcurl_slist_free_all(headers);\n>  \t\tgoto retry;\n>  \t}\n"},{"id":"541755","messageId":"xmqq7bq63lll.fsf@gitster.g","threadId":"65492","inReplyTo":"f175294459c9370ed79c8338d6008b69c2028f99.1776331259.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/3] http: attempt Negotiate auth in http.emptyAuth=auto mode","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-16T16:40:06Z","receivedAt":"2026-04-16T16:40:09Z","isPatch":true,"body":"\"Matthew John Cheetham via GitGitGadget\" <gitgitgadget@gmail.com>\nwrites:\n\n> From: Matthew John Cheetham <mjcheetham@outlook.com>\n>\n> When a server advertises Negotiate (SPNEGO) authentication, the\n> \"auto\" mode of http.emptyAuth should detect this as an \"exotic\"\n> method and proactively send empty credentials, allowing libcurl to\n> use the system Kerberos ticket without prompting the user.\n>\n> However, two features interact to prevent this from working:\n>\n> The Negotiate-stripping logic, introduced in 4dbe66464b\n> (remote-curl: fall back to Basic auth if Negotiate fails,\n> 2015-01-08), removes CURLAUTH_GSSNEGOTIATE from the allowed\n> methods on the first 401 response. The empty-auth auto-detection,\n> introduced in 40a18fc77c (http: add an \"auto\" mode for\n> http.emptyauth, 2017-02-25), then checks the remaining methods\n> for anything \"exotic\" -- but Negotiate has already been removed,\n> so auto mode never activates for servers whose only non-Basic/Digest\n> method is Negotiate (e.g., Apache with mod_auth_kerb offering\n> Basic + Negotiate).\n\nWell explained.\n\n> Fix this by delaying the Negotiate stripping in auto mode: on the\n> first 401, keep Negotiate in the allowed methods so that auto mode\n> can detect it and retry with empty credentials. If that attempt\n> fails (no valid Kerberos ticket), strip Negotiate on the second 401\n> and fall through to credential_fill() as usual.\n\nOK, succeeding after two attempts is much better than failing after\nonly one attempt.\n\n> To support this, also teach http_reauth_prepare() to skip\n> credential_fill() when empty auth is about to be attempted, since\n> filling real credentials would bypass the empty-auth mechanism.\n\nAnd that is why the previous step shines.  Very neat.\n\n> The true and false modes are unchanged: true sends empty credentials\n> on the very first request (before any 401), and false never sends\n> them.\n\nOK.  This is a tangent, but \"git config --help\" on \"http.emptyAuth\"\nis horrible.  It does not say what the allowed values are, so I had\nto first write \"There are million other things in the system that\nthis patch does not modify, so what's the point of singling out\nthese two settings and saying that this patch does not change\nthem?\", before realizing that 'auto' the patch (and the explanation\nof the \"empty-autho auto-detction\" above) is about the third\npossiblity of the same variable and take it back.\n\n> Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>\n> ---\n>  http.c | 25 ++++++++++++++++++++++++-\n>  1 file changed, 24 insertions(+), 1 deletion(-)\n>\n> diff --git a/http.c b/http.c\n> index f208e0ad82..1c7ea32ef2 100644\n> --- a/http.c\n> +++ b/http.c\n> @@ -138,6 +138,7 @@ static unsigned long empty_auth_useless =\n>  \tCURLAUTH_BASIC\n>  \t| CURLAUTH_DIGEST_IE\n>  \t| CURLAUTH_DIGEST;\n> +static int empty_auth_try_negotiate;\n>  \n>  static struct curl_slist *pragma_header;\n>  static struct string_list extra_http_headers = STRING_LIST_INIT_DUP;\n\nI guess the existing code already assumes that we connect to a\nsingle destination, run a single \"session\", and then die, so it is\nin line with the existing design to have a file-scope global keep\ntrack of our \"state\".  In the longer run we may want to move these\nthings to a struct so that we can run multiple sessions without\nhaving to kill ourselves and restart, but that is totally outside\nthe topic of these patches to fix the negotiate auth.\n\n> @@ -667,6 +668,17 @@ static void init_curl_http_auth(CURL *result)\n>  \n>  void http_reauth_prepare(int all_capabilities)\n>  {\n> +\t/*\n> +\t * If we deferred stripping Negotiate to give empty auth a\n> +\t * chance (auto mode), skip credential_fill on this retry so\n> +\t * that init_curl_http_auth() sends empty credentials and\n> +\t * libcurl can attempt Negotiate with the system ticket cache.\n> +\t */\n> +\tif (empty_auth_try_negotiate &&\n> +\t    !http_auth.password && !http_auth.credential &&\n> +\t    (http_auth_methods & CURLAUTH_GSSNEGOTIATE))\n> +\t\treturn;\n> +\n>  \tcredential_fill(the_repository, &http_auth, all_capabilities);\n>  }\n>  \n> @@ -1895,7 +1907,18 @@ static int handle_curl_result(struct slot_results *results)\n>  \t\t\t\thttp_proactive_auth = PROACTIVE_AUTH_NONE;\n>  \t\t\treturn HTTP_NOAUTH;\n>  \t\t} else {\n> -\t\t\thttp_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;\n> +\t\t\tif (curl_empty_auth == -1 &&\n> +\t\t\t    !empty_auth_try_negotiate &&\n> +\t\t\t    (results->auth_avail & CURLAUTH_GSSNEGOTIATE)) {\n> +\t\t\t\t/*\n> +\t\t\t\t * In auto mode, give Negotiate a chance via\n> +\t\t\t\t * empty auth before stripping it. If it fails,\n> +\t\t\t\t * we will strip it on the next 401.\n> +\t\t\t\t */\n> +\t\t\t\tempty_auth_try_negotiate = 1;\n> +\t\t\t} else {\n> +\t\t\t\thttp_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;\n> +\t\t\t}\n>  \t\t\tif (results->auth_avail) {\n>  \t\t\t\thttp_auth_methods &= results->auth_avail;\n>  \t\t\t\thttp_auth_methods_restricted = 1;\n"},{"id":"542430","messageId":"VI0PR03MB11634FE845793CEA7D25FA2D0C0372@VI0PR03MB11634.eurprd03.prod.outlook.com","threadId":"65492","inReplyTo":"xmqq7bq63lll.fsf@gitster.g","subject":"Re: [PATCH 2/3] http: attempt Negotiate auth in http.emptyAuth=auto mode","fromName":"Matthew John Cheetham","fromEmail":"mjcheetham@outlook.com","sentAt":"2026-04-28T14:38:17Z","receivedAt":"2026-04-28T14:38:21Z","isPatch":true,"body":"On 2026-04-16 17:40, Junio C Hamano wrote:\n>> The true and false modes are unchanged: true sends empty credentials\n>> on the very first request (before any 401), and false never sends\n>> them.\n> \n> OK.  This is a tangent, but \"git config --help\" on \"http.emptyAuth\"\n> is horrible.  It does not say what the allowed values are, so I had\n> to first write \"There are million other things in the system that\n> this patch does not modify, so what's the point of singling out\n> these two settings and saying that this patch does not change\n> them?\", before realizing that 'auto' the patch (and the explanation\n> of the \"empty-autho auto-detction\" above) is about the third\n> possiblity of the same variable and take it back.\n\nAgreed - the existing description is pretty opaque about what values it\nactually takes. Should I add another patch to this series to spell out\nthe three values explicitly? How about something like this:\n\n      http.emptyAuth::\n              Attempt authentication without seeking a username or\n              password.  This can be used to attempt GSS-Negotiate\n              authentication without specifying a username in the URL,\n              as libcurl normally requires a username for\n              authentication. Possible values are:\n      +\n      --\n      * `auto` (default) - Send empty credentials only if the server's\n        401 response advertises an authentication mechanism that\n        requires them (such as GSS-Negotiate); otherwise fall back to\n        prompting via the credential helper.\n      * `true` - Always send empty credentials on the very first\n        request, before receiving any 401 response from the server.\n      * `false` - Never send empty credentials. Mechanisms that\n        require empty credentials, such as GSS-Negotiate, will not\n        work.\n      --\n\n  Does that read better?\n\n  Thanks,\n  Matthew\n\n"},{"id":"542524","messageId":"VI0PR03MB1163459D06AB474AD97D29E43C0352@VI0PR03MB11634.eurprd03.prod.outlook.com","threadId":"65492","inReplyTo":"xmqqse8dz4pi.fsf@gitster.g","subject":"Re: [PATCH 2/3] http: attempt Negotiate auth in http.emptyAuth=auto mode","fromName":"Matthew John Cheetham","fromEmail":"mjcheetham@outlook.com","sentAt":"2026-04-30T10:53:36Z","receivedAt":"2026-04-30T10:53:41Z","isPatch":true,"body":"[re-cc:ing the accidentially dropped mailing list]\n\nOn 2026-04-30 01:12, Junio C Hamano wrote:\n\n> Matthew John Cheetham <mjcheetham@outlook.com> writes:\n> \n>> Agreed - the existing description is pretty opaque about what values it\n>> actually takes. Should I add another patch to this series to spell out\n>> the three values explicitly? How about something like this:\n>>\n>>        http.emptyAuth::\n>>                Attempt authentication without seeking a username or\n>>                password.  This can be used to attempt GSS-Negotiate\n>>                authentication without specifying a username in the URL,\n>>                as libcurl normally requires a username for\n>>                authentication. Possible values are:\n>>        +\n>>        --\n>>        * `auto` (default) - Send empty credentials only if the server's\n>>          401 response advertises an authentication mechanism that\n>>          requires them (such as GSS-Negotiate); otherwise fall back to\n>>          prompting via the credential helper.\n>>        * `true` - Always send empty credentials on the very first\n>>          request, before receiving any 401 response from the server.\n>>        * `false` - Never send empty credentials. Mechanisms that\n>>          require empty credentials, such as GSS-Negotiate, will not\n>>          work.\n>>        --\n>>\n>>    Does that read better?\n> \n> Surely.  Thanks.\n\n\nSubmitted as v2\n\nThanks,\nMatthew\n\n"},{"id":"542525","messageId":"pull.2087.v2.git.1777546472.gitgitgadget@gmail.com","threadId":"65492","inReplyTo":"pull.2087.git.1776331259.gitgitgadget@gmail.com","subject":"[PATCH v2 0/4] http: fix emptyAuth=auto for Negotiate/SPNEGO","fromName":"Matthew John Cheetham via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-30T10:54:28Z","receivedAt":"2026-04-30T10:54:35Z","isPatch":true,"body":"When a server advertises Negotiate (SPNEGO) authentication alongside Basic,\nthe \"auto\" mode of http.emptyAuth should allow libcurl to attempt Kerberos\nauthentication using the system ticket cache before falling back to\ncredential_fill(). Currently this never happens due to an interaction\nbetween two older features.\n\nThe Negotiate-stripping logic from 4dbe66464b (remote-curl: fall back to\nBasic auth if Negotiate fails, 2015-01-08) removes CURLAUTH_GSSNEGOTIATE on\nthe first 401, before the auto-detection from 40a18fc77c (http: add an\n\"auto\" mode for http.emptyauth, 2017-02-25) gets a chance to see it as an\n\"exotic\" method. The result is that auto mode silently degrades to the same\nbehavior as emptyAuth=false for any server whose only non-Basic/Digest\nmethod is Negotiate, forcing Kerberos users to manually set\nhttp.emptyAuth=true to get seamless ticket-based authentication.\n\nThis series fixes the interaction by delaying the Negotiate stripping in\nauto mode by one round-trip, giving empty auth a chance to use the system\nKerberos ticket. If there is no valid ticket, Negotiate is stripped on the\nsecond 401 and we fall through to credential_fill() as before. The true and\nfalse modes are unchanged.\n\nPatch 1: Extract a http_reauth_prepare() helper from the three retry paths\nthat call credential_fill() on HTTP_REAUTH. Pure refactor, no behavior\nchange.\n\nPatch 2: Delay the GSSNEGOTIATE stripping in auto mode and teach\nhttp_reauth_prepare() to skip credential_fill() when empty auth should be\nattempted first.\n\nPatch 3: Add tests verifying that auto mode produces an extra round-trip\n(empty auth attempt) compared to false mode, using the existing\nnph-custom-auth.sh CGI infrastructure.\n\nPatch 4: Update http.emptyAuth documentation to clarify possible values\n(true, false, and auto).\n\nThere is a trade-off in auto mode: when a server advertises Negotiate but\nthe client has no valid Kerberos ticket, there is one extra round-trip\ncompared to the current behavior. This matches the trade-off already\ndocumented in 40a18fc77c. Users who want to avoid it can set\nhttp.emptyAuth=false.\n\nNote: this patch series was taken early into Git for Windows for the\n2.54.0-rc2 release.\nhttps://github.com/git-for-windows/git/commit/8e94b65c003783d7d7b09d9fccdf06a1363e347c\n\n----------------------------------------------------------------------------\n\nUpdate in v2:\n\n * Add patch 4 to clarify the available options for http.emptyAuth in the\n   config documentation.\n\nMatthew John Cheetham (4):\n  http: extract http_reauth_prepare() from retry paths\n  http: attempt Negotiate auth in http.emptyAuth=auto mode\n  t5563: add tests for http.emptyAuth with Negotiate\n  doc: clarify http.emptyAuth values\n\n Documentation/config/http.adoc | 13 +++++-\n http.c                         | 32 ++++++++++++++-\n http.h                         |  6 +++\n remote-curl.c                  |  4 +-\n t/t5563-simple-http-auth.sh    | 74 ++++++++++++++++++++++++++++++++++\n 5 files changed, 124 insertions(+), 5 deletions(-)\n\n\nbase-commit: 2b39a27d40682c09ac1c031f099ee602061597cd\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2087%2Fmjcheetham%2Fspnego-fix-upstream-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2087/mjcheetham/spnego-fix-upstream-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2087\n\nRange-diff vs v1:\n\n 1:  49488cc7d4 = 1:  49488cc7d4 http: extract http_reauth_prepare() from retry paths\n 2:  f175294459 = 2:  f175294459 http: attempt Negotiate auth in http.emptyAuth=auto mode\n 3:  650acab79e = 3:  650acab79e t5563: add tests for http.emptyAuth with Negotiate\n -:  ---------- > 4:  e0f236767f doc: clarify http.emptyAuth values\n\n-- \ngitgitgadget\n"},{"id":"542526","messageId":"49488cc7d44404b9af55859dd892427bc8ee9142.1777546472.git.gitgitgadget@gmail.com","threadId":"65492","inReplyTo":"pull.2087.v2.git.1777546472.gitgitgadget@gmail.com","subject":"[PATCH v2 1/4] http: extract http_reauth_prepare() from retry paths","fromName":"Matthew John Cheetham via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-30T10:54:29Z","receivedAt":"2026-04-30T10:54:37Z","isPatch":true,"body":"From: Matthew John Cheetham <mjcheetham@outlook.com>\n\nAll three HTTP retry paths (http_request_recoverable, post_rpc,\nprobe_rpc) call credential_fill() directly when handling\nHTTP_REAUTH. Extract this into a helper function so that a\nsubsequent commit can add pre-fill logic (such as attempting\nempty-auth before prompting) in one place.\n\nNo functional change.\n\nSigned-off-by: Matthew John Cheetham <mjcheetham@outlook.com>\n---\n http.c        | 7 ++++++-\n http.h        | 6 ++++++\n remote-curl.c | 4 ++--\n 3 files changed, 14 insertions(+), 3 deletions(-)\n\ndiff --git a/http.c b/http.c\nindex d8d016891b..f208e0ad82 100644\n--- a/http.c\n+++ b/http.c\n@@ -665,6 +665,11 @@ static void init_curl_http_auth(CURL *result)\n \t}\n }\n \n+void http_reauth_prepare(int all_capabilities)\n+{\n+\tcredential_fill(the_repository, &http_auth, all_capabilities);\n+}\n+\n /* *var must be free-able */\n static void var_override(char **var, char *value)\n {\n@@ -2398,7 +2403,7 @@ static int http_request_recoverable(const char *url,\n \t\t\t\tsleep(retry_delay);\n \t\t\t}\n \t\t} else if (ret == HTTP_REAUTH) {\n-\t\t\tcredential_fill(the_repository, &http_auth, 1);\n+\t\t\thttp_reauth_prepare(1);\n \t\t}\n \n \t\tret = http_request(url, result, target, options);\ndiff --git a/http.h b/http.h\nindex f9ee888c3e..729c51904d 100644\n--- a/http.h\n+++ b/http.h\n@@ -76,6 +76,12 @@ extern int http_is_verbose;\n extern ssize_t http_post_buffer;\n extern struct credential http_auth;\n \n+/**\n+ * Prepare for an HTTP re-authentication retry. This fills credentials\n+ * via credential_fill() so the next request can include them.\n+ */\n+void http_reauth_prepare(int all_capabilities);\n+\n extern char curl_errorstr[CURL_ERROR_SIZE];\n \n enum http_follow_config {\ndiff --git a/remote-curl.c b/remote-curl.c\nindex aba60d5712..affdb880f7 100644\n--- a/remote-curl.c\n+++ b/remote-curl.c\n@@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece\n \t\tdo {\n \t\t\terr = probe_rpc(rpc, &results);\n \t\t\tif (err == HTTP_REAUTH)\n-\t\t\t\tcredential_fill(the_repository, &http_auth, 0);\n+\t\t\t\thttp_reauth_prepare(0);\n \t\t} while (err == HTTP_REAUTH);\n \t\tif (err != HTTP_OK)\n \t\t\treturn -1;\n@@ -1068,7 +1068,7 @@ retry:\n \trpc->any_written = 0;\n \terr = run_slot(slot, NULL);\n \tif (err == HTTP_REAUTH && !large_request) {\n-\t\tcredential_fill(the_repository, &http_auth, 0);\n+\t\thttp_reauth_prepare(0);\n \t\tcurl_slist_free_all(headers);\n \t\tgoto retry;\n \t}\n-- \ngitgitgadget\n\n"},{"id":"542527","messageId":"f175294459c9370ed79c8338d6008b69c2028f99.1777546472.git.gitgitgadget@gmail.com","threadId":"65492","inReplyTo":"pull.2087.v2.git.1777546472.gitgitgadget@gmail.com","subject":"[PATCH v2 2/4] http: attempt Negotiate auth in http.emptyAuth=auto mode","fromName":"Matthew John Cheetham via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-30T10:54:30Z","receivedAt":"2026-04-30T10:54:38Z","isPatch":true,"body":"From: Matthew John Cheetham <mjcheetham@outlook.com>\n\nWhen a server advertises Negotiate (SPNEGO) authentication, the\n\"auto\" mode of http.emptyAuth should detect this as an \"exotic\"\nmethod and proactively send empty credentials, allowing libcurl to\nuse the system Kerberos ticket without prompting the user.\n\nHowever, two features interact to prevent this from working:\n\nThe Negotiate-stripping logic, introduced in 4dbe66464b\n(remote-curl: fall back to Basic auth if Negotiate fails,\n2015-01-08), removes CURLAUTH_GSSNEGOTIATE from the allowed\nmethods on the first 401 response. The empty-auth auto-detection,\nintroduced in 40a18fc77c (http: add an \"auto\" mode for\nhttp.emptyauth, 2017-02-25), then checks the remaining methods\nfor anything \"exotic\" -- but Negotiate has already been removed,\nso auto mode never activates for servers whose only non-Basic/Digest\nmethod is Negotiate (e.g., Apache with mod_auth_kerb offering\nBasic + Negotiate).\n\nFix this by delaying the Negotiate stripping in auto mode: on the\nfirst 401, keep Negotiate in the allowed methods so that auto mode\ncan detect it and retry with empty credentials. If that attempt\nfails (no valid Kerberos ticket), strip Negotiate on the second 401\nand fall through to credential_fill() as usual.\n\nTo support this, also teach http_reauth_prepare() to skip\ncredential_fill() when empty auth is about to be attempted, since\nfilling real credentials would bypass the empty-auth mechanism.\n\nThe true and false modes are unchanged: true sends empty credentials\non the very first request (before any 401), and false never sends\nthem.\n\nSigned-off-by: Matthew John Cheetham <mjcheetham@outlook.com>\n---\n http.c | 25 ++++++++++++++++++++++++-\n 1 file changed, 24 insertions(+), 1 deletion(-)\n\ndiff --git a/http.c b/http.c\nindex f208e0ad82..1c7ea32ef2 100644\n--- a/http.c\n+++ b/http.c\n@@ -138,6 +138,7 @@ static unsigned long empty_auth_useless =\n \tCURLAUTH_BASIC\n \t| CURLAUTH_DIGEST_IE\n \t| CURLAUTH_DIGEST;\n+static int empty_auth_try_negotiate;\n \n static struct curl_slist *pragma_header;\n static struct string_list extra_http_headers = STRING_LIST_INIT_DUP;\n@@ -667,6 +668,17 @@ static void init_curl_http_auth(CURL *result)\n \n void http_reauth_prepare(int all_capabilities)\n {\n+\t/*\n+\t * If we deferred stripping Negotiate to give empty auth a\n+\t * chance (auto mode), skip credential_fill on this retry so\n+\t * that init_curl_http_auth() sends empty credentials and\n+\t * libcurl can attempt Negotiate with the system ticket cache.\n+\t */\n+\tif (empty_auth_try_negotiate &&\n+\t    !http_auth.password && !http_auth.credential &&\n+\t    (http_auth_methods & CURLAUTH_GSSNEGOTIATE))\n+\t\treturn;\n+\n \tcredential_fill(the_repository, &http_auth, all_capabilities);\n }\n \n@@ -1895,7 +1907,18 @@ static int handle_curl_result(struct slot_results *results)\n \t\t\t\thttp_proactive_auth = PROACTIVE_AUTH_NONE;\n \t\t\treturn HTTP_NOAUTH;\n \t\t} else {\n-\t\t\thttp_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;\n+\t\t\tif (curl_empty_auth == -1 &&\n+\t\t\t    !empty_auth_try_negotiate &&\n+\t\t\t    (results->auth_avail & CURLAUTH_GSSNEGOTIATE)) {\n+\t\t\t\t/*\n+\t\t\t\t * In auto mode, give Negotiate a chance via\n+\t\t\t\t * empty auth before stripping it. If it fails,\n+\t\t\t\t * we will strip it on the next 401.\n+\t\t\t\t */\n+\t\t\t\tempty_auth_try_negotiate = 1;\n+\t\t\t} else {\n+\t\t\t\thttp_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;\n+\t\t\t}\n \t\t\tif (results->auth_avail) {\n \t\t\t\thttp_auth_methods &= results->auth_avail;\n \t\t\t\thttp_auth_methods_restricted = 1;\n-- \ngitgitgadget\n\n"},{"id":"542528","messageId":"650acab79ef5e45b6835b523a37cde184ad60e04.1777546472.git.gitgitgadget@gmail.com","threadId":"65492","inReplyTo":"pull.2087.v2.git.1777546472.gitgitgadget@gmail.com","subject":"[PATCH v2 3/4] t5563: add tests for http.emptyAuth with Negotiate","fromName":"Matthew John Cheetham via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-30T10:54:31Z","receivedAt":"2026-04-30T10:54:40Z","isPatch":true,"body":"From: Matthew John Cheetham <mjcheetham@outlook.com>\n\nAdd tests exercising the interaction between http.emptyAuth and\nservers that advertise Negotiate (SPNEGO) authentication.\n\nVerify that auto mode gives Negotiate a chance via empty auth\n(resulting in two 401 responses before falling through to\ncredential_fill with Basic credentials), and that false mode\nstrips Negotiate immediately (only one 401 response).\n\nSigned-off-by: Matthew John Cheetham <mjcheetham@outlook.com>\n---\n t/t5563-simple-http-auth.sh | 74 +++++++++++++++++++++++++++++++++++++\n 1 file changed, 74 insertions(+)\n\ndiff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh\nindex 0063581615..a7d475dd68 100755\n--- a/t/t5563-simple-http-auth.sh\n+++ b/t/t5563-simple-http-auth.sh\n@@ -719,4 +719,78 @@ test_expect_success 'access using three-legged auth' '\n \tEOF\n '\n \n+test_lazy_prereq SPNEGO 'curl --version | grep -qi \"SPNEGO\\|GSS-API\\|Kerberos\\|negotiate\"'\n+\n+test_expect_success SPNEGO 'http.emptyAuth=auto attempts Negotiate before credential_fill' '\n+\ttest_when_finished \"per_test_cleanup\" &&\n+\n+\tset_credential_reply get <<-EOF &&\n+\tusername=alice\n+\tpassword=secret-passwd\n+\tEOF\n+\n+\t# Basic base64(alice:secret-passwd)\n+\tcat >\"$HTTPD_ROOT_PATH/custom-auth.valid\" <<-EOF &&\n+\tid=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==\n+\tEOF\n+\n+\tcat >\"$HTTPD_ROOT_PATH/custom-auth.challenge\" <<-EOF &&\n+\tid=1 status=200\n+\tid=default response=WWW-Authenticate: Negotiate\n+\tid=default response=WWW-Authenticate: Basic realm=\"example.com\"\n+\tEOF\n+\n+\ttest_config_global credential.helper test-helper &&\n+\tGIT_TRACE_CURL=\"$TRASH_DIRECTORY/trace-auto\" \\\n+\t\tgit -c http.emptyAuth=auto \\\n+\t\tls-remote \"$HTTPD_URL/custom_auth/repo.git\" &&\n+\n+\t# In auto mode with a Negotiate+Basic server, there should be\n+\t# three 401 responses: (1) initial no-auth request, (2) empty-auth\n+\t# retry where Negotiate fails (no Kerberos ticket), (3) libcurl\n+\t# internal Negotiate retry. The fourth attempt uses Basic\n+\t# credentials from credential_fill and succeeds.\n+\tgrep \"HTTP/[0-9.]* 401\" \"$TRASH_DIRECTORY/trace-auto\" >actual_401s &&\n+\ttest_line_count = 3 actual_401s &&\n+\n+\texpect_credential_query get <<-EOF\n+\tcapability[]=authtype\n+\tcapability[]=state\n+\tprotocol=http\n+\thost=$HTTPD_DEST\n+\twwwauth[]=Negotiate\n+\twwwauth[]=Basic realm=\"example.com\"\n+\tEOF\n+'\n+\n+test_expect_success SPNEGO 'http.emptyAuth=false skips Negotiate' '\n+\ttest_when_finished \"per_test_cleanup\" &&\n+\n+\tset_credential_reply get <<-EOF &&\n+\tusername=alice\n+\tpassword=secret-passwd\n+\tEOF\n+\n+\t# Basic base64(alice:secret-passwd)\n+\tcat >\"$HTTPD_ROOT_PATH/custom-auth.valid\" <<-EOF &&\n+\tid=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==\n+\tEOF\n+\n+\tcat >\"$HTTPD_ROOT_PATH/custom-auth.challenge\" <<-EOF &&\n+\tid=1 status=200\n+\tid=default response=WWW-Authenticate: Negotiate\n+\tid=default response=WWW-Authenticate: Basic realm=\"example.com\"\n+\tEOF\n+\n+\ttest_config_global credential.helper test-helper &&\n+\tGIT_TRACE_CURL=\"$TRASH_DIRECTORY/trace-false\" \\\n+\t\tgit -c http.emptyAuth=false \\\n+\t\tls-remote \"$HTTPD_URL/custom_auth/repo.git\" &&\n+\n+\t# With emptyAuth=false, Negotiate is stripped immediately and\n+\t# credential_fill is called right away. Only one 401 response.\n+\tgrep \"HTTP/[0-9.]* 401\" \"$TRASH_DIRECTORY/trace-false\" >actual_401s &&\n+\ttest_line_count = 1 actual_401s\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"542529","messageId":"e0f236767f81ea60f90749d1bc00ab78081efd0e.1777546472.git.gitgitgadget@gmail.com","threadId":"65492","inReplyTo":"pull.2087.v2.git.1777546472.gitgitgadget@gmail.com","subject":"[PATCH v2 4/4] doc: clarify http.emptyAuth values","fromName":"Matthew John Cheetham via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-04-30T10:54:32Z","receivedAt":"2026-04-30T10:54:41Z","isPatch":true,"body":"From: Matthew John Cheetham <mjcheetham@outlook.com>\n\nThe existing description of http.emptyAuth explains the purpose of the\nsetting but never says what values it accepts. Readers have to infer\nfrom context (or read the source) that it takes 'true', 'false', or\n'auto', and what each one means.\n\nDocument the three accepted values explicitly:\n\n* 'auto' (the default) only sends empty credentials when the server's\n  401 response advertises a mechanism that requires them, such as\n  GSS-Negotiate. This matches the long-standing auto-detection\n  behaviour added in 40a18fc77c (http: add an \"auto\" mode for\n  http.emptyauth, 2017-02-25).\n\n* 'true' unconditionally sends empty credentials on the very first\n  request, before any 401 response, for callers that know they want\n  this behaviour up front.\n\n* 'false' disables the feature entirely; mechanisms that depend on\n  empty credentials, such as GSS-Negotiate, will not work in this\n  mode.\n\nSigned-off-by: Matthew John Cheetham <mjcheetham@outlook.com>\n---\n Documentation/config/http.adoc | 13 ++++++++++++-\n 1 file changed, 12 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc\nindex 849c89f36c..792a71b413 100644\n--- a/Documentation/config/http.adoc\n+++ b/Documentation/config/http.adoc\n@@ -59,7 +59,18 @@ http.emptyAuth::\n \tAttempt authentication without seeking a username or password.  This\n \tcan be used to attempt GSS-Negotiate authentication without specifying\n \ta username in the URL, as libcurl normally requires a username for\n-\tauthentication.\n+\tauthentication. Possible values are:\n++\n+--\n+* `auto` (default) - Send empty credentials only if the server's 401 response\n+  advertises an authentication mechanism that requires them (such as\n+  GSS-Negotiate); otherwise fall back to prompting via the credential helper.\n+* `true` - Always send empty credentials on the very first request, before\n+  receiving any 401 response from the server.\n+* `false` - Never send empty credentials. Mechanisms that require\n+  empty credentials or an explicit username, such as GSS-Negotiate, will not\n+  work.\n+--\n \n http.proactiveAuth::\n \tAttempt authentication without first making an unauthenticated attempt and\n-- \ngitgitgadget\n"}]}