http: allow authenticating proactively

When making a request over HTTP(S), Git only sends authentication if it receives a 401 response. Thus, if a repository is open to the public for reading, Git will typically never ask for authentication for fetches and clones. However, there may be times when a user would like to authenticate nevertheless. For example, a forge may give higher rate limits to users who authenticate because they are easier to contact in case of excessive use. Or it may be useful for a known heavy user, such as an internal service, to proactively authenticate so its use can be monitored and, if necessary, throttled. Let's make this possible with a new option, "http.proactiveAuth". This option specifies a type of authentication which can be used to authenticate against the host in question. This is necessary because we lack the WWW-Authenticate header to provide us details; similarly, we cannot accept certain types of authentication because we require information from the server, such as a nonce or challenge, to successfully authenticate. If we're in auto mode and we got a username and password, set the authentication scheme to Basic. libcurl will not send authentication proactively unless there's a single choice of allowed authentication, and we know in this case we didn't get an authtype entry telling us what scheme to use, or we would have taken a different codepath and written the header ourselves. In any event, of the other schemes that libcurl supports, Digest and NTLM require a nonce or challenge, which means that they cannot work with proactive auth, and GSSAPI does not use a username and password at all, so Basic is the only logical choice among the built-in options. Note that the existing http_proactive_auth variable signifies proactive auth if there are already credentials, which is different from the functionality we're adding, which always seeks credentials even if none are provided. Nonetheless, t5540 tests the existing behavior for WebDAV-based pushes to an open repository without credentials, so we preserve it. While at first this may seem an insecure and bizarre decision, it may be that authentication is done with TLS certificates, in which case it might actually provide a quite high level of security. Expand the variable to use an enum to handle the additional cases and a helper function to distinguish our new cases from the old ones. Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

brian m. carlson committed Jul 10, 2024 at 00:01 UTC 610cbc1dfb4ad947b82d7312c54c7987177f979f
3 files changed +192 -6
Documentation/config/http.txt
+20
@@ -56,6 +56,26 @@ http.emptyAuth::
56 a username in the URL, as libcurl normally requires a username for
57 authentication.
58
59 +http.proactiveAuth::
60 + Attempt authentication without first making an unauthenticated attempt and
61 + receiving a 401 response. This can be used to ensure that all requests are
62 + authenticated. If `http.emptyAuth` is set to true, this value has no effect.
63 ++
64 +If the credential helper used specifies an authentication scheme (i.e., via the
65 +`authtype` field), that value will be used; if a username and password is
66 +provided without a scheme, then Basic authentication is used. The value of the
67 +option determines the scheme requested from the helper. Possible values are:
68 ++
69 +--
70 +* `basic` - Request Basic authentication from the helper.
71 +* `auto` - Allow the helper to pick an appropriate scheme.
72 +* `none` - Disable proactive authentication.
73 +--
74 ++
75 +Note that TLS should always be used with this configuration, since otherwise it
76 +is easy to accidentally expose plaintext credentials if Basic authentication
77 +is selected.
78 +
79 http.delegation::
80 Control GSSAPI credential delegation. The delegation is disabled
81 by default in libcurl since version 7.21.7. Set parameter to tell
http.c
+56 -6
@@ -106,12 +106,19 @@ static struct {
106 };
107 #endif
108
109 +enum proactive_auth {
110 + PROACTIVE_AUTH_NONE = 0,
111 + PROACTIVE_AUTH_IF_CREDENTIALS,
112 + PROACTIVE_AUTH_AUTO,
113 + PROACTIVE_AUTH_BASIC,
114 +};
115 +
116 static struct credential proxy_auth = CREDENTIAL_INIT;
117 static const char *curl_proxyuserpwd;
118 static char *curl_cookie_file;
119 static int curl_save_cookies;
120 struct credential http_auth = CREDENTIAL_INIT;
114 -static int http_proactive_auth;
121 +static enum proactive_auth http_proactive_auth;
122 static char *user_agent;
123 static int curl_empty_auth = -1;
124
@@ -146,6 +153,12 @@ static int http_schannel_check_revoke = 1;
153 */
154 static int http_schannel_use_ssl_cainfo;
155
156 +static int always_auth_proactively(void)
157 +{
158 + return http_proactive_auth != PROACTIVE_AUTH_NONE &&
159 + http_proactive_auth != PROACTIVE_AUTH_IF_CREDENTIALS;
160 +}
161 +
162 size_t fread_buffer(char *ptr, size_t eltsize, size_t nmemb, void *buffer_)
163 {
164 size_t size = eltsize * nmemb;
@@ -537,6 +550,20 @@ static int http_options(const char *var, const char *value,
550 return 0;
551 }
552
553 + if (!strcmp("http.proactiveauth", var)) {
554 + if (!value)
555 + return config_error_nonbool(var);
556 + if (!strcmp(value, "auto"))
557 + http_proactive_auth = PROACTIVE_AUTH_AUTO;
558 + else if (!strcmp(value, "basic"))
559 + http_proactive_auth = PROACTIVE_AUTH_BASIC;
560 + else if (!strcmp(value, "none"))
561 + http_proactive_auth = PROACTIVE_AUTH_NONE;
562 + else
563 + warning(_("Unknown value for http.proactiveauth"));
564 + return 0;
565 + }
566 +
567 /* Fall back on the default ones */
568 return git_default_config(var, value, ctx, data);
569 }
@@ -578,14 +605,29 @@ static void init_curl_http_auth(CURL *result)
605 {
606 if ((!http_auth.username || !*http_auth.username) &&
607 (!http_auth.credential || !*http_auth.credential)) {
581 - if (curl_empty_auth_enabled())
608 + int empty_auth = curl_empty_auth_enabled();
609 + if ((empty_auth != -1 && !always_auth_proactively()) || empty_auth == 1) {
610 curl_easy_setopt(result, CURLOPT_USERPWD, ":");
583 - return;
611 + return;
612 + } else if (!always_auth_proactively()) {
613 + return;
614 + } else if (http_proactive_auth == PROACTIVE_AUTH_BASIC) {
615 + strvec_push(&http_auth.wwwauth_headers, "Basic");
616 + }
617 }
618
619 credential_fill(&http_auth, 1);
620
621 if (http_auth.password) {
622 + if (always_auth_proactively()) {
623 + /*
624 + * We got a credential without an authtype and we don't
625 + * know what's available. Since our only two options at
626 + * the moment are auto (which defaults to basic) and
627 + * basic, use basic for now.
628 + */
629 + curl_easy_setopt(result, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
630 + }
631 curl_easy_setopt(result, CURLOPT_USERNAME, http_auth.username);
632 curl_easy_setopt(result, CURLOPT_PASSWORD, http_auth.password);
633 }
@@ -1048,7 +1090,7 @@ static CURL *get_curl_handle(void)
1090 #endif
1091 }
1092
1051 - if (http_proactive_auth)
1093 + if (http_proactive_auth != PROACTIVE_AUTH_NONE)
1094 init_curl_http_auth(result);
1095
1096 if (getenv("GIT_SSL_VERSION"))
@@ -1292,7 +1334,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
1334 if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK)
1335 die("curl_global_init failed");
1336
1295 - http_proactive_auth = proactive_auth;
1337 + if (proactive_auth && http_proactive_auth == PROACTIVE_AUTH_NONE)
1338 + http_proactive_auth = PROACTIVE_AUTH_IF_CREDENTIALS;
1339
1340 if (remote && remote->http_proxy)
1341 curl_http_proxy = xstrdup(remote->http_proxy);
@@ -1788,6 +1831,8 @@ static int handle_curl_result(struct slot_results *results)
1831 return HTTP_REAUTH;
1832 }
1833 credential_reject(&http_auth);
1834 + if (always_auth_proactively())
1835 + http_proactive_auth = PROACTIVE_AUTH_NONE;
1836 return HTTP_NOAUTH;
1837 } else {
1838 http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
@@ -2184,7 +2229,12 @@ static int http_request_reauth(const char *url,
2229 struct http_get_options *options)
2230 {
2231 int i = 3;
2187 - int ret = http_request(url, result, target, options);
2232 + int ret;
2233 +
2234 + if (always_auth_proactively())
2235 + credential_fill(&http_auth, 1);
2236 +
2237 + ret = http_request(url, result, target, options);
2238
2239 if (ret != HTTP_OK && ret != HTTP_REAUTH)
2240 return ret;
t/t5563-simple-http-auth.sh
+116
@@ -178,6 +178,122 @@ test_expect_success 'access using basic auth invalid credentials' '
178 EOF
179 '
180
181 +test_expect_success 'access using basic proactive auth' '
182 + test_when_finished "per_test_cleanup" &&
183 +
184 + set_credential_reply get <<-EOF &&
185 + username=alice
186 + password=secret-passwd
187 + EOF
188 +
189 + # Basic base64(alice:secret-passwd)
190 + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
191 + id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
192 + EOF
193 +
194 + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
195 + id=1 status=200
196 + id=default status=403
197 + EOF
198 +
199 + test_config_global credential.helper test-helper &&
200 + test_config_global http.proactiveAuth basic &&
201 + git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
202 +
203 + expect_credential_query get <<-EOF &&
204 + capability[]=authtype
205 + capability[]=state
206 + protocol=http
207 + host=$HTTPD_DEST
208 + wwwauth[]=Basic
209 + EOF
210 +
211 + expect_credential_query store <<-EOF
212 + protocol=http
213 + host=$HTTPD_DEST
214 + username=alice
215 + password=secret-passwd
216 + EOF
217 +'
218 +
219 +test_expect_success 'access using auto proactive auth with basic default' '
220 + test_when_finished "per_test_cleanup" &&
221 +
222 + set_credential_reply get <<-EOF &&
223 + username=alice
224 + password=secret-passwd
225 + EOF
226 +
227 + # Basic base64(alice:secret-passwd)
228 + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
229 + id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
230 + EOF
231 +
232 + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
233 + id=1 status=200
234 + id=default status=403
235 + EOF
236 +
237 + test_config_global credential.helper test-helper &&
238 + test_config_global http.proactiveAuth auto &&
239 + git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
240 +
241 + expect_credential_query get <<-EOF &&
242 + capability[]=authtype
243 + capability[]=state
244 + protocol=http
245 + host=$HTTPD_DEST
246 + EOF
247 +
248 + expect_credential_query store <<-EOF
249 + protocol=http
250 + host=$HTTPD_DEST
251 + username=alice
252 + password=secret-passwd
253 + EOF
254 +'
255 +
256 +test_expect_success 'access using auto proactive auth with authtype from credential helper' '
257 + test_when_finished "per_test_cleanup" &&
258 +
259 + set_credential_reply get <<-EOF &&
260 + capability[]=authtype
261 + authtype=Bearer
262 + credential=YS1naXQtdG9rZW4=
263 + EOF
264 +
265 + # Basic base64(a-git-token)
266 + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
267 + id=1 creds=Bearer YS1naXQtdG9rZW4=
268 + EOF
269 +
270 + CHALLENGE="$HTTPD_ROOT_PATH/custom-auth.challenge" &&
271 +
272 + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
273 + id=1 status=200
274 + id=default status=403
275 + EOF
276 +
277 + test_config_global credential.helper test-helper &&
278 + test_config_global http.proactiveAuth auto &&
279 + git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
280 +
281 + expect_credential_query get <<-EOF &&
282 + capability[]=authtype
283 + capability[]=state
284 + protocol=http
285 + host=$HTTPD_DEST
286 + EOF
287 +
288 + expect_credential_query store <<-EOF
289 + capability[]=authtype
290 + authtype=Bearer
291 + credential=YS1naXQtdG9rZW4=
292 + protocol=http
293 + host=$HTTPD_DEST
294 + EOF
295 +'
296 +
297 test_expect_success 'access using basic auth with extra challenges' '
298 test_when_finished "per_test_cleanup" &&
299