http: fix emptyAuth=auto for Negotiate/SPNEGO
cover 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.
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
Signature · 1 line
gitgitgadget
PATCH 1/3 http: extract http_reauth_prepare() from retry paths
From: Matthew John Cheetham <[email protected]>
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.
--- http.c | 7 ++++++- http.h | 6 ++++++ remote-curl.c | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-)
http.c +6 -1
@@ -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);
http.h +6 -0
@@ -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 {
remote-curl.c +2 -2
@@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_recedo { 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; }
Signature · 1 line
gitgitgadget
Re: [PATCH 1/3] http: extract http_reauth_prepare() from retry paths
"Matthew John Cheetham via GitGitGadget" <[email protected]>
Quoted 8 lines · writes:
From: Matthew John Cheetham <[email protected]>
All three HTTP retry paths (
http_request_recoverable,post_rpc,probe_rpc) callcredential_fill()directly when handlingHTTP_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.
Quoted 8 lines
No functional change.
Signed-off-by Matthew John Cheetham <[email protected]>--- http.c | 7 ++++++- http.h | 6 ++++++ remote-curl.c | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-)
Neat.
Quoted 65 lines
http.c +6
-1@@ -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);http.h +6
-0@@ -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 {remote-curl.c +2
-2@@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_recedo { 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; }
PATCH 2/3 http: attempt Negotiate auth in http.emptyAuth=auto mode
In reply to Matthew John Cheetham via GitGitGadgetFrom: Matthew John Cheetham <[email protected]>
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.
--- http.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-)
http.c +24 -1
@@ -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 ;
Signature · 1 line
gitgitgadget
Re: [PATCH 2/3] http: attempt Negotiate auth in http.emptyAuth=auto mode
"Matthew John Cheetham via GitGitGadget" <[email protected]>
Quoted 9 lines · writes:
From: Matthew John Cheetham <[email protected]>
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:
Quoted 10 lines
The Negotiate-stripping logic, introduced in
4dbe66464b(remote-curl: fall back to Basic auth if Negotiate fails, 2015-01-08), removesCURLAUTH_GSSNEGOTIATEfrom the allowed methods on the first 401 response. The empty-auth auto-detection, introduced in40a18fc77c(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 withmod_auth_kerboffering Basic + Negotiate).
Well explained.
Quoted 5 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.
Quoted 3 lines
To support this, also teach
http_reauth_prepare()to skipcredential_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.
Quoted 3 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.
Quoted 10 lines
Signed-off-by Matthew John Cheetham <[email protected]>--- http.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-)http.c +0
-0
http.c static unsigned long empty_auth_useless = · line 144
Quoted 7 lines
http.c +1
-0@@ -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.
Quoted 38 lines
http.c +23
-1@@ -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 ;
Re: [PATCH 2/3] http: attempt Negotiate auth in http.emptyAuth=auto mode
Quoted 4 lines · Junio C Hamano
Quoted 3 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.
Quoted 8 lines
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
A message that is not in the archive (sent off-list or not yet received): [email protected]
Re: [PATCH 2/3] http: attempt Negotiate auth in http.emptyAuth=auto mode
[re-cc:ing the accidentially dropped mailing list]
Quoted 25 lines · Junio C Hamano
Quoted 23 lines · Matthew John Cheetham
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.
--
Quoted 3 lines
Quoted 1 line
Does that read better?
Surely. Thanks.
Submitted as v2
Thanks,
Matthew
PATCH 3/3 t5563: add tests for http.emptyAuth with Negotiate
In reply to Matthew John Cheetham via GitGitGadgetFrom: Matthew John Cheetham <[email protected]>
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).
--- t/t5563-simple-http-auth.sh | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+)
t/t5563-simple-http-auth.sh +74 -0
@@ -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
Signature · 1 line
gitgitgadget
v2 cover http: fix emptyAuth=auto for Negotiate/SPNEGO
In reply to Matthew John Cheetham via GitGitGadgetWhen 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
Signature · 1 line
gitgitgadget
PATCH v2 1/4 http: extract http_reauth_prepare() from retry paths
From: Matthew John Cheetham <[email protected]>
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.
--- http.c | 7 ++++++- http.h | 6 ++++++ remote-curl.c | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-)
http.c +6 -1
@@ -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);
http.h +6 -0
@@ -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 {
remote-curl.c +2 -2
@@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_recedo { 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; }
Signature · 1 line
gitgitgadget
PATCH v2 2/4 http: attempt Negotiate auth in http.emptyAuth=auto mode
In reply to Matthew John Cheetham via GitGitGadgetFrom: Matthew John Cheetham <[email protected]>
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.
--- http.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-)
http.c +24 -1
@@ -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 ;
Signature · 1 line
gitgitgadget
PATCH v2 3/4 t5563: add tests for http.emptyAuth with Negotiate
In reply to Matthew John Cheetham via GitGitGadgetFrom: Matthew John Cheetham <[email protected]>
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).
--- t/t5563-simple-http-auth.sh | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+)
t/t5563-simple-http-auth.sh +74 -0
@@ -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
Signature · 1 line
gitgitgadget
PATCH v2 4/4 doc: clarify http.emptyAuth values
In reply to Matthew John Cheetham via GitGitGadgetFrom: Matthew John Cheetham <[email protected]>
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.
--- Documentation/config/http.adoc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-)
Documentation/config/http.adoc +12 -1
@@ -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
Signature · 1 line
gitgitgadget