Skip to content

Commit ea36a7c

Browse files
dschoGit for Windows Build Agent
authored andcommitted
http: fix emptyAuth=auto for Negotiate/SPNEGO (#6170)
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 4dbe664 (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 40a18fc (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 40a18fc. Users who want to avoid it can set http.emptyAuth=false.
2 parents a3b228b + dbe735c commit ea36a7c

File tree

4 files changed

+112
-4
lines changed

4 files changed

+112
-4
lines changed

http.c

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ static unsigned long empty_auth_useless =
139139
CURLAUTH_BASIC
140140
| CURLAUTH_DIGEST_IE
141141
| CURLAUTH_DIGEST;
142+
static int empty_auth_try_negotiate;
142143

143144
static struct curl_slist *pragma_header;
144145
static struct string_list extra_http_headers = STRING_LIST_INIT_DUP;
@@ -704,6 +705,22 @@ static void init_curl_http_auth(CURL *result)
704705
}
705706
}
706707

708+
void http_reauth_prepare(int all_capabilities)
709+
{
710+
/*
711+
* If we deferred stripping Negotiate to give empty auth a
712+
* chance (auto mode), skip credential_fill on this retry so
713+
* that init_curl_http_auth() sends empty credentials and
714+
* libcurl can attempt Negotiate with the system ticket cache.
715+
*/
716+
if (empty_auth_try_negotiate &&
717+
!http_auth.password && !http_auth.credential &&
718+
(http_auth_methods & CURLAUTH_GSSNEGOTIATE))
719+
return;
720+
721+
credential_fill(the_repository, &http_auth, all_capabilities);
722+
}
723+
707724
/* *var must be free-able */
708725
static void var_override(char **var, char *value)
709726
{
@@ -1954,7 +1971,18 @@ static int handle_curl_result(struct slot_results *results)
19541971
}
19551972
return HTTP_NOAUTH;
19561973
} else {
1957-
http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
1974+
if (curl_empty_auth == -1 &&
1975+
!empty_auth_try_negotiate &&
1976+
(results->auth_avail & CURLAUTH_GSSNEGOTIATE)) {
1977+
/*
1978+
* In auto mode, give Negotiate a chance via
1979+
* empty auth before stripping it. If it fails,
1980+
* we will strip it on the next 401.
1981+
*/
1982+
empty_auth_try_negotiate = 1;
1983+
} else {
1984+
http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
1985+
}
19581986
if (results->auth_avail) {
19591987
http_auth_methods &= results->auth_avail;
19601988
http_auth_methods_restricted = 1;
@@ -2462,7 +2490,7 @@ static int http_request_recoverable(const char *url,
24622490
sleep(retry_delay);
24632491
}
24642492
} else if (ret == HTTP_REAUTH) {
2465-
credential_fill(the_repository, &http_auth, 1);
2493+
http_reauth_prepare(1);
24662494
}
24672495

24682496
/*

http.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ extern int http_is_verbose;
7676
extern ssize_t http_post_buffer;
7777
extern struct credential http_auth;
7878

79+
/**
80+
* Prepare for an HTTP re-authentication retry. This fills credentials
81+
* via credential_fill() so the next request can include them.
82+
*/
83+
void http_reauth_prepare(int all_capabilities);
84+
7985
extern char curl_errorstr[CURL_ERROR_SIZE];
8086

8187
enum http_follow_config {

remote-curl.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece
946946
do {
947947
err = probe_rpc(rpc, &results);
948948
if (err == HTTP_REAUTH)
949-
credential_fill(the_repository, &http_auth, 0);
949+
http_reauth_prepare(0);
950950
} while (err == HTTP_REAUTH);
951951
if (err != HTTP_OK)
952952
return -1;
@@ -1068,7 +1068,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece
10681068
rpc->any_written = 0;
10691069
err = run_slot(slot, NULL);
10701070
if (err == HTTP_REAUTH && !large_request) {
1071-
credential_fill(the_repository, &http_auth, 0);
1071+
http_reauth_prepare(0);
10721072
curl_slist_free_all(headers);
10731073
goto retry;
10741074
}

t/t5563-simple-http-auth.sh

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,4 +748,78 @@ test_expect_success NTLM 'access using NTLM auth' '
748748
git ls-remote "$HTTPD_URL/ntlm_auth/repo.git"
749749
'
750750

751+
test_lazy_prereq SPNEGO 'curl --version | grep -qi "SPNEGO\|GSS-API\|Kerberos\|negotiate"'
752+
753+
test_expect_success SPNEGO 'http.emptyAuth=auto attempts Negotiate before credential_fill' '
754+
test_when_finished "per_test_cleanup" &&
755+
756+
set_credential_reply get <<-EOF &&
757+
username=alice
758+
password=secret-passwd
759+
EOF
760+
761+
# Basic base64(alice:secret-passwd)
762+
cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
763+
id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
764+
EOF
765+
766+
cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
767+
id=1 status=200
768+
id=default response=WWW-Authenticate: Negotiate
769+
id=default response=WWW-Authenticate: Basic realm="example.com"
770+
EOF
771+
772+
test_config_global credential.helper test-helper &&
773+
GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-auto" \
774+
git -c http.emptyAuth=auto \
775+
ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
776+
777+
# In auto mode with a Negotiate+Basic server, there should be
778+
# three 401 responses: (1) initial no-auth request, (2) empty-auth
779+
# retry where Negotiate fails (no Kerberos ticket), (3) libcurl
780+
# internal Negotiate retry. The fourth attempt uses Basic
781+
# credentials from credential_fill and succeeds.
782+
grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-auto" >actual_401s &&
783+
test_line_count = 3 actual_401s &&
784+
785+
expect_credential_query get <<-EOF
786+
capability[]=authtype
787+
capability[]=state
788+
protocol=http
789+
host=$HTTPD_DEST
790+
wwwauth[]=Negotiate
791+
wwwauth[]=Basic realm="example.com"
792+
EOF
793+
'
794+
795+
test_expect_success SPNEGO 'http.emptyAuth=false skips Negotiate' '
796+
test_when_finished "per_test_cleanup" &&
797+
798+
set_credential_reply get <<-EOF &&
799+
username=alice
800+
password=secret-passwd
801+
EOF
802+
803+
# Basic base64(alice:secret-passwd)
804+
cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
805+
id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
806+
EOF
807+
808+
cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
809+
id=1 status=200
810+
id=default response=WWW-Authenticate: Negotiate
811+
id=default response=WWW-Authenticate: Basic realm="example.com"
812+
EOF
813+
814+
test_config_global credential.helper test-helper &&
815+
GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-false" \
816+
git -c http.emptyAuth=false \
817+
ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
818+
819+
# With emptyAuth=false, Negotiate is stripped immediately and
820+
# credential_fill is called right away. Only one 401 response.
821+
grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-false" >actual_401s &&
822+
test_line_count = 1 actual_401s
823+
'
824+
751825
test_done

0 commit comments

Comments
 (0)