* [PATCH 0/1] Proactive authentication over HTTP
@ 2024-06-28 0:27 brian m. carlson
2024-06-28 0:27 ` [PATCH 1/1] http: allow authenticating proactively brian m. carlson
2024-07-04 0:17 ` [PATCH v2 0/1] Proactive authentication over HTTP brian m. carlson
0 siblings, 2 replies; 12+ messages in thread
From: brian m. carlson @ 2024-06-28 0:27 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
Currently Git only sends authentication over HTTP once it's received a
401 response from the server. This series allows users to indicate that
they (or the credential helper) know what kind of authentication is to
be performed and avoid the extra round trip.
This has a couple of use cases:
* Some connections are high-latency or potentially unreliable and the
extra round trip is expensive or flaky, so avoiding it can be
desirable.
* Users can authenticate even to public repositories for fetches and
clones, such as to access higher rate limits or to allow better
identification of the requesting party. This can be useful to help
identify, say, an internal service that is requesting excessive
amounts of resources and throttle it appropriately. (An incident
involving this very situation is the impetus for this series.)
* Some environments have very unusual proxy setups which require all
requests to be authenticated, and this might be useful there. (This
is not a goal of this series and not a problem I'm generally
interested in solving, but it happens to benefit those people as well,
so I thought I'd mention it.)
Note that the types of authentication for which we can do proactive auth
are relatively limited. Of the standard forms provided by libcurl, I
believe only Basic is possible, because the others require a nonce or
challenge from the server that is used in the computation of the
authentication value. Due to the new credential functionality recently
introduced, Bearer is also a useful possibility.
brian m. carlson (1):
http: allow authenticating proactively
Documentation/config/http.txt | 15 +++++
http.c | 59 +++++++++++++++--
t/t5563-simple-http-auth.sh | 116 ++++++++++++++++++++++++++++++++++
3 files changed, 184 insertions(+), 6 deletions(-)
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 1/1] http: allow authenticating proactively
2024-06-28 0:27 [PATCH 0/1] Proactive authentication over HTTP brian m. carlson
@ 2024-06-28 0:27 ` brian m. carlson
2024-06-28 18:16 ` Junio C Hamano
2024-07-04 0:17 ` [PATCH v2 0/1] Proactive authentication over HTTP brian m. carlson
1 sibling, 1 reply; 12+ messages in thread
From: brian m. carlson @ 2024-06-28 0:27 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
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.
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>
---
Documentation/config/http.txt | 15 +++++
http.c | 59 +++++++++++++++--
t/t5563-simple-http-auth.sh | 116 ++++++++++++++++++++++++++++++++++
3 files changed, 184 insertions(+), 6 deletions(-)
diff --git a/Documentation/config/http.txt b/Documentation/config/http.txt
index 2d4e0c9b86..2bacb2b862 100644
--- a/Documentation/config/http.txt
+++ b/Documentation/config/http.txt
@@ -56,6 +56,21 @@ http.emptyAuth::
a username in the URL, as libcurl normally requires a username for
authentication.
+http.proactiveAuth::
+ Attempt authentication without first making an unauthenticated attempt and
+ receiving a 401 response. This can be used to ensure that all requests are
+ authenticated. If `http.emptyAuth` is set to true, this value has no effect.
++
+If the credential helper used specifies an authentication scheme (i.e., via the
+`authtype` field), that value will be used; if a username and password is
+provided without a scheme, then Basic authentication is used. The value of the
+option determines the scheme requested from the helper. Possible values are:
++
+--
+* `basic` - Request Basic authentication from the helper.
+* `auto` - Don't request any scheme from the helper.
+--
+
http.delegation::
Control GSSAPI credential delegation. The delegation is disabled
by default in libcurl since version 7.21.7. Set parameter to tell
diff --git a/http.c b/http.c
index 2dea2d03da..2e54eddb45 100644
--- a/http.c
+++ b/http.c
@@ -106,12 +106,19 @@ static struct {
};
#endif
+enum proactive_auth {
+ PROACTIVE_AUTH_NONE,
+ PROACTIVE_AUTH_IF_CREDENTIALS,
+ PROACTIVE_AUTH_AUTO,
+ PROACTIVE_AUTH_BASIC,
+};
+
static struct credential proxy_auth = CREDENTIAL_INIT;
static const char *curl_proxyuserpwd;
static char *curl_cookie_file;
static int curl_save_cookies;
struct credential http_auth = CREDENTIAL_INIT;
-static int http_proactive_auth;
+static enum proactive_auth http_proactive_auth;
static char *user_agent;
static int curl_empty_auth = -1;
@@ -146,6 +153,11 @@ static int http_schannel_check_revoke = 1;
*/
static int http_schannel_use_ssl_cainfo;
+static int always_auth_proactively(void)
+{
+ return http_proactive_auth != PROACTIVE_AUTH_NONE && http_proactive_auth != PROACTIVE_AUTH_IF_CREDENTIALS;
+}
+
size_t fread_buffer(char *ptr, size_t eltsize, size_t nmemb, void *buffer_)
{
size_t size = eltsize * nmemb;
@@ -537,6 +549,18 @@ static int http_options(const char *var, const char *value,
return 0;
}
+ if (!strcmp("http.proactiveauth", var)) {
+ if (value && !strcmp(value, "auto"))
+ http_proactive_auth = PROACTIVE_AUTH_AUTO;
+ else if (value && !strcmp(value, "basic"))
+ http_proactive_auth = PROACTIVE_AUTH_BASIC;
+ else if (!value)
+ http_proactive_auth = PROACTIVE_AUTH_NONE;
+ else
+ warning(_("Unknown value %s for http.proactiveauth"), value);
+ return 0;
+ }
+
/* Fall back on the default ones */
return git_default_config(var, value, ctx, data);
}
@@ -578,14 +602,29 @@ static void init_curl_http_auth(CURL *result)
{
if ((!http_auth.username || !*http_auth.username) &&
(!http_auth.credential || !*http_auth.credential)) {
- if (curl_empty_auth_enabled())
+ int empty_auth = curl_empty_auth_enabled();
+ if ((empty_auth != -1 && !always_auth_proactively()) || empty_auth == 1) {
curl_easy_setopt(result, CURLOPT_USERPWD, ":");
- return;
+ return;
+ } else if (!always_auth_proactively()) {
+ return;
+ } else if (http_proactive_auth == PROACTIVE_AUTH_BASIC) {
+ strvec_push(&http_auth.wwwauth_headers, "Basic");
+ }
}
credential_fill(&http_auth, 1);
if (http_auth.password) {
+ if (always_auth_proactively()) {
+ /*
+ * We got a credential without an authtype and we don't
+ * know what's available. Since our only two options at
+ * the moment are auto (which defaults to basic) and
+ * basic, use basic for now.
+ */
+ curl_easy_setopt(result, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
+ }
curl_easy_setopt(result, CURLOPT_USERNAME, http_auth.username);
curl_easy_setopt(result, CURLOPT_PASSWORD, http_auth.password);
}
@@ -1048,7 +1087,7 @@ static CURL *get_curl_handle(void)
#endif
}
- if (http_proactive_auth)
+ if (http_proactive_auth != PROACTIVE_AUTH_NONE)
init_curl_http_auth(result);
if (getenv("GIT_SSL_VERSION"))
@@ -1292,7 +1331,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK)
die("curl_global_init failed");
- http_proactive_auth = proactive_auth;
+ if (proactive_auth && http_proactive_auth == PROACTIVE_AUTH_NONE)
+ http_proactive_auth = PROACTIVE_AUTH_IF_CREDENTIALS;
if (remote && remote->http_proxy)
curl_http_proxy = xstrdup(remote->http_proxy);
@@ -1788,6 +1828,8 @@ static int handle_curl_result(struct slot_results *results)
return HTTP_REAUTH;
}
credential_reject(&http_auth);
+ if (always_auth_proactively())
+ http_proactive_auth = PROACTIVE_AUTH_NONE;
return HTTP_NOAUTH;
} else {
http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
@@ -2184,7 +2226,12 @@ static int http_request_reauth(const char *url,
struct http_get_options *options)
{
int i = 3;
- int ret = http_request(url, result, target, options);
+ int ret;
+
+ if (always_auth_proactively())
+ credential_fill(&http_auth, 1);
+
+ ret = http_request(url, result, target, options);
if (ret != HTTP_OK && ret != HTTP_REAUTH)
return ret;
diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh
index 4af796de67..ba03f6a09f 100755
--- a/t/t5563-simple-http-auth.sh
+++ b/t/t5563-simple-http-auth.sh
@@ -178,6 +178,122 @@ test_expect_success 'access using basic auth invalid credentials' '
EOF
'
+test_expect_success 'access using basic proactive auth' '
+ 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 status=403
+ EOF
+
+ test_config_global credential.helper test-helper &&
+ test_config_global http.proactiveAuth basic &&
+ git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+ expect_credential_query get <<-EOF &&
+ capability[]=authtype
+ capability[]=state
+ protocol=http
+ host=$HTTPD_DEST
+ wwwauth[]=Basic
+ EOF
+
+ expect_credential_query store <<-EOF
+ protocol=http
+ host=$HTTPD_DEST
+ username=alice
+ password=secret-passwd
+ EOF
+'
+
+test_expect_success 'access using auto proactive auth with basic default' '
+ 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 status=403
+ EOF
+
+ test_config_global credential.helper test-helper &&
+ test_config_global http.proactiveAuth auto &&
+ git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+ expect_credential_query get <<-EOF &&
+ capability[]=authtype
+ capability[]=state
+ protocol=http
+ host=$HTTPD_DEST
+ EOF
+
+ expect_credential_query store <<-EOF
+ protocol=http
+ host=$HTTPD_DEST
+ username=alice
+ password=secret-passwd
+ EOF
+'
+
+test_expect_success 'access using auto proactive auth with authtype from credential helper' '
+ test_when_finished "per_test_cleanup" &&
+
+ set_credential_reply get <<-EOF &&
+ capability[]=authtype
+ authtype=Bearer
+ credential=YS1naXQtdG9rZW4=
+ EOF
+
+ # Basic base64(a-git-token)
+ cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
+ id=1 creds=Bearer YS1naXQtdG9rZW4=
+ EOF
+
+ CHALLENGE="$HTTPD_ROOT_PATH/custom-auth.challenge" &&
+
+ cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
+ id=1 status=200
+ id=default status=403
+ EOF
+
+ test_config_global credential.helper test-helper &&
+ test_config_global http.proactiveAuth auto &&
+ git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+ expect_credential_query get <<-EOF &&
+ capability[]=authtype
+ capability[]=state
+ protocol=http
+ host=$HTTPD_DEST
+ EOF
+
+ expect_credential_query store <<-EOF
+ capability[]=authtype
+ authtype=Bearer
+ credential=YS1naXQtdG9rZW4=
+ protocol=http
+ host=$HTTPD_DEST
+ EOF
+'
+
test_expect_success 'access using basic auth with extra challenges' '
test_when_finished "per_test_cleanup" &&
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH 1/1] http: allow authenticating proactively
2024-06-28 0:27 ` [PATCH 1/1] http: allow authenticating proactively brian m. carlson
@ 2024-06-28 18:16 ` Junio C Hamano
2024-06-28 22:00 ` brian m. carlson
0 siblings, 1 reply; 12+ messages in thread
From: Junio C Hamano @ 2024-06-28 18:16 UTC (permalink / raw)
To: brian m. carlson; +Cc: git
"brian m. carlson" <sandals@crustytoothpaste.net> writes:
> diff --git a/Documentation/config/http.txt b/Documentation/config/http.txt
> index 2d4e0c9b86..2bacb2b862 100644
> --- a/Documentation/config/http.txt
> +++ b/Documentation/config/http.txt
> @@ -56,6 +56,21 @@ http.emptyAuth::
> a username in the URL, as libcurl normally requires a username for
> authentication.
>
> +http.proactiveAuth::
> + Attempt authentication without first making an unauthenticated attempt and
> + receiving a 401 response. This can be used to ensure that all requests are
> + authenticated. If `http.emptyAuth` is set to true, this value has no effect.
Well written.
> ++
> +If the credential helper used specifies an authentication scheme (i.e., via the
> +`authtype` field), that value will be used; if a username and password is
> +provided without a scheme, then Basic authentication is used. The value of the
> +option determines the scheme requested from the helper. Possible values are:
> ++
> +--
> +* `basic` - Request Basic authentication from the helper.
> +* `auto` - Don't request any scheme from the helper.
> +--
What does "don't request" exactly mean? It is not like we are
telling the helper "Don't give us anything", right? Are we telling
the helper "Give us any username/password for the URL in any
authentication scheme you know about?"
As we are not getting an initial "401 Unauthorized", which is a good
channel to convey WWW-Authenticate, Digest is not available to us in
this context; we may end up using Basic---if the other side then
says "No, I do not like basic, please use Diest in this realm with
this nonce" with a "401 Unauthorized" with WWW-Authenticate, then
all we gained was a chance to expose the username/password in
plaintext (ok, that's still TLS protected in practice so it may not
be a huge deal). Hopefully that wouldn't be a problem, but perhaps
we would want to suggest that this mechanism should primarily be
used when the user _knows_ that the other side is happy accepting
you with Basic, or something, in the documentation?
> diff --git a/http.c b/http.c
> index 2dea2d03da..2e54eddb45 100644
> --- a/http.c
> +++ b/http.c
> @@ -106,12 +106,19 @@ static struct {
> };
> #endif
>
> +enum proactive_auth {
> + PROACTIVE_AUTH_NONE,
> + PROACTIVE_AUTH_IF_CREDENTIALS,
> + PROACTIVE_AUTH_AUTO,
> + PROACTIVE_AUTH_BASIC,
> +};
PROACTIVE_AUTH_NONE being at the first position to be assigned 0
in this enum has significance, because ...
> static struct credential proxy_auth = CREDENTIAL_INIT;
> static const char *curl_proxyuserpwd;
> static char *curl_cookie_file;
> static int curl_save_cookies;
> struct credential http_auth = CREDENTIAL_INIT;
> -static int http_proactive_auth;
> +static enum proactive_auth http_proactive_auth;
... this implicitly initializes the variable to *_NONE and we do
rely on that value. It may clarify what we are doing, if we are a
bit more explicit, i.e.,
enum proactive_auth {
PROACTIVE_AUTH_NONE = 0,
...
It would give a hint to future developers that they shouldn't
reorder the enum def without thinking.
> +static int always_auth_proactively(void)
> +{
> + return http_proactive_auth != PROACTIVE_AUTH_NONE && http_proactive_auth != PROACTIVE_AUTH_IF_CREDENTIALS;
> +}
An overly long line.
> size_t fread_buffer(char *ptr, size_t eltsize, size_t nmemb, void *buffer_)
> {
> size_t size = eltsize * nmemb;
> @@ -537,6 +549,18 @@ static int http_options(const char *var, const char *value,
> return 0;
> }
>
> + if (!strcmp("http.proactiveauth", var)) {
> + if (value && !strcmp(value, "auto"))
> + http_proactive_auth = PROACTIVE_AUTH_AUTO;
> + else if (value && !strcmp(value, "basic"))
> + http_proactive_auth = PROACTIVE_AUTH_BASIC;
> + else if (!value)
> + http_proactive_auth = PROACTIVE_AUTH_NONE;
This is how you "reset" the variable that is set in lower-precedence
configuration source back to the default, but use of "I exist
therefore I represent true without giving any value" for that
purpose is rather unusual. Even if this were about resetting a
multi-valued configuration variable to empty, don't we usually allow
an empty string to serve that purpose as well?
And more importantly we apparently do not use this variable as
multi-valued one---the above callback is a bog-standard "last one
wins" single value variable.
So instead of using the "I exist" true, I think it is a lot easier
to see if you used an explicit string "none" for this purpose.
If you insist to use "I exist" true, checking it upfront would allow
you (and future developers) to lose many "value&&", i.e.,
if (!value || !*value)
... reset ...;
else if (!strcmp(value, "auto"))
...
but I do not see a strong reason to use the !value to begin with. I
suspect that the usual
if (!value)
return config_error_nonbool(var);
elseif (!strcmp(value, "none"))
http_proactive_auth = PROACTIVE_AUTH_NONE;
else if ...
would be easier to read and maintain.
In any case, the documentation update above failed to mention the
mechanism to reset to the state that is equivalent to an
unconfigured state, which needs an update. If we document what you
do in your patch, it would read something like this, perhaps.
+
An earlier setting made in configuration files with lower-precedence
can be overridden by setting it to a valueless truth, e.g.,
--
[http]
proactiveAuth ; no "= value" needed here.
--
And that is awkward, which is one of the reasons why I would prefer
an explicit "none" (or "default" perhaps, if there were some reason
you wanted to avoid it).
> + else
> + warning(_("Unknown value %s for http.proactiveauth"), value);
> + return 0;
> + }
> +
> @@ -578,14 +602,29 @@ static void init_curl_http_auth(CURL *result)
> {
> if ((!http_auth.username || !*http_auth.username) &&
> (!http_auth.credential || !*http_auth.credential)) {
> - if (curl_empty_auth_enabled())
> + int empty_auth = curl_empty_auth_enabled();
> + if ((empty_auth != -1 && !always_auth_proactively()) || empty_auth == 1) {
> curl_easy_setopt(result, CURLOPT_USERPWD, ":");
> - return;
> + return;
> + } else if (!always_auth_proactively()) {
> + return;
> + } else if (http_proactive_auth == PROACTIVE_AUTH_BASIC) {
> + strvec_push(&http_auth.wwwauth_headers, "Basic");
> + }
> }
When http.proactiveauth explicitly says "basic", we push "Basic" to
the strvec here.
> credential_fill(&http_auth, 1);
>
> if (http_auth.password) {
> + if (always_auth_proactively()) {
But when http.proactiveauth is set to either "auto" or "basic",
always_auth_proactively() returns true (because it is not set to
*_NONE and it is not *_IF_CREDENTIALS) and we come here, to do
curl_easy_setop() to use basic anyway.
> + /*
> + * We got a credential without an authtype and we don't
> + * know what's available. Since our only two options at
> + * the moment are auto (which defaults to basic) and
> + * basic, use basic for now.
> + */
> + curl_easy_setopt(result, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
So users with "auto" will not see "Basic" in .wwwauth_headers
strvec, while those with "basic" will. Is this intended? What is
the expected difference in behaviour coming from this difference?
Or am I simply reading the code incorrectly and not understanding
what happens before the control reaches fwrite_wwwauth()?
> + }
> curl_easy_setopt(result, CURLOPT_USERNAME, http_auth.username);
> curl_easy_setopt(result, CURLOPT_PASSWORD, http_auth.password);
> }
> @@ -1048,7 +1087,7 @@ static CURL *get_curl_handle(void)
> #endif
> }
>
> - if (http_proactive_auth)
> + if (http_proactive_auth != PROACTIVE_AUTH_NONE)
> init_curl_http_auth(result);
Makes sense.
> @@ -1292,7 +1331,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
> if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK)
> die("curl_global_init failed");
>
> - http_proactive_auth = proactive_auth;
> + if (proactive_auth && http_proactive_auth == PROACTIVE_AUTH_NONE)
> + http_proactive_auth = PROACTIVE_AUTH_IF_CREDENTIALS;
The webdav http-push is the only caller of http_init() with the
proactive_auth parameter set to true. In such a case, if we do not
see the configuration variable left/set to the default, we force the
"if-credentials" behaviour. IOW, if http.proactive_auth is set to
some value, we leave it as-is even for http-push (which wants to use
the "if-credentials" behaviour).
I wonder if there are cases where you would want to do the
proactive-auth for fetches (which you couldn't do before), but you
want to drive http-push with if-credentials, and if so, if this
changes the behaviour in a way that is visible to the end-user.
Or perhaps http-push over webdav outlived its usefulness and we
should send a patch to Documentation/BreakingChanges.txt to declare
its deprecation and removal, in which case there is nothing to
wonder or be worried here ;-)
> @@ -1788,6 +1828,8 @@ static int handle_curl_result(struct slot_results *results)
> return HTTP_REAUTH;
> }
> credential_reject(&http_auth);
> + if (always_auth_proactively())
> + http_proactive_auth = PROACTIVE_AUTH_NONE;
Once we see a failure, there is not much point in doing the
proactive-auth to the same destination in requests we are going to
make from now on. Makes sense.
> @@ -2184,7 +2226,12 @@ static int http_request_reauth(const char *url,
> struct http_get_options *options)
> {
> int i = 3;
> - int ret = http_request(url, result, target, options);
> + int ret;
> +
> + if (always_auth_proactively())
> + credential_fill(&http_auth, 1);
> +
> + ret = http_request(url, result, target, options);
>
> if (ret != HTTP_OK && ret != HTTP_REAUTH)
> return ret;
OK, if we need to auth proactively, of course we need to fill the
credential before making a request. Makes sense.
Thanks.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 1/1] http: allow authenticating proactively
2024-06-28 18:16 ` Junio C Hamano
@ 2024-06-28 22:00 ` brian m. carlson
2024-06-28 22:18 ` Junio C Hamano
0 siblings, 1 reply; 12+ messages in thread
From: brian m. carlson @ 2024-06-28 22:00 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
[-- Attachment #1: Type: text/plain, Size: 10562 bytes --]
On 2024-06-28 at 18:16:43, Junio C Hamano wrote:
> "brian m. carlson" <sandals@crustytoothpaste.net> writes:
>
> > diff --git a/Documentation/config/http.txt b/Documentation/config/http.txt
> > index 2d4e0c9b86..2bacb2b862 100644
> > --- a/Documentation/config/http.txt
> > +++ b/Documentation/config/http.txt
> > @@ -56,6 +56,21 @@ http.emptyAuth::
> > a username in the URL, as libcurl normally requires a username for
> > authentication.
> >
> > +http.proactiveAuth::
> > + Attempt authentication without first making an unauthenticated attempt and
> > + receiving a 401 response. This can be used to ensure that all requests are
> > + authenticated. If `http.emptyAuth` is set to true, this value has no effect.
>
> Well written.
>
> > ++
> > +If the credential helper used specifies an authentication scheme (i.e., via the
> > +`authtype` field), that value will be used; if a username and password is
> > +provided without a scheme, then Basic authentication is used. The value of the
> > +option determines the scheme requested from the helper. Possible values are:
> > ++
> > +--
> > +* `basic` - Request Basic authentication from the helper.
> > +* `auto` - Don't request any scheme from the helper.
> > +--
>
> What does "don't request" exactly mean? It is not like we are
> telling the helper "Don't give us anything", right? Are we telling
> the helper "Give us any username/password for the URL in any
> authentication scheme you know about?"
It means we don't send a `wwwauth[]` entry in the request. We are
giving the helper carte blanche to decide what scheme is best (maybe it
knows we want Bearer, for example).
> As we are not getting an initial "401 Unauthorized", which is a good
> channel to convey WWW-Authenticate, Digest is not available to us in
> this context; we may end up using Basic---if the other side then
> says "No, I do not like basic, please use Diest in this realm with
> this nonce" with a "401 Unauthorized" with WWW-Authenticate, then
> all we gained was a chance to expose the username/password in
> plaintext (ok, that's still TLS protected in practice so it may not
> be a huge deal). Hopefully that wouldn't be a problem, but perhaps
> we would want to suggest that this mechanism should primarily be
> used when the user _knows_ that the other side is happy accepting
> you with Basic, or something, in the documentation?
I think it might suffice to say that TLS should always be used with this
configuration. The documentation is very clear that it will use Basic
if no scheme is provided, so presumably the user is either very
confident in their helper configuration (e.g., they know for certain
that the helper will always emit an `authtype` field because they've so
configured it), or they're willing to accept the risk that the password
is sent with basic auth.
> PROACTIVE_AUTH_NONE being at the first position to be assigned 0
> in this enum has significance, because ...
>
> > static struct credential proxy_auth = CREDENTIAL_INIT;
> > static const char *curl_proxyuserpwd;
> > static char *curl_cookie_file;
> > static int curl_save_cookies;
> > struct credential http_auth = CREDENTIAL_INIT;
> > -static int http_proactive_auth;
> > +static enum proactive_auth http_proactive_auth;
>
> ... this implicitly initializes the variable to *_NONE and we do
> rely on that value. It may clarify what we are doing, if we are a
> bit more explicit, i.e.,
>
> enum proactive_auth {
> PROACTIVE_AUTH_NONE = 0,
> ...
>
> It would give a hint to future developers that they shouldn't
> reorder the enum def without thinking.
Sure, that seems like a good idea.
> > +static int always_auth_proactively(void)
> > +{
> > + return http_proactive_auth != PROACTIVE_AUTH_NONE && http_proactive_auth != PROACTIVE_AUTH_IF_CREDENTIALS;
> > +}
>
> An overly long line.
Will fix.
> > size_t fread_buffer(char *ptr, size_t eltsize, size_t nmemb, void *buffer_)
> > {
> > size_t size = eltsize * nmemb;
> > @@ -537,6 +549,18 @@ static int http_options(const char *var, const char *value,
> > return 0;
> > }
> >
> > + if (!strcmp("http.proactiveauth", var)) {
> > + if (value && !strcmp(value, "auto"))
> > + http_proactive_auth = PROACTIVE_AUTH_AUTO;
> > + else if (value && !strcmp(value, "basic"))
> > + http_proactive_auth = PROACTIVE_AUTH_BASIC;
> > + else if (!value)
> > + http_proactive_auth = PROACTIVE_AUTH_NONE;
>
> This is how you "reset" the variable that is set in lower-precedence
> configuration source back to the default, but use of "I exist
> therefore I represent true without giving any value" for that
> purpose is rather unusual. Even if this were about resetting a
> multi-valued configuration variable to empty, don't we usually allow
> an empty string to serve that purpose as well?
>
> And more importantly we apparently do not use this variable as
> multi-valued one---the above callback is a bog-standard "last one
> wins" single value variable.
>
> So instead of using the "I exist" true, I think it is a lot easier
> to see if you used an explicit string "none" for this purpose.
Okay, I think that seems reasonable.
> > credential_fill(&http_auth, 1);
> >
> > if (http_auth.password) {
> > + if (always_auth_proactively()) {
>
> But when http.proactiveauth is set to either "auto" or "basic",
> always_auth_proactively() returns true (because it is not set to
> *_NONE and it is not *_IF_CREDENTIALS) and we come here, to do
> curl_easy_setop() to use basic anyway.
Yes, iff there's a password. The reason is that it isn't sufficient for
libcurl to have a username and password to send authentication. Either
we have to explicitly fill in the Authorization header (which we do with
`authtype` and `credential`) or we have to specify exactly one
authentication scheme; otherwise, libcurl doesn't auth. Note that
entire branch is not taken if we have `authtype` and `credential`; that
is handled elsewhere.
This is subtle, so I'll mention it in the commit message on a reroll.
> > + /*
> > + * We got a credential without an authtype and we don't
> > + * know what's available. Since our only two options at
> > + * the moment are auto (which defaults to basic) and
> > + * basic, use basic for now.
> > + */
> > + curl_easy_setopt(result, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
>
> So users with "auto" will not see "Basic" in .wwwauth_headers
> strvec, while those with "basic" will. Is this intended? What is
> the expected difference in behaviour coming from this difference?
Yes. The difference is that a credential helper supporting multiple
schemes will (or at least _should_) read the `wwwauth[]` entry and say,
"Oh, the server only supports Basic auth, so I must send Basic auth",
whereas not sending anything tells it that it may choose freely. For
example, a helper might legitimately return Bearer for the case where we
don't send a `wwwauth[]` entry (which would be handled differently),
whereas it probably would not want to do that when the option is set to
`basic`.
> > @@ -1292,7 +1331,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
> > if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK)
> > die("curl_global_init failed");
> >
> > - http_proactive_auth = proactive_auth;
> > + if (proactive_auth && http_proactive_auth == PROACTIVE_AUTH_NONE)
> > + http_proactive_auth = PROACTIVE_AUTH_IF_CREDENTIALS;
>
> The webdav http-push is the only caller of http_init() with the
> proactive_auth parameter set to true. In such a case, if we do not
> see the configuration variable left/set to the default, we force the
> "if-credentials" behaviour. IOW, if http.proactive_auth is set to
> some value, we leave it as-is even for http-push (which wants to use
> the "if-credentials" behaviour).
Correct. When this option was a boolean, the if-credentials behaviour
was what happened when the option was true.
> I wonder if there are cases where you would want to do the
> proactive-auth for fetches (which you couldn't do before), but you
> want to drive http-push with if-credentials, and if so, if this
> changes the behaviour in a way that is visible to the end-user.
I think that's remarkably unlikely. Pushing effectively always requires
some sort of authentication because you don't want random people
modifying your codebase/writing project/other collection of bits.
That's like security 101.
Therefore, you're _going_ to authenticate sooner or later on push. The
only case where if-credentials is helpful is what I mentioned about
t5540, where we actually have an open push. That isn't a real world use
case for security reasons, but using TLS certificates to authenticate
and not otherwise sending credentials, which _looks_ the same to this
code, is legitimate.
However, you wouldn't configure TLS certificates for push only and use
regular auth for fetch, since you'd already have the TLS certificate on
the client and you'd have already requested the certificate when
establishing the TLS connection. Moreover, there's no reason that you
_would_ want to auth for fetches or clones and not for pushes; if your
goal is to make it easier to throttle heavy users, say, pushes are more
expensive because you have to run pre-receive hooks and fsck the data.
Perhaps someone has come up with such an unusual use case and is still
using the WebDAV approach, and in such a case, we can simply let them
send a nice patch to implement http.pushProactiveAuth for that case. In
the mean time, I'm happy to let one option control both.
> Or perhaps http-push over webdav outlived its usefulness and we
> should send a patch to Documentation/BreakingChanges.txt to declare
> its deprecation and removal, in which case there is nothing to
> wonder or be worried here ;-)
I think cgit still supports it because it's easier to implement in a
CGI-style piece of code, and maybe that's useful. While I don't use it,
it can be easier to set up in certain environments, and I don't really
want to be the one to announce its demise.
Anyway, thanks for the review, and I'll try to get a reroll out
relatively soon. I have a couple days of vacation with the Canada Day
holiday, so hopefully I can find some time to get one out then.
--
brian m. carlson (they/them or he/him)
Toronto, Ontario, CA
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 262 bytes --]
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 1/1] http: allow authenticating proactively
2024-06-28 22:00 ` brian m. carlson
@ 2024-06-28 22:18 ` Junio C Hamano
2024-06-29 0:23 ` brian m. carlson
0 siblings, 1 reply; 12+ messages in thread
From: Junio C Hamano @ 2024-06-28 22:18 UTC (permalink / raw)
To: brian m. carlson; +Cc: git
"brian m. carlson" <sandals@crustytoothpaste.net> writes:
>> > +* `basic` - Request Basic authentication from the helper.
>> > +* `auto` - Don't request any scheme from the helper.
>> > +--
>>
>> What does "don't request" exactly mean? It is not like we are
>> telling the helper "Don't give us anything", right? Are we telling
>> the helper "Give us any username/password for the URL in any
>> authentication scheme you know about?"
>
> It means we don't send a `wwwauth[]` entry in the request. We are
> giving the helper carte blanche to decide what scheme is best (maybe it
> knows we want Bearer, for example).
Well, at least I couldn't read the proposed document update and read
that much out of it, and I suspect there may be other readers who
will share my confusion. I think the source of the confusion is
that "don't request" does not automatically imply "allow the helper
to pick any scheme as it sees fit" if you do not know how helper is
designed to behave when it is not requested "any scheme".
`basic` - Request Basic authentication from the helper.
`auto` - Ask the helper to pick an appropriate scheme.
`none` - Disable proactive authentication.
or something, perhaps?
Thanks.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 1/1] http: allow authenticating proactively
2024-06-28 22:18 ` Junio C Hamano
@ 2024-06-29 0:23 ` brian m. carlson
2024-07-01 15:26 ` Junio C Hamano
0 siblings, 1 reply; 12+ messages in thread
From: brian m. carlson @ 2024-06-29 0:23 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
[-- Attachment #1: Type: text/plain, Size: 904 bytes --]
On 2024-06-28 at 22:18:37, Junio C Hamano wrote:
> Well, at least I couldn't read the proposed document update and read
> that much out of it, and I suspect there may be other readers who
> will share my confusion. I think the source of the confusion is
> that "don't request" does not automatically imply "allow the helper
> to pick any scheme as it sees fit" if you do not know how helper is
> designed to behave when it is not requested "any scheme".
>
> `basic` - Request Basic authentication from the helper.
> `auto` - Ask the helper to pick an appropriate scheme.
> `none` - Disable proactive authentication.
>
> or something, perhaps?
That sounds good. I appreciate you providing some language that would
be less confusing to you, because it will probably be less confusing to
others as well.
--
brian m. carlson (they/them or he/him)
Toronto, Ontario, CA
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 262 bytes --]
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 1/1] http: allow authenticating proactively
2024-06-29 0:23 ` brian m. carlson
@ 2024-07-01 15:26 ` Junio C Hamano
0 siblings, 0 replies; 12+ messages in thread
From: Junio C Hamano @ 2024-07-01 15:26 UTC (permalink / raw)
To: brian m. carlson; +Cc: git
"brian m. carlson" <sandals@crustytoothpaste.net> writes:
> On 2024-06-28 at 22:18:37, Junio C Hamano wrote:
>> Well, at least I couldn't read the proposed document update and read
>> that much out of it, and I suspect there may be other readers who
>> will share my confusion. I think the source of the confusion is
>> that "don't request" does not automatically imply "allow the helper
>> to pick any scheme as it sees fit" if you do not know how helper is
>> designed to behave when it is not requested "any scheme".
>>
>> `basic` - Request Basic authentication from the helper.
>> `auto` - Ask the helper to pick an appropriate scheme.
>> `none` - Disable proactive authentication.
>>
>> or something, perhaps?
>
> That sounds good. I appreciate you providing some language that would
> be less confusing to you, because it will probably be less confusing to
> others as well.
Thanks.
Giving a possible alternative, when you are certain you understood
what you originally found confusing, is probably one of the things
we should add as a tip to ReviewingPatches document (do we have one
already--- #leftoverbits ?).
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 0/1] Proactive authentication over HTTP
2024-06-28 0:27 [PATCH 0/1] Proactive authentication over HTTP brian m. carlson
2024-06-28 0:27 ` [PATCH 1/1] http: allow authenticating proactively brian m. carlson
@ 2024-07-04 0:17 ` brian m. carlson
2024-07-04 0:17 ` [PATCH v2 1/1] http: allow authenticating proactively brian m. carlson
2024-07-10 0:01 ` [PATCH v3 0/1] Proactive authentication over HTTP brian m. carlson
1 sibling, 2 replies; 12+ messages in thread
From: brian m. carlson @ 2024-07-04 0:17 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
Currently Git only sends authentication over HTTP once it's received a
401 response from the server. This series allows users to indicate that
they (or the credential helper) know what kind of authentication is to
be performed and avoid the extra round trip.
Changes since v1:
* Add a `none` option to explicitly reset the value.
* Improve explanation of `auto`.
* Improve commit message to explain tricky edge case.
* Note that TLS should always be used.
brian m. carlson (1):
http: allow authenticating proactively
Documentation/config/http.txt | 20 ++++++
http.c | 60 ++++++++++++++++--
t/t5563-simple-http-auth.sh | 116 ++++++++++++++++++++++++++++++++++
3 files changed, 190 insertions(+), 6 deletions(-)
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 1/1] http: allow authenticating proactively
2024-07-04 0:17 ` [PATCH v2 0/1] Proactive authentication over HTTP brian m. carlson
@ 2024-07-04 0:17 ` brian m. carlson
2024-07-04 5:49 ` Junio C Hamano
2024-07-10 0:01 ` [PATCH v3 0/1] Proactive authentication over HTTP brian m. carlson
1 sibling, 1 reply; 12+ messages in thread
From: brian m. carlson @ 2024-07-04 0:17 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
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>
---
Documentation/config/http.txt | 20 ++++++
http.c | 60 ++++++++++++++++--
t/t5563-simple-http-auth.sh | 116 ++++++++++++++++++++++++++++++++++
3 files changed, 190 insertions(+), 6 deletions(-)
diff --git a/Documentation/config/http.txt b/Documentation/config/http.txt
index 2d4e0c9b86..9fd6c38252 100644
--- a/Documentation/config/http.txt
+++ b/Documentation/config/http.txt
@@ -56,6 +56,26 @@ http.emptyAuth::
a username in the URL, as libcurl normally requires a username for
authentication.
+http.proactiveAuth::
+ Attempt authentication without first making an unauthenticated attempt and
+ receiving a 401 response. This can be used to ensure that all requests are
+ authenticated. If `http.emptyAuth` is set to true, this value has no effect.
++
+If the credential helper used specifies an authentication scheme (i.e., via the
+`authtype` field), that value will be used; if a username and password is
+provided without a scheme, then Basic authentication is used. The value of the
+option determines the scheme requested from the helper. Possible values are:
++
+--
+* `basic` - Request Basic authentication from the helper.
+* `auto` - Allow the helper to pick an appropriate scheme.
+* `none` - Disable proactive authentication.
+--
++
+Note that TLS should always be used with this configuration, since otherwise it
+is easy to accidentally expose plaintext credentials if Basic authentication
+is selected.
+
http.delegation::
Control GSSAPI credential delegation. The delegation is disabled
by default in libcurl since version 7.21.7. Set parameter to tell
diff --git a/http.c b/http.c
index 13fa94bef3..3e90b32bb0 100644
--- a/http.c
+++ b/http.c
@@ -108,12 +108,19 @@ static struct {
};
#endif
+enum proactive_auth {
+ PROACTIVE_AUTH_NONE = 0,
+ PROACTIVE_AUTH_IF_CREDENTIALS,
+ PROACTIVE_AUTH_AUTO,
+ PROACTIVE_AUTH_BASIC,
+};
+
static struct credential proxy_auth = CREDENTIAL_INIT;
static const char *curl_proxyuserpwd;
static char *curl_cookie_file;
static int curl_save_cookies;
struct credential http_auth = CREDENTIAL_INIT;
-static int http_proactive_auth;
+static enum proactive_auth http_proactive_auth;
static char *user_agent;
static int curl_empty_auth = -1;
@@ -148,6 +155,12 @@ static int http_schannel_check_revoke = 1;
*/
static int http_schannel_use_ssl_cainfo;
+static int always_auth_proactively(void)
+{
+ return http_proactive_auth != PROACTIVE_AUTH_NONE &&
+ http_proactive_auth != PROACTIVE_AUTH_IF_CREDENTIALS;
+}
+
size_t fread_buffer(char *ptr, size_t eltsize, size_t nmemb, void *buffer_)
{
size_t size = eltsize * nmemb;
@@ -539,6 +552,18 @@ static int http_options(const char *var, const char *value,
return 0;
}
+ if (!strcmp("http.proactiveauth", var)) {
+ if (value && !strcmp(value, "auto"))
+ http_proactive_auth = PROACTIVE_AUTH_AUTO;
+ else if (value && !strcmp(value, "basic"))
+ http_proactive_auth = PROACTIVE_AUTH_BASIC;
+ else if (value && !strcmp(value, "none"))
+ http_proactive_auth = PROACTIVE_AUTH_NONE;
+ else
+ warning(_("Unknown value for http.proactiveauth"));
+ return 0;
+ }
+
/* Fall back on the default ones */
return git_default_config(var, value, ctx, data);
}
@@ -580,14 +605,29 @@ static void init_curl_http_auth(CURL *result)
{
if ((!http_auth.username || !*http_auth.username) &&
(!http_auth.credential || !*http_auth.credential)) {
- if (curl_empty_auth_enabled())
+ int empty_auth = curl_empty_auth_enabled();
+ if ((empty_auth != -1 && !always_auth_proactively()) || empty_auth == 1) {
curl_easy_setopt(result, CURLOPT_USERPWD, ":");
- return;
+ return;
+ } else if (!always_auth_proactively()) {
+ return;
+ } else if (http_proactive_auth == PROACTIVE_AUTH_BASIC) {
+ strvec_push(&http_auth.wwwauth_headers, "Basic");
+ }
}
credential_fill(&http_auth, 1);
if (http_auth.password) {
+ if (always_auth_proactively()) {
+ /*
+ * We got a credential without an authtype and we don't
+ * know what's available. Since our only two options at
+ * the moment are auto (which defaults to basic) and
+ * basic, use basic for now.
+ */
+ curl_easy_setopt(result, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
+ }
curl_easy_setopt(result, CURLOPT_USERNAME, http_auth.username);
curl_easy_setopt(result, CURLOPT_PASSWORD, http_auth.password);
}
@@ -1050,7 +1090,7 @@ static CURL *get_curl_handle(void)
#endif
}
- if (http_proactive_auth)
+ if (http_proactive_auth != PROACTIVE_AUTH_NONE)
init_curl_http_auth(result);
if (getenv("GIT_SSL_VERSION"))
@@ -1294,7 +1334,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK)
die("curl_global_init failed");
- http_proactive_auth = proactive_auth;
+ if (proactive_auth && http_proactive_auth == PROACTIVE_AUTH_NONE)
+ http_proactive_auth = PROACTIVE_AUTH_IF_CREDENTIALS;
if (remote && remote->http_proxy)
curl_http_proxy = xstrdup(remote->http_proxy);
@@ -1790,6 +1831,8 @@ static int handle_curl_result(struct slot_results *results)
return HTTP_REAUTH;
}
credential_reject(&http_auth);
+ if (always_auth_proactively())
+ http_proactive_auth = PROACTIVE_AUTH_NONE;
return HTTP_NOAUTH;
} else {
http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
@@ -2186,7 +2229,12 @@ static int http_request_reauth(const char *url,
struct http_get_options *options)
{
int i = 3;
- int ret = http_request(url, result, target, options);
+ int ret;
+
+ if (always_auth_proactively())
+ credential_fill(&http_auth, 1);
+
+ ret = http_request(url, result, target, options);
if (ret != HTTP_OK && ret != HTTP_REAUTH)
return ret;
diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh
index 4af796de67..ba03f6a09f 100755
--- a/t/t5563-simple-http-auth.sh
+++ b/t/t5563-simple-http-auth.sh
@@ -178,6 +178,122 @@ test_expect_success 'access using basic auth invalid credentials' '
EOF
'
+test_expect_success 'access using basic proactive auth' '
+ 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 status=403
+ EOF
+
+ test_config_global credential.helper test-helper &&
+ test_config_global http.proactiveAuth basic &&
+ git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+ expect_credential_query get <<-EOF &&
+ capability[]=authtype
+ capability[]=state
+ protocol=http
+ host=$HTTPD_DEST
+ wwwauth[]=Basic
+ EOF
+
+ expect_credential_query store <<-EOF
+ protocol=http
+ host=$HTTPD_DEST
+ username=alice
+ password=secret-passwd
+ EOF
+'
+
+test_expect_success 'access using auto proactive auth with basic default' '
+ 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 status=403
+ EOF
+
+ test_config_global credential.helper test-helper &&
+ test_config_global http.proactiveAuth auto &&
+ git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+ expect_credential_query get <<-EOF &&
+ capability[]=authtype
+ capability[]=state
+ protocol=http
+ host=$HTTPD_DEST
+ EOF
+
+ expect_credential_query store <<-EOF
+ protocol=http
+ host=$HTTPD_DEST
+ username=alice
+ password=secret-passwd
+ EOF
+'
+
+test_expect_success 'access using auto proactive auth with authtype from credential helper' '
+ test_when_finished "per_test_cleanup" &&
+
+ set_credential_reply get <<-EOF &&
+ capability[]=authtype
+ authtype=Bearer
+ credential=YS1naXQtdG9rZW4=
+ EOF
+
+ # Basic base64(a-git-token)
+ cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
+ id=1 creds=Bearer YS1naXQtdG9rZW4=
+ EOF
+
+ CHALLENGE="$HTTPD_ROOT_PATH/custom-auth.challenge" &&
+
+ cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
+ id=1 status=200
+ id=default status=403
+ EOF
+
+ test_config_global credential.helper test-helper &&
+ test_config_global http.proactiveAuth auto &&
+ git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+ expect_credential_query get <<-EOF &&
+ capability[]=authtype
+ capability[]=state
+ protocol=http
+ host=$HTTPD_DEST
+ EOF
+
+ expect_credential_query store <<-EOF
+ capability[]=authtype
+ authtype=Bearer
+ credential=YS1naXQtdG9rZW4=
+ protocol=http
+ host=$HTTPD_DEST
+ EOF
+'
+
test_expect_success 'access using basic auth with extra challenges' '
test_when_finished "per_test_cleanup" &&
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v2 1/1] http: allow authenticating proactively
2024-07-04 0:17 ` [PATCH v2 1/1] http: allow authenticating proactively brian m. carlson
@ 2024-07-04 5:49 ` Junio C Hamano
0 siblings, 0 replies; 12+ messages in thread
From: Junio C Hamano @ 2024-07-04 5:49 UTC (permalink / raw)
To: brian m. carlson; +Cc: git
"brian m. carlson" <sandals@crustytoothpaste.net> writes:
> 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.
Nice explanation.
> +http.proactiveAuth::
> + Attempt authentication without first making an unauthenticated attempt and
> + receiving a 401 response. This can be used to ensure that all requests are
> + authenticated. If `http.emptyAuth` is set to true, this value has no effect.
> ++
> +If the credential helper used specifies an authentication scheme (i.e., via the
> +`authtype` field), that value will be used; if a username and password is
> +provided without a scheme, then Basic authentication is used. The value of the
> +option determines the scheme requested from the helper. Possible values are:
> ++
> +--
> +* `basic` - Request Basic authentication from the helper.
> +* `auto` - Allow the helper to pick an appropriate scheme.
> +* `none` - Disable proactive authentication.
> +--
> ++
> +Note that TLS should always be used with this configuration, since otherwise it
> +is easy to accidentally expose plaintext credentials if Basic authentication
> +is selected.
OK.
> @@ -539,6 +552,18 @@ static int http_options(const char *var, const char *value,
> return 0;
> }
>
> + if (!strcmp("http.proactiveauth", var)) {
If we choose to make
[http] proactiveauth ; nothing else on the line
an error, we could do
if (!value)
return config_error_nonbool(var);
and lose all the "we have to make sure value is not NULL before
feeding it to strcmp()" checks below.
Or
if (!value) {
warning(_("http.proactiveauth set to true???"));
return 0;
}
if we wanted to be more lenient (which is more in line with how we
treat unknown string value below).
> + if (value && !strcmp(value, "auto"))
> + http_proactive_auth = PROACTIVE_AUTH_AUTO;
> + else if (value && !strcmp(value, "basic"))
> + http_proactive_auth = PROACTIVE_AUTH_BASIC;
> + else if (value && !strcmp(value, "none"))
> + http_proactive_auth = PROACTIVE_AUTH_NONE;
> + else
> + warning(_("Unknown value for http.proactiveauth"));
> + return 0;
> + }
> +
Other than that I saw nothing puzzling or curious. Looking very
good.
Thanks. Will queue.
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v3 0/1] Proactive authentication over HTTP
2024-07-04 0:17 ` [PATCH v2 0/1] Proactive authentication over HTTP brian m. carlson
2024-07-04 0:17 ` [PATCH v2 1/1] http: allow authenticating proactively brian m. carlson
@ 2024-07-10 0:01 ` brian m. carlson
2024-07-10 0:01 ` [PATCH v3 1/1] http: allow authenticating proactively brian m. carlson
1 sibling, 1 reply; 12+ messages in thread
From: brian m. carlson @ 2024-07-10 0:01 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
Currently Git only sends authentication over HTTP once it's received a
401 response from the server. This series allows users to indicate that
they (or the credential helper) know what kind of authentication is to
be performed and avoid the extra round trip.
Changes since v2:
* Improve parsing of configuration options as suggested by Junio.
Changes since v1:
* Add a `none` option to explicitly reset the value.
* Improve explanation of `auto`.
* Improve commit message to explain tricky edge case.
* Note that TLS should always be used.
brian m. carlson (1):
http: allow authenticating proactively
Documentation/config/http.txt | 20 ++++++
http.c | 62 ++++++++++++++++--
t/t5563-simple-http-auth.sh | 116 ++++++++++++++++++++++++++++++++++
3 files changed, 192 insertions(+), 6 deletions(-)
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v3 1/1] http: allow authenticating proactively
2024-07-10 0:01 ` [PATCH v3 0/1] Proactive authentication over HTTP brian m. carlson
@ 2024-07-10 0:01 ` brian m. carlson
0 siblings, 0 replies; 12+ messages in thread
From: brian m. carlson @ 2024-07-10 0:01 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano
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>
---
Documentation/config/http.txt | 20 ++++++
http.c | 62 ++++++++++++++++--
t/t5563-simple-http-auth.sh | 116 ++++++++++++++++++++++++++++++++++
3 files changed, 192 insertions(+), 6 deletions(-)
diff --git a/Documentation/config/http.txt b/Documentation/config/http.txt
index 2d4e0c9b86..9fd6c38252 100644
--- a/Documentation/config/http.txt
+++ b/Documentation/config/http.txt
@@ -56,6 +56,26 @@ http.emptyAuth::
a username in the URL, as libcurl normally requires a username for
authentication.
+http.proactiveAuth::
+ Attempt authentication without first making an unauthenticated attempt and
+ receiving a 401 response. This can be used to ensure that all requests are
+ authenticated. If `http.emptyAuth` is set to true, this value has no effect.
++
+If the credential helper used specifies an authentication scheme (i.e., via the
+`authtype` field), that value will be used; if a username and password is
+provided without a scheme, then Basic authentication is used. The value of the
+option determines the scheme requested from the helper. Possible values are:
++
+--
+* `basic` - Request Basic authentication from the helper.
+* `auto` - Allow the helper to pick an appropriate scheme.
+* `none` - Disable proactive authentication.
+--
++
+Note that TLS should always be used with this configuration, since otherwise it
+is easy to accidentally expose plaintext credentials if Basic authentication
+is selected.
+
http.delegation::
Control GSSAPI credential delegation. The delegation is disabled
by default in libcurl since version 7.21.7. Set parameter to tell
diff --git a/http.c b/http.c
index 13fa94bef3..81b5cf8039 100644
--- a/http.c
+++ b/http.c
@@ -108,12 +108,19 @@ static struct {
};
#endif
+enum proactive_auth {
+ PROACTIVE_AUTH_NONE = 0,
+ PROACTIVE_AUTH_IF_CREDENTIALS,
+ PROACTIVE_AUTH_AUTO,
+ PROACTIVE_AUTH_BASIC,
+};
+
static struct credential proxy_auth = CREDENTIAL_INIT;
static const char *curl_proxyuserpwd;
static char *curl_cookie_file;
static int curl_save_cookies;
struct credential http_auth = CREDENTIAL_INIT;
-static int http_proactive_auth;
+static enum proactive_auth http_proactive_auth;
static char *user_agent;
static int curl_empty_auth = -1;
@@ -148,6 +155,12 @@ static int http_schannel_check_revoke = 1;
*/
static int http_schannel_use_ssl_cainfo;
+static int always_auth_proactively(void)
+{
+ return http_proactive_auth != PROACTIVE_AUTH_NONE &&
+ http_proactive_auth != PROACTIVE_AUTH_IF_CREDENTIALS;
+}
+
size_t fread_buffer(char *ptr, size_t eltsize, size_t nmemb, void *buffer_)
{
size_t size = eltsize * nmemb;
@@ -539,6 +552,20 @@ static int http_options(const char *var, const char *value,
return 0;
}
+ if (!strcmp("http.proactiveauth", var)) {
+ if (!value)
+ return config_error_nonbool(var);
+ if (!strcmp(value, "auto"))
+ http_proactive_auth = PROACTIVE_AUTH_AUTO;
+ else if (!strcmp(value, "basic"))
+ http_proactive_auth = PROACTIVE_AUTH_BASIC;
+ else if (!strcmp(value, "none"))
+ http_proactive_auth = PROACTIVE_AUTH_NONE;
+ else
+ warning(_("Unknown value for http.proactiveauth"));
+ return 0;
+ }
+
/* Fall back on the default ones */
return git_default_config(var, value, ctx, data);
}
@@ -580,14 +607,29 @@ static void init_curl_http_auth(CURL *result)
{
if ((!http_auth.username || !*http_auth.username) &&
(!http_auth.credential || !*http_auth.credential)) {
- if (curl_empty_auth_enabled())
+ int empty_auth = curl_empty_auth_enabled();
+ if ((empty_auth != -1 && !always_auth_proactively()) || empty_auth == 1) {
curl_easy_setopt(result, CURLOPT_USERPWD, ":");
- return;
+ return;
+ } else if (!always_auth_proactively()) {
+ return;
+ } else if (http_proactive_auth == PROACTIVE_AUTH_BASIC) {
+ strvec_push(&http_auth.wwwauth_headers, "Basic");
+ }
}
credential_fill(&http_auth, 1);
if (http_auth.password) {
+ if (always_auth_proactively()) {
+ /*
+ * We got a credential without an authtype and we don't
+ * know what's available. Since our only two options at
+ * the moment are auto (which defaults to basic) and
+ * basic, use basic for now.
+ */
+ curl_easy_setopt(result, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
+ }
curl_easy_setopt(result, CURLOPT_USERNAME, http_auth.username);
curl_easy_setopt(result, CURLOPT_PASSWORD, http_auth.password);
}
@@ -1050,7 +1092,7 @@ static CURL *get_curl_handle(void)
#endif
}
- if (http_proactive_auth)
+ if (http_proactive_auth != PROACTIVE_AUTH_NONE)
init_curl_http_auth(result);
if (getenv("GIT_SSL_VERSION"))
@@ -1294,7 +1336,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth)
if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK)
die("curl_global_init failed");
- http_proactive_auth = proactive_auth;
+ if (proactive_auth && http_proactive_auth == PROACTIVE_AUTH_NONE)
+ http_proactive_auth = PROACTIVE_AUTH_IF_CREDENTIALS;
if (remote && remote->http_proxy)
curl_http_proxy = xstrdup(remote->http_proxy);
@@ -1790,6 +1833,8 @@ static int handle_curl_result(struct slot_results *results)
return HTTP_REAUTH;
}
credential_reject(&http_auth);
+ if (always_auth_proactively())
+ http_proactive_auth = PROACTIVE_AUTH_NONE;
return HTTP_NOAUTH;
} else {
http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
@@ -2186,7 +2231,12 @@ static int http_request_reauth(const char *url,
struct http_get_options *options)
{
int i = 3;
- int ret = http_request(url, result, target, options);
+ int ret;
+
+ if (always_auth_proactively())
+ credential_fill(&http_auth, 1);
+
+ ret = http_request(url, result, target, options);
if (ret != HTTP_OK && ret != HTTP_REAUTH)
return ret;
diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh
index 4af796de67..ba03f6a09f 100755
--- a/t/t5563-simple-http-auth.sh
+++ b/t/t5563-simple-http-auth.sh
@@ -178,6 +178,122 @@ test_expect_success 'access using basic auth invalid credentials' '
EOF
'
+test_expect_success 'access using basic proactive auth' '
+ 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 status=403
+ EOF
+
+ test_config_global credential.helper test-helper &&
+ test_config_global http.proactiveAuth basic &&
+ git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+ expect_credential_query get <<-EOF &&
+ capability[]=authtype
+ capability[]=state
+ protocol=http
+ host=$HTTPD_DEST
+ wwwauth[]=Basic
+ EOF
+
+ expect_credential_query store <<-EOF
+ protocol=http
+ host=$HTTPD_DEST
+ username=alice
+ password=secret-passwd
+ EOF
+'
+
+test_expect_success 'access using auto proactive auth with basic default' '
+ 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 status=403
+ EOF
+
+ test_config_global credential.helper test-helper &&
+ test_config_global http.proactiveAuth auto &&
+ git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+ expect_credential_query get <<-EOF &&
+ capability[]=authtype
+ capability[]=state
+ protocol=http
+ host=$HTTPD_DEST
+ EOF
+
+ expect_credential_query store <<-EOF
+ protocol=http
+ host=$HTTPD_DEST
+ username=alice
+ password=secret-passwd
+ EOF
+'
+
+test_expect_success 'access using auto proactive auth with authtype from credential helper' '
+ test_when_finished "per_test_cleanup" &&
+
+ set_credential_reply get <<-EOF &&
+ capability[]=authtype
+ authtype=Bearer
+ credential=YS1naXQtdG9rZW4=
+ EOF
+
+ # Basic base64(a-git-token)
+ cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
+ id=1 creds=Bearer YS1naXQtdG9rZW4=
+ EOF
+
+ CHALLENGE="$HTTPD_ROOT_PATH/custom-auth.challenge" &&
+
+ cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
+ id=1 status=200
+ id=default status=403
+ EOF
+
+ test_config_global credential.helper test-helper &&
+ test_config_global http.proactiveAuth auto &&
+ git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
+
+ expect_credential_query get <<-EOF &&
+ capability[]=authtype
+ capability[]=state
+ protocol=http
+ host=$HTTPD_DEST
+ EOF
+
+ expect_credential_query store <<-EOF
+ capability[]=authtype
+ authtype=Bearer
+ credential=YS1naXQtdG9rZW4=
+ protocol=http
+ host=$HTTPD_DEST
+ EOF
+'
+
test_expect_success 'access using basic auth with extra challenges' '
test_when_finished "per_test_cleanup" &&
^ permalink raw reply related [flat|nested] 12+ messages in thread
end of thread, other threads:[~2024-07-10 0:01 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-06-28 0:27 [PATCH 0/1] Proactive authentication over HTTP brian m. carlson
2024-06-28 0:27 ` [PATCH 1/1] http: allow authenticating proactively brian m. carlson
2024-06-28 18:16 ` Junio C Hamano
2024-06-28 22:00 ` brian m. carlson
2024-06-28 22:18 ` Junio C Hamano
2024-06-29 0:23 ` brian m. carlson
2024-07-01 15:26 ` Junio C Hamano
2024-07-04 0:17 ` [PATCH v2 0/1] Proactive authentication over HTTP brian m. carlson
2024-07-04 0:17 ` [PATCH v2 1/1] http: allow authenticating proactively brian m. carlson
2024-07-04 5:49 ` Junio C Hamano
2024-07-10 0:01 ` [PATCH v3 0/1] Proactive authentication over HTTP brian m. carlson
2024-07-10 0:01 ` [PATCH v3 1/1] http: allow authenticating proactively brian m. carlson
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).