Volume XXII, number 280Wednesday, October 7, 2026Latest message 1 hour ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

patch, 3 partshttp: fix emptyAuth=auto for Negotiate/SPNEGO

13 messages between Apr 16, 2026 and Apr 30, 2026, from Matthew John Cheetham via GitGitGadget, Junio C Hamano, Matthew John Cheetham.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Matthew John Cheetham via GitGitGadgetApr 16, 2026, 09:20 UTC on lore

When a server advertises Negotiate (SPNEGO) authentication alongside Basic, the "auto" mode of http.emptyAuth should allow libcurl to attempt Kerberos authentication using the system ticket cache before falling back to credential_fill(). Currently this never happens due to an interaction between two older features.

The Negotiate-stripping logic from 4dbe66464b (remote-curl: fall back to Basic auth if Negotiate fails, 2015-01-08) removes CURLAUTH_GSSNEGOTIATE on the first 401, before the auto-detection from 40a18fc77c (http: add an "auto" mode for http.emptyauth, 2017-02-25) gets a chance to see it as an "exotic" method. The result is that auto mode silently degrades to the same behavior as emptyAuth=false for any server whose only non-Basic/Digest method is Negotiate, forcing Kerberos users to manually set http.emptyAuth=true to get seamless ticket-based authentication.

This series fixes the interaction by delaying the Negotiate stripping in auto mode by one round-trip, giving empty auth a chance to use the system Kerberos ticket. If there is no valid ticket, Negotiate is stripped on the second 401 and we fall through to credential_fill() as before. The true and false modes are unchanged.

Patch 1: Extract a http_reauth_prepare() helper from the three retry paths that call credential_fill() on HTTP_REAUTH. Pure refactor, no behavior change.

Patch 2: Delay the GSSNEGOTIATE stripping in auto mode and teach http_reauth_prepare() to skip credential_fill() when empty auth should be attempted first.

Patch 3: Add tests verifying that auto mode produces an extra round-trip (empty auth attempt) compared to false mode, using the existing nph-custom-auth.sh CGI infrastructure.

There is a trade-off in auto mode: when a server advertises Negotiate but the client has no valid Kerberos ticket, there is one extra round-trip compared to the current behavior. This matches the trade-off already documented in 40a18fc77c. Users who want to avoid it can set http.emptyAuth=false.

Note: this patch series was taken early into Git for Windows for the
2.54.0-rc2 release.
https://github.com/git-for-windows/git/commit/8e94b65c003783d7d7b09d9fccdf06a1363e347c
Matthew John Cheetham (3):
  http: extract http_reauth_prepare() from retry paths
  http: attempt Negotiate auth in http.emptyAuth=auto mode
  t5563: add tests for http.emptyAuth with Negotiate
 http.c                      | 32 +++++++++++++++-
 http.h                      |  6 +++
 remote-curl.c               |  4 +-
 t/t5563-simple-http-auth.sh | 74 +++++++++++++++++++++++++++++++++++++
 4 files changed, 112 insertions(+), 4 deletions(-)
base-commit: 2b39a27d40682c09ac1c031f099ee602061597cd
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2087%2Fmjcheetham%2Fspnego-fix-upstream-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2087/mjcheetham/spnego-fix-upstream-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2087
-- 
gitgitgadget
Matthew John Cheetham via GitGitGadgetApr 16, 2026, 09:20 UTC in reply to Matthew John Cheetham via GitGitGadget on lore

[PATCH 1/3] http: extract http_reauth_prepare() from retry paths

From: Matthew John Cheetham <mjcheetham@outlook.com>

All three HTTP retry paths (http_request_recoverable, post_rpc, probe_rpc) call credential_fill() directly when handling HTTP_REAUTH. Extract this into a helper function so that a subsequent commit can add pre-fill logic (such as attempting empty-auth before prompting) in one place.

No functional change.
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
---
 http.c        | 7 ++++++-
 http.h        | 6 ++++++
 remote-curl.c | 4 ++--
 3 files changed, 14 insertions(+), 3 deletions(-)
Show changes to 3 files +14 −3

http.c, http.h, remote-curl.c

diff --git a/http.c b/http.c
index d8d016891b..f208e0ad82 100644
--- a/http.c
+++ b/http.c
@@ -665,6 +665,11 @@ static void init_curl_http_auth(CURL *result)
 	}
 }
 
+void http_reauth_prepare(int all_capabilities)
+{
+	credential_fill(the_repository, &http_auth, all_capabilities);
+}
+
 /* *var must be free-able */
 static void var_override(char **var, char *value)
 {
@@ -2398,7 +2403,7 @@ static int http_request_recoverable(const char *url,
 				sleep(retry_delay);
 			}
 		} else if (ret == HTTP_REAUTH) {
-			credential_fill(the_repository, &http_auth, 1);
+			http_reauth_prepare(1);
 		}
 
 		ret = http_request(url, result, target, options);
diff --git a/http.h b/http.h
index f9ee888c3e..729c51904d 100644
--- a/http.h
+++ b/http.h
@@ -76,6 +76,12 @@ extern int http_is_verbose;
 extern ssize_t http_post_buffer;
 extern struct credential http_auth;
 
+/**
+ * Prepare for an HTTP re-authentication retry. This fills credentials
+ * via credential_fill() so the next request can include them.
+ */
+void http_reauth_prepare(int all_capabilities);
+
 extern char curl_errorstr[CURL_ERROR_SIZE];
 
 enum http_follow_config {
diff --git a/remote-curl.c b/remote-curl.c
index aba60d5712..affdb880f7 100644
--- a/remote-curl.c
+++ b/remote-curl.c
@@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece
 		do {
 			err = probe_rpc(rpc, &results);
 			if (err == HTTP_REAUTH)
-				credential_fill(the_repository, &http_auth, 0);
+				http_reauth_prepare(0);
 		} while (err == HTTP_REAUTH);
 		if (err != HTTP_OK)
 			return -1;
@@ -1068,7 +1068,7 @@ retry:
 	rpc->any_written = 0;
 	err = run_slot(slot, NULL);
 	if (err == HTTP_REAUTH && !large_request) {
-		credential_fill(the_repository, &http_auth, 0);
+		http_reauth_prepare(0);
 		curl_slist_free_all(headers);
 		goto retry;
 	}
-- 
gitgitgadget
Matthew John Cheetham via GitGitGadgetApr 16, 2026, 09:20 UTC in reply to Matthew John Cheetham via GitGitGadget on lore

[PATCH 2/3] http: attempt Negotiate auth in http.emptyAuth=auto mode

From: Matthew John Cheetham <mjcheetham@outlook.com>

When a server advertises Negotiate (SPNEGO) authentication, the "auto" mode of http.emptyAuth should detect this as an "exotic" method and proactively send empty credentials, allowing libcurl to use the system Kerberos ticket without prompting the user.

However, two features interact to prevent this from working:

The Negotiate-stripping logic, introduced in 4dbe66464b (remote-curl: fall back to Basic auth if Negotiate fails, 2015-01-08), removes CURLAUTH_GSSNEGOTIATE from the allowed methods on the first 401 response. The empty-auth auto-detection, introduced in 40a18fc77c (http: add an "auto" mode for http.emptyauth, 2017-02-25), then checks the remaining methods for anything "exotic" -- but Negotiate has already been removed, so auto mode never activates for servers whose only non-Basic/Digest method is Negotiate (e.g., Apache with mod_auth_kerb offering Basic + Negotiate).

Fix this by delaying the Negotiate stripping in auto mode: on the first 401, keep Negotiate in the allowed methods so that auto mode can detect it and retry with empty credentials. If that attempt fails (no valid Kerberos ticket), strip Negotiate on the second 401 and fall through to credential_fill() as usual.

To support this, also teach http_reauth_prepare() to skip credential_fill() when empty auth is about to be attempted, since filling real credentials would bypass the empty-auth mechanism.

The true and false modes are unchanged: true sends empty credentials on the very first request (before any 401), and false never sends them.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
---
 http.c | 25 ++++++++++++++++++++++++-
 1 file changed, 24 insertions(+), 1 deletion(-)
Show changes to http.c +24 −1
diff --git a/http.c b/http.c
index f208e0ad82..1c7ea32ef2 100644
--- a/http.c
+++ b/http.c
@@ -138,6 +138,7 @@ static unsigned long empty_auth_useless =
 	CURLAUTH_BASIC
 	| CURLAUTH_DIGEST_IE
 	| CURLAUTH_DIGEST;
+static int empty_auth_try_negotiate;
 
 static struct curl_slist *pragma_header;
 static struct string_list extra_http_headers = STRING_LIST_INIT_DUP;
@@ -667,6 +668,17 @@ static void init_curl_http_auth(CURL *result)
 
 void http_reauth_prepare(int all_capabilities)
 {
+	/*
+	 * If we deferred stripping Negotiate to give empty auth a
+	 * chance (auto mode), skip credential_fill on this retry so
+	 * that init_curl_http_auth() sends empty credentials and
+	 * libcurl can attempt Negotiate with the system ticket cache.
+	 */
+	if (empty_auth_try_negotiate &&
+	    !http_auth.password && !http_auth.credential &&
+	    (http_auth_methods & CURLAUTH_GSSNEGOTIATE))
+		return;
+
 	credential_fill(the_repository, &http_auth, all_capabilities);
 }
 
@@ -1895,7 +1907,18 @@ static int handle_curl_result(struct slot_results *results)
 				http_proactive_auth = PROACTIVE_AUTH_NONE;
 			return HTTP_NOAUTH;
 		} else {
-			http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
+			if (curl_empty_auth == -1 &&
+			    !empty_auth_try_negotiate &&
+			    (results->auth_avail & CURLAUTH_GSSNEGOTIATE)) {
+				/*
+				 * In auto mode, give Negotiate a chance via
+				 * empty auth before stripping it. If it fails,
+				 * we will strip it on the next 401.
+				 */
+				empty_auth_try_negotiate = 1;
+			} else {
+				http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
+			}
 			if (results->auth_avail) {
 				http_auth_methods &= results->auth_avail;
 				http_auth_methods_restricted = 1;
-- 
gitgitgadget
Matthew John Cheetham via GitGitGadgetApr 16, 2026, 09:20 UTC in reply to Matthew John Cheetham via GitGitGadget on lore

[PATCH 3/3] t5563: add tests for http.emptyAuth with Negotiate

From: Matthew John Cheetham <mjcheetham@outlook.com>

Add tests exercising the interaction between http.emptyAuth and servers that advertise Negotiate (SPNEGO) authentication.

Verify that auto mode gives Negotiate a chance via empty auth (resulting in two 401 responses before falling through to credential_fill with Basic credentials), and that false mode strips Negotiate immediately (only one 401 response).

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
---
 t/t5563-simple-http-auth.sh | 74 +++++++++++++++++++++++++++++++++++++
 1 file changed, 74 insertions(+)
Show changes to t/t5563-simple-http-auth.sh +74 −0
diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh
index 0063581615..a7d475dd68 100755
--- a/t/t5563-simple-http-auth.sh
+++ b/t/t5563-simple-http-auth.sh
@@ -719,4 +719,78 @@ test_expect_success 'access using three-legged auth' '
 	EOF
 '
 
+test_lazy_prereq SPNEGO 'curl --version | grep -qi "SPNEGO\|GSS-API\|Kerberos\|negotiate"'
+
+test_expect_success SPNEGO 'http.emptyAuth=auto attempts Negotiate before credential_fill' '
+	test_when_finished "per_test_cleanup" &&
+
+	set_credential_reply get <<-EOF &&
+	username=alice
+	password=secret-passwd
+	EOF
+
+	# Basic base64(alice:secret-passwd)
+	cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
+	id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
+	EOF
+
+	cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
+	id=1 status=200
+	id=default response=WWW-Authenticate: Negotiate
+	id=default response=WWW-Authenticate: Basic realm="example.com"
+	EOF
+
+	test_config_global credential.helper test-helper &&
+	GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-auto" \
+		git -c http.emptyAuth=auto \
+		ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+	# In auto mode with a Negotiate+Basic server, there should be
+	# three 401 responses: (1) initial no-auth request, (2) empty-auth
+	# retry where Negotiate fails (no Kerberos ticket), (3) libcurl
+	# internal Negotiate retry. The fourth attempt uses Basic
+	# credentials from credential_fill and succeeds.
+	grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-auto" >actual_401s &&
+	test_line_count = 3 actual_401s &&
+
+	expect_credential_query get <<-EOF
+	capability[]=authtype
+	capability[]=state
+	protocol=http
+	host=$HTTPD_DEST
+	wwwauth[]=Negotiate
+	wwwauth[]=Basic realm="example.com"
+	EOF
+'
+
+test_expect_success SPNEGO 'http.emptyAuth=false skips Negotiate' '
+	test_when_finished "per_test_cleanup" &&
+
+	set_credential_reply get <<-EOF &&
+	username=alice
+	password=secret-passwd
+	EOF
+
+	# Basic base64(alice:secret-passwd)
+	cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
+	id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
+	EOF
+
+	cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
+	id=1 status=200
+	id=default response=WWW-Authenticate: Negotiate
+	id=default response=WWW-Authenticate: Basic realm="example.com"
+	EOF
+
+	test_config_global credential.helper test-helper &&
+	GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-false" \
+		git -c http.emptyAuth=false \
+		ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+	# With emptyAuth=false, Negotiate is stripped immediately and
+	# credential_fill is called right away. Only one 401 response.
+	grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-false" >actual_401s &&
+	test_line_count = 1 actual_401s
+'
+
 test_done
-- 
gitgitgadget
Junio C HamanoApr 16, 2026, 16:21 UTC in reply to Matthew John Cheetham via GitGitGadget on lore

Re: [PATCH 1/3] http: extract http_reauth_prepare() from retry paths

"Matthew John Cheetham via GitGitGadget" <gitgitgadget@gmail.com> writes:

Show 16 quoted lines
> From: Matthew John Cheetham <mjcheetham@outlook.com>
>
> All three HTTP retry paths (http_request_recoverable, post_rpc,
> probe_rpc) call credential_fill() directly when handling
> HTTP_REAUTH. Extract this into a helper function so that a
> subsequent commit can add pre-fill logic (such as attempting
> empty-auth before prompting) in one place.
>
> No functional change.
>
> Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
> ---
>  http.c        | 7 ++++++-
>  http.h        | 6 ++++++
>  remote-curl.c | 4 ++--
>  3 files changed, 14 insertions(+), 3 deletions(-)
Neat.
Show 65 quoted lines
>
> diff --git a/http.c b/http.c
> index d8d016891b..f208e0ad82 100644
> --- a/http.c
> +++ b/http.c
> @@ -665,6 +665,11 @@ static void init_curl_http_auth(CURL *result)
>  	}
>  }
>  
> +void http_reauth_prepare(int all_capabilities)
> +{
> +	credential_fill(the_repository, &http_auth, all_capabilities);
> +}
> +
>  /* *var must be free-able */
>  static void var_override(char **var, char *value)
>  {
> @@ -2398,7 +2403,7 @@ static int http_request_recoverable(const char *url,
>  				sleep(retry_delay);
>  			}
>  		} else if (ret == HTTP_REAUTH) {
> -			credential_fill(the_repository, &http_auth, 1);
> +			http_reauth_prepare(1);
>  		}
>  
>  		ret = http_request(url, result, target, options);
> diff --git a/http.h b/http.h
> index f9ee888c3e..729c51904d 100644
> --- a/http.h
> +++ b/http.h
> @@ -76,6 +76,12 @@ extern int http_is_verbose;
>  extern ssize_t http_post_buffer;
>  extern struct credential http_auth;
>  
> +/**
> + * Prepare for an HTTP re-authentication retry. This fills credentials
> + * via credential_fill() so the next request can include them.
> + */
> +void http_reauth_prepare(int all_capabilities);
> +
>  extern char curl_errorstr[CURL_ERROR_SIZE];
>  
>  enum http_follow_config {
> diff --git a/remote-curl.c b/remote-curl.c
> index aba60d5712..affdb880f7 100644
> --- a/remote-curl.c
> +++ b/remote-curl.c
> @@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece
>  		do {
>  			err = probe_rpc(rpc, &results);
>  			if (err == HTTP_REAUTH)
> -				credential_fill(the_repository, &http_auth, 0);
> +				http_reauth_prepare(0);
>  		} while (err == HTTP_REAUTH);
>  		if (err != HTTP_OK)
>  			return -1;
> @@ -1068,7 +1068,7 @@ retry:
>  	rpc->any_written = 0;
>  	err = run_slot(slot, NULL);
>  	if (err == HTTP_REAUTH && !large_request) {
> -		credential_fill(the_repository, &http_auth, 0);
> +		http_reauth_prepare(0);
>  		curl_slist_free_all(headers);
>  		goto retry;
>  	}
Junio C HamanoApr 16, 2026, 16:40 UTC in reply to Matthew John Cheetham via GitGitGadget on lore

Re: [PATCH 2/3] http: attempt Negotiate auth in http.emptyAuth=auto mode

"Matthew John Cheetham via GitGitGadget" <gitgitgadget@gmail.com> writes:

Show 19 quoted lines
> From: Matthew John Cheetham <mjcheetham@outlook.com>
>
> When a server advertises Negotiate (SPNEGO) authentication, the
> "auto" mode of http.emptyAuth should detect this as an "exotic"
> method and proactively send empty credentials, allowing libcurl to
> use the system Kerberos ticket without prompting the user.
>
> However, two features interact to prevent this from working:
>
> The Negotiate-stripping logic, introduced in 4dbe66464b
> (remote-curl: fall back to Basic auth if Negotiate fails,
> 2015-01-08), removes CURLAUTH_GSSNEGOTIATE from the allowed
> methods on the first 401 response. The empty-auth auto-detection,
> introduced in 40a18fc77c (http: add an "auto" mode for
> http.emptyauth, 2017-02-25), then checks the remaining methods
> for anything "exotic" -- but Negotiate has already been removed,
> so auto mode never activates for servers whose only non-Basic/Digest
> method is Negotiate (e.g., Apache with mod_auth_kerb offering
> Basic + Negotiate).
Well explained.
Show 5 quoted lines
> Fix this by delaying the Negotiate stripping in auto mode: on the
> first 401, keep Negotiate in the allowed methods so that auto mode
> can detect it and retry with empty credentials. If that attempt
> fails (no valid Kerberos ticket), strip Negotiate on the second 401
> and fall through to credential_fill() as usual.

OK, succeeding after two attempts is much better than failing after only one attempt.

> To support this, also teach http_reauth_prepare() to skip
> credential_fill() when empty auth is about to be attempted, since
> filling real credentials would bypass the empty-auth mechanism.
And that is why the previous step shines.  Very neat.
> The true and false modes are unchanged: true sends empty credentials
> on the very first request (before any 401), and false never sends
> them.

OK. This is a tangent, but "git config --help" on "http.emptyAuth" is horrible. It does not say what the allowed values are, so I had to first write "There are million other things in the system that this patch does not modify, so what's the point of singling out these two settings and saying that this patch does not change them?", before realizing that 'auto' the patch (and the explanation of the "empty-autho auto-detction" above) is about the third possiblity of the same variable and take it back.

Show 17 quoted lines
> Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
> ---
>  http.c | 25 ++++++++++++++++++++++++-
>  1 file changed, 24 insertions(+), 1 deletion(-)
>
> diff --git a/http.c b/http.c
> index f208e0ad82..1c7ea32ef2 100644
> --- a/http.c
> +++ b/http.c
> @@ -138,6 +138,7 @@ static unsigned long empty_auth_useless =
>  	CURLAUTH_BASIC
>  	| CURLAUTH_DIGEST_IE
>  	| CURLAUTH_DIGEST;
> +static int empty_auth_try_negotiate;
>  
>  static struct curl_slist *pragma_header;
>  static struct string_list extra_http_headers = STRING_LIST_INIT_DUP;

I guess the existing code already assumes that we connect to a single destination, run a single "session", and then die, so it is in line with the existing design to have a file-scope global keep track of our "state". In the longer run we may want to move these things to a struct so that we can run multiple sessions without having to kill ourselves and restart, but that is totally outside the topic of these patches to fix the negotiate auth.

Show 38 quoted lines
> @@ -667,6 +668,17 @@ static void init_curl_http_auth(CURL *result)
>  
>  void http_reauth_prepare(int all_capabilities)
>  {
> +	/*
> +	 * If we deferred stripping Negotiate to give empty auth a
> +	 * chance (auto mode), skip credential_fill on this retry so
> +	 * that init_curl_http_auth() sends empty credentials and
> +	 * libcurl can attempt Negotiate with the system ticket cache.
> +	 */
> +	if (empty_auth_try_negotiate &&
> +	    !http_auth.password && !http_auth.credential &&
> +	    (http_auth_methods & CURLAUTH_GSSNEGOTIATE))
> +		return;
> +
>  	credential_fill(the_repository, &http_auth, all_capabilities);
>  }
>  
> @@ -1895,7 +1907,18 @@ static int handle_curl_result(struct slot_results *results)
>  				http_proactive_auth = PROACTIVE_AUTH_NONE;
>  			return HTTP_NOAUTH;
>  		} else {
> -			http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
> +			if (curl_empty_auth == -1 &&
> +			    !empty_auth_try_negotiate &&
> +			    (results->auth_avail & CURLAUTH_GSSNEGOTIATE)) {
> +				/*
> +				 * In auto mode, give Negotiate a chance via
> +				 * empty auth before stripping it. If it fails,
> +				 * we will strip it on the next 401.
> +				 */
> +				empty_auth_try_negotiate = 1;
> +			} else {
> +				http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
> +			}
>  			if (results->auth_avail) {
>  				http_auth_methods &= results->auth_avail;
>  				http_auth_methods_restricted = 1;
Matthew John CheethamApr 28, 2026, 14:38 UTC in reply to Junio C Hamano on lore

Re: [PATCH 2/3] http: attempt Negotiate auth in http.emptyAuth=auto mode

On 2026-04-16 17:40, Junio C Hamano wrote:
Show 12 quoted lines
>> The true and false modes are unchanged: true sends empty credentials
>> on the very first request (before any 401), and false never sends
>> them.
> 
> OK.  This is a tangent, but "git config --help" on "http.emptyAuth"
> is horrible.  It does not say what the allowed values are, so I had
> to first write "There are million other things in the system that
> this patch does not modify, so what's the point of singling out
> these two settings and saying that this patch does not change
> them?", before realizing that 'auto' the patch (and the explanation
> of the "empty-autho auto-detction" above) is about the third
> possiblity of the same variable and take it back.

Agreed - the existing description is pretty opaque about what values it actually takes. Should I add another patch to this series to spell out the three values explicitly? How about something like this:

      http.emptyAuth::
              Attempt authentication without seeking a username or
              password.  This can be used to attempt GSS-Negotiate
              authentication without specifying a username in the URL,
              as libcurl normally requires a username for
              authentication. Possible values are:
      +
      --
      * `auto` (default) - Send empty credentials only if the server's
        401 response advertises an authentication mechanism that
        requires them (such as GSS-Negotiate); otherwise fall back to
        prompting via the credential helper.
      * `true` - Always send empty credentials on the very first
        request, before receiving any 401 response from the server.
      * `false` - Never send empty credentials. Mechanisms that
        require empty credentials, such as GSS-Negotiate, will not
        work.
      --
  Does that read better?
  Thanks,
  Matthew
Matthew John CheethamApr 30, 2026, 10:53 UTC on lore

Re: [PATCH 2/3] http: attempt Negotiate auth in http.emptyAuth=auto mode

[re-cc:ing the accidentially dropped mailing list]
On 2026-04-30 01:12, Junio C Hamano wrote:
Show 28 quoted lines
> Matthew John Cheetham <mjcheetham@outlook.com> writes:
> 
>> Agreed - the existing description is pretty opaque about what values it
>> actually takes. Should I add another patch to this series to spell out
>> the three values explicitly? How about something like this:
>>
>>        http.emptyAuth::
>>                Attempt authentication without seeking a username or
>>                password.  This can be used to attempt GSS-Negotiate
>>                authentication without specifying a username in the URL,
>>                as libcurl normally requires a username for
>>                authentication. Possible values are:
>>        +
>>        --
>>        * `auto` (default) - Send empty credentials only if the server's
>>          401 response advertises an authentication mechanism that
>>          requires them (such as GSS-Negotiate); otherwise fall back to
>>          prompting via the credential helper.
>>        * `true` - Always send empty credentials on the very first
>>          request, before receiving any 401 response from the server.
>>        * `false` - Never send empty credentials. Mechanisms that
>>          require empty credentials, such as GSS-Negotiate, will not
>>          work.
>>        --
>>
>>    Does that read better?
> 
> Surely.  Thanks.
Submitted as v2

Thanks, Matthew

Matthew John Cheetham via GitGitGadgetApr 30, 2026, 10:54 UTC in reply to Matthew John Cheetham via GitGitGadget on lore

[PATCH v2 0/4] http: fix emptyAuth=auto for Negotiate/SPNEGO

When a server advertises Negotiate (SPNEGO) authentication alongside Basic, the "auto" mode of http.emptyAuth should allow libcurl to attempt Kerberos authentication using the system ticket cache before falling back to credential_fill(). Currently this never happens due to an interaction between two older features.

The Negotiate-stripping logic from 4dbe66464b (remote-curl: fall back to Basic auth if Negotiate fails, 2015-01-08) removes CURLAUTH_GSSNEGOTIATE on the first 401, before the auto-detection from 40a18fc77c (http: add an "auto" mode for http.emptyauth, 2017-02-25) gets a chance to see it as an "exotic" method. The result is that auto mode silently degrades to the same behavior as emptyAuth=false for any server whose only non-Basic/Digest method is Negotiate, forcing Kerberos users to manually set http.emptyAuth=true to get seamless ticket-based authentication.

This series fixes the interaction by delaying the Negotiate stripping in auto mode by one round-trip, giving empty auth a chance to use the system Kerberos ticket. If there is no valid ticket, Negotiate is stripped on the second 401 and we fall through to credential_fill() as before. The true and false modes are unchanged.

Patch 1: Extract a http_reauth_prepare() helper from the three retry paths that call credential_fill() on HTTP_REAUTH. Pure refactor, no behavior change.

Patch 2: Delay the GSSNEGOTIATE stripping in auto mode and teach http_reauth_prepare() to skip credential_fill() when empty auth should be attempted first.

Patch 3: Add tests verifying that auto mode produces an extra round-trip (empty auth attempt) compared to false mode, using the existing nph-custom-auth.sh CGI infrastructure.

Patch 4: Update http.emptyAuth documentation to clarify possible values (true, false, and auto).

There is a trade-off in auto mode: when a server advertises Negotiate but the client has no valid Kerberos ticket, there is one extra round-trip compared to the current behavior. This matches the trade-off already documented in 40a18fc77c. Users who want to avoid it can set http.emptyAuth=false.

Note: this patch series was taken early into Git for Windows for the
2.54.0-rc2 release.
https://github.com/git-for-windows/git/commit/8e94b65c003783d7d7b09d9fccdf06a1363e347c
----------------------------------------------------------------------------
Update in v2:
 * Add patch 4 to clarify the available options for http.emptyAuth in the
   config documentation.
Matthew John Cheetham (4):
  http: extract http_reauth_prepare() from retry paths
  http: attempt Negotiate auth in http.emptyAuth=auto mode
  t5563: add tests for http.emptyAuth with Negotiate
  doc: clarify http.emptyAuth values
 Documentation/config/http.adoc | 13 +++++-
 http.c                         | 32 ++++++++++++++-
 http.h                         |  6 +++
 remote-curl.c                  |  4 +-
 t/t5563-simple-http-auth.sh    | 74 ++++++++++++++++++++++++++++++++++
 5 files changed, 124 insertions(+), 5 deletions(-)
base-commit: 2b39a27d40682c09ac1c031f099ee602061597cd
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2087%2Fmjcheetham%2Fspnego-fix-upstream-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2087/mjcheetham/spnego-fix-upstream-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2087
Range-diff vs v1:
 1:  49488cc7d4 = 1:  49488cc7d4 http: extract http_reauth_prepare() from retry paths
 2:  f175294459 = 2:  f175294459 http: attempt Negotiate auth in http.emptyAuth=auto mode
 3:  650acab79e = 3:  650acab79e t5563: add tests for http.emptyAuth with Negotiate
 -:  ---------- > 4:  e0f236767f doc: clarify http.emptyAuth values
-- 
gitgitgadget
Matthew John Cheetham via GitGitGadgetApr 30, 2026, 10:54 UTC in reply to Matthew John Cheetham via GitGitGadget on lore

[PATCH v2 1/4] http: extract http_reauth_prepare() from retry paths

From: Matthew John Cheetham <mjcheetham@outlook.com>

All three HTTP retry paths (http_request_recoverable, post_rpc, probe_rpc) call credential_fill() directly when handling HTTP_REAUTH. Extract this into a helper function so that a subsequent commit can add pre-fill logic (such as attempting empty-auth before prompting) in one place.

No functional change.
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
---
 http.c        | 7 ++++++-
 http.h        | 6 ++++++
 remote-curl.c | 4 ++--
 3 files changed, 14 insertions(+), 3 deletions(-)
Show changes to 3 files +14 −3

http.c, http.h, remote-curl.c

diff --git a/http.c b/http.c
index d8d016891b..f208e0ad82 100644
--- a/http.c
+++ b/http.c
@@ -665,6 +665,11 @@ static void init_curl_http_auth(CURL *result)
 	}
 }
 
+void http_reauth_prepare(int all_capabilities)
+{
+	credential_fill(the_repository, &http_auth, all_capabilities);
+}
+
 /* *var must be free-able */
 static void var_override(char **var, char *value)
 {
@@ -2398,7 +2403,7 @@ static int http_request_recoverable(const char *url,
 				sleep(retry_delay);
 			}
 		} else if (ret == HTTP_REAUTH) {
-			credential_fill(the_repository, &http_auth, 1);
+			http_reauth_prepare(1);
 		}
 
 		ret = http_request(url, result, target, options);
diff --git a/http.h b/http.h
index f9ee888c3e..729c51904d 100644
--- a/http.h
+++ b/http.h
@@ -76,6 +76,12 @@ extern int http_is_verbose;
 extern ssize_t http_post_buffer;
 extern struct credential http_auth;
 
+/**
+ * Prepare for an HTTP re-authentication retry. This fills credentials
+ * via credential_fill() so the next request can include them.
+ */
+void http_reauth_prepare(int all_capabilities);
+
 extern char curl_errorstr[CURL_ERROR_SIZE];
 
 enum http_follow_config {
diff --git a/remote-curl.c b/remote-curl.c
index aba60d5712..affdb880f7 100644
--- a/remote-curl.c
+++ b/remote-curl.c
@@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece
 		do {
 			err = probe_rpc(rpc, &results);
 			if (err == HTTP_REAUTH)
-				credential_fill(the_repository, &http_auth, 0);
+				http_reauth_prepare(0);
 		} while (err == HTTP_REAUTH);
 		if (err != HTTP_OK)
 			return -1;
@@ -1068,7 +1068,7 @@ retry:
 	rpc->any_written = 0;
 	err = run_slot(slot, NULL);
 	if (err == HTTP_REAUTH && !large_request) {
-		credential_fill(the_repository, &http_auth, 0);
+		http_reauth_prepare(0);
 		curl_slist_free_all(headers);
 		goto retry;
 	}
-- 
gitgitgadget
Matthew John Cheetham via GitGitGadgetApr 30, 2026, 10:54 UTC in reply to Matthew John Cheetham via GitGitGadget on lore

[PATCH v2 2/4] http: attempt Negotiate auth in http.emptyAuth=auto mode

From: Matthew John Cheetham <mjcheetham@outlook.com>

When a server advertises Negotiate (SPNEGO) authentication, the "auto" mode of http.emptyAuth should detect this as an "exotic" method and proactively send empty credentials, allowing libcurl to use the system Kerberos ticket without prompting the user.

However, two features interact to prevent this from working:

The Negotiate-stripping logic, introduced in 4dbe66464b (remote-curl: fall back to Basic auth if Negotiate fails, 2015-01-08), removes CURLAUTH_GSSNEGOTIATE from the allowed methods on the first 401 response. The empty-auth auto-detection, introduced in 40a18fc77c (http: add an "auto" mode for http.emptyauth, 2017-02-25), then checks the remaining methods for anything "exotic" -- but Negotiate has already been removed, so auto mode never activates for servers whose only non-Basic/Digest method is Negotiate (e.g., Apache with mod_auth_kerb offering Basic + Negotiate).

Fix this by delaying the Negotiate stripping in auto mode: on the first 401, keep Negotiate in the allowed methods so that auto mode can detect it and retry with empty credentials. If that attempt fails (no valid Kerberos ticket), strip Negotiate on the second 401 and fall through to credential_fill() as usual.

To support this, also teach http_reauth_prepare() to skip credential_fill() when empty auth is about to be attempted, since filling real credentials would bypass the empty-auth mechanism.

The true and false modes are unchanged: true sends empty credentials on the very first request (before any 401), and false never sends them.

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
---
 http.c | 25 ++++++++++++++++++++++++-
 1 file changed, 24 insertions(+), 1 deletion(-)
Show changes to http.c +24 −1
diff --git a/http.c b/http.c
index f208e0ad82..1c7ea32ef2 100644
--- a/http.c
+++ b/http.c
@@ -138,6 +138,7 @@ static unsigned long empty_auth_useless =
 	CURLAUTH_BASIC
 	| CURLAUTH_DIGEST_IE
 	| CURLAUTH_DIGEST;
+static int empty_auth_try_negotiate;
 
 static struct curl_slist *pragma_header;
 static struct string_list extra_http_headers = STRING_LIST_INIT_DUP;
@@ -667,6 +668,17 @@ static void init_curl_http_auth(CURL *result)
 
 void http_reauth_prepare(int all_capabilities)
 {
+	/*
+	 * If we deferred stripping Negotiate to give empty auth a
+	 * chance (auto mode), skip credential_fill on this retry so
+	 * that init_curl_http_auth() sends empty credentials and
+	 * libcurl can attempt Negotiate with the system ticket cache.
+	 */
+	if (empty_auth_try_negotiate &&
+	    !http_auth.password && !http_auth.credential &&
+	    (http_auth_methods & CURLAUTH_GSSNEGOTIATE))
+		return;
+
 	credential_fill(the_repository, &http_auth, all_capabilities);
 }
 
@@ -1895,7 +1907,18 @@ static int handle_curl_result(struct slot_results *results)
 				http_proactive_auth = PROACTIVE_AUTH_NONE;
 			return HTTP_NOAUTH;
 		} else {
-			http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
+			if (curl_empty_auth == -1 &&
+			    !empty_auth_try_negotiate &&
+			    (results->auth_avail & CURLAUTH_GSSNEGOTIATE)) {
+				/*
+				 * In auto mode, give Negotiate a chance via
+				 * empty auth before stripping it. If it fails,
+				 * we will strip it on the next 401.
+				 */
+				empty_auth_try_negotiate = 1;
+			} else {
+				http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
+			}
 			if (results->auth_avail) {
 				http_auth_methods &= results->auth_avail;
 				http_auth_methods_restricted = 1;
-- 
gitgitgadget
Matthew John Cheetham via GitGitGadgetApr 30, 2026, 10:54 UTC in reply to Matthew John Cheetham via GitGitGadget on lore

[PATCH v2 3/4] t5563: add tests for http.emptyAuth with Negotiate

From: Matthew John Cheetham <mjcheetham@outlook.com>

Add tests exercising the interaction between http.emptyAuth and servers that advertise Negotiate (SPNEGO) authentication.

Verify that auto mode gives Negotiate a chance via empty auth (resulting in two 401 responses before falling through to credential_fill with Basic credentials), and that false mode strips Negotiate immediately (only one 401 response).

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
---
 t/t5563-simple-http-auth.sh | 74 +++++++++++++++++++++++++++++++++++++
 1 file changed, 74 insertions(+)
Show changes to t/t5563-simple-http-auth.sh +74 −0
diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh
index 0063581615..a7d475dd68 100755
--- a/t/t5563-simple-http-auth.sh
+++ b/t/t5563-simple-http-auth.sh
@@ -719,4 +719,78 @@ test_expect_success 'access using three-legged auth' '
 	EOF
 '
 
+test_lazy_prereq SPNEGO 'curl --version | grep -qi "SPNEGO\|GSS-API\|Kerberos\|negotiate"'
+
+test_expect_success SPNEGO 'http.emptyAuth=auto attempts Negotiate before credential_fill' '
+	test_when_finished "per_test_cleanup" &&
+
+	set_credential_reply get <<-EOF &&
+	username=alice
+	password=secret-passwd
+	EOF
+
+	# Basic base64(alice:secret-passwd)
+	cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
+	id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
+	EOF
+
+	cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
+	id=1 status=200
+	id=default response=WWW-Authenticate: Negotiate
+	id=default response=WWW-Authenticate: Basic realm="example.com"
+	EOF
+
+	test_config_global credential.helper test-helper &&
+	GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-auto" \
+		git -c http.emptyAuth=auto \
+		ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+	# In auto mode with a Negotiate+Basic server, there should be
+	# three 401 responses: (1) initial no-auth request, (2) empty-auth
+	# retry where Negotiate fails (no Kerberos ticket), (3) libcurl
+	# internal Negotiate retry. The fourth attempt uses Basic
+	# credentials from credential_fill and succeeds.
+	grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-auto" >actual_401s &&
+	test_line_count = 3 actual_401s &&
+
+	expect_credential_query get <<-EOF
+	capability[]=authtype
+	capability[]=state
+	protocol=http
+	host=$HTTPD_DEST
+	wwwauth[]=Negotiate
+	wwwauth[]=Basic realm="example.com"
+	EOF
+'
+
+test_expect_success SPNEGO 'http.emptyAuth=false skips Negotiate' '
+	test_when_finished "per_test_cleanup" &&
+
+	set_credential_reply get <<-EOF &&
+	username=alice
+	password=secret-passwd
+	EOF
+
+	# Basic base64(alice:secret-passwd)
+	cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
+	id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
+	EOF
+
+	cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
+	id=1 status=200
+	id=default response=WWW-Authenticate: Negotiate
+	id=default response=WWW-Authenticate: Basic realm="example.com"
+	EOF
+
+	test_config_global credential.helper test-helper &&
+	GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-false" \
+		git -c http.emptyAuth=false \
+		ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+	# With emptyAuth=false, Negotiate is stripped immediately and
+	# credential_fill is called right away. Only one 401 response.
+	grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-false" >actual_401s &&
+	test_line_count = 1 actual_401s
+'
+
 test_done
-- 
gitgitgadget
Matthew John Cheetham via GitGitGadgetApr 30, 2026, 10:54 UTC in reply to Matthew John Cheetham via GitGitGadget on lore

[PATCH v2 4/4] doc: clarify http.emptyAuth values

From: Matthew John Cheetham <mjcheetham@outlook.com>

The existing description of http.emptyAuth explains the purpose of the setting but never says what values it accepts. Readers have to infer from context (or read the source) that it takes 'true', 'false', or 'auto', and what each one means.

Document the three accepted values explicitly:
* 'auto' (the default) only sends empty credentials when the server's
  401 response advertises a mechanism that requires them, such as
  GSS-Negotiate. This matches the long-standing auto-detection
  behaviour added in 40a18fc77c (http: add an "auto" mode for
  http.emptyauth, 2017-02-25).
* 'true' unconditionally sends empty credentials on the very first
  request, before any 401 response, for callers that know they want
  this behaviour up front.
* 'false' disables the feature entirely; mechanisms that depend on
  empty credentials, such as GSS-Negotiate, will not work in this
  mode.
Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
---
 Documentation/config/http.adoc | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)
Show changes to Documentation/config/http.adoc +12 −1
diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc
index 849c89f36c..792a71b413 100644
--- a/Documentation/config/http.adoc
+++ b/Documentation/config/http.adoc
@@ -59,7 +59,18 @@ http.emptyAuth::
 	Attempt authentication without seeking a username or password.  This
 	can be used to attempt GSS-Negotiate authentication without specifying
 	a username in the URL, as libcurl normally requires a username for
-	authentication.
+	authentication. Possible values are:
++
+--
+* `auto` (default) - Send empty credentials only if the server's 401 response
+  advertises an authentication mechanism that requires them (such as
+  GSS-Negotiate); otherwise fall back to prompting via the credential helper.
+* `true` - Always send empty credentials on the very first request, before
+  receiving any 401 response from the server.
+* `false` - Never send empty credentials. Mechanisms that require
+  empty credentials or an explicit username, such as GSS-Negotiate, will not
+  work.
+--
 
 http.proactiveAuth::
 	Attempt authentication without first making an unauthenticated attempt and
-- 
gitgitgadget

Back to recent threads