diff mbox series

[v7,12/12] credential: add WWW-Authenticate header to cred requests

Message ID 09164f77d56e8efd1450091cf1b12af2bc6cf2f5.1674252531.git.gitgitgadget@gmail.com (mailing list archive)
State Superseded
Commit 026012ce26e8fac0a1a506546f0c6542ea8178c2
Headers show
Series Enhance credential helper protocol to include auth headers | expand

Commit Message

Matthew John Cheetham Jan. 20, 2023, 10:08 p.m. UTC
From: Matthew John Cheetham <mjcheetham@outlook.com>

Add the value of the WWW-Authenticate response header to credential
requests. Credential helpers that understand and support HTTP
authentication and authorization can use this standard header (RFC 2616
Section 14.47 [1]) to generate valid credentials.

WWW-Authenticate headers can contain information pertaining to the
authority, authentication mechanism, or extra parameters/scopes that are
required.

The current I/O format for credential helpers only allows for unique
names for properties/attributes, so in order to transmit multiple header
values (with a specific order) we introduce a new convention whereby a
C-style array syntax is used in the property name to denote multiple
ordered values for the same property.

In this case we send multiple `wwwauth[]` properties where the order
that the repeated attributes appear in the conversation reflects the
order that the WWW-Authenticate headers appeared in the HTTP response.

Add a set of tests to exercise the HTTP authentication header parsing
and the interop with credential helpers. Credential helpers will receive
WWW-Authenticate information in credential requests.

[1] https://datatracker.ietf.org/doc/html/rfc2616#section-14.47

Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
---
 Documentation/git-credential.txt |  19 ++-
 credential.c                     |  11 ++
 t/lib-credential-helper.sh       |  27 ++++
 t/t5556-http-auth.sh             | 242 +++++++++++++++++++++++++++++++
 4 files changed, 298 insertions(+), 1 deletion(-)
 create mode 100644 t/lib-credential-helper.sh

Comments

Jeff King Jan. 26, 2023, 11:25 a.m. UTC | #1
On Fri, Jan 20, 2023 at 10:08:50PM +0000, Matthew John Cheetham via GitGitGadget wrote:

> From: Matthew John Cheetham <mjcheetham@outlook.com>
> 
> Add the value of the WWW-Authenticate response header to credential
> requests. Credential helpers that understand and support HTTP
> authentication and authorization can use this standard header (RFC 2616
> Section 14.47 [1]) to generate valid credentials.
> 
> WWW-Authenticate headers can contain information pertaining to the
> authority, authentication mechanism, or extra parameters/scopes that are
> required.

I'm definitely on board with sending these to the helpers. It does feel
a bit weird that we don't parse them at all, and just foist that on the
helpers.

If I understand the RFC correctly, you can have multiple challenges per
header, but also multiple headers. So:

  WWW-Authenticate: Basic realm="foo", OtherAuth realm="bar"
  WWW-Authenticate: YetAnotherScheme some-token

could be normalized as:

  www-auth-challenge=Basic realm="foo"
  www-auth-challenge=OtherAuth realm="bar"
  www-auth-challenge=YetAnotherScheme some-token

which saves each helper from having to do the same work. Likewise, we
can do a _little_ more parsing to get:

  www-auth-basic=realm="foo"
  www-auth-otherauth=realm="bar"
  www-auth-yetanotherscheme=some-token

I don't think we can go beyond there, though, without understanding the
syntax of individual schemes. Which is a shame, as one of the goals of
the credential format was to let the helpers do as little as possible
(so they can't get it wrong!). But helpers are stuck doing things like
handling backslashed double-quotes, soaking up extra whitespace, etc.

I'm not really sure what we expect to see in the real world. I guess for
your purposes, you are working on an already-big helper that is happy to
just get the raw values and process them according to the rfc. I'm just
wondering if there are use cases where somebody might want to do
something with this header, but in a quick shell script kind of way. For
example, my credential config is still:

  [credential "https://github.com"]
  username = peff
  helper = "!f() { test $1 = get && echo password=$(pass ...); }; f"

That's an extreme example, but I'm wondering if there's _anything_
useful somebody would want to do in a similar quick-and-dirty kind of
way. For example, deciding which cred to use based on basic realm, like:

  realm=foo
  while read line; do
    case "$line" in
    www-auth-basic=)
        value=${line#*=}
	# oops, we're just assuming it's realm= here, and we're
	# not handling quotes at all. I think it could technically be
	# realm=foo or realm="foo"
	realm=${value#realm=}
	;;
    esac
  done
  echo password=$(pass "pats-by-realm/$realm")

which could be made a lot easier if we did more parsing (e.g.,
www-auth-basic-realm or something). I dunno. Maybe that is just opening
up a can of worms, as we're stuffing structured data into a linearized
key-value list. The nice thing about your proposal is that Git does not
even have to know anything about these schemes; it's all the problem of
the helper. My biggest fear is just that we'll want to shift that later,
and we'll be stuck with this microformat forever.

> The current I/O format for credential helpers only allows for unique
> names for properties/attributes, so in order to transmit multiple header
> values (with a specific order) we introduce a new convention whereby a
> C-style array syntax is used in the property name to denote multiple
> ordered values for the same property.

I don't know if this is strictly necessary. The semantics of duplicate
keys are not really defined anywhere, and just because the
implementations of current readers happen to replace duplicates for the
current set of keys doesn't mean everything has to. So you could just
define "wwwauth" to behave differently. But I don't mind having a
syntactic marker to indicate this new type.

If you're at all convinced by what I said above, then we also might be
able to get away with having unique keys anyway.

>  Documentation/git-credential.txt |  19 ++-
>  credential.c                     |  11 ++
>  t/lib-credential-helper.sh       |  27 ++++
>  t/t5556-http-auth.sh             | 242 +++++++++++++++++++++++++++++++
>  4 files changed, 298 insertions(+), 1 deletion(-)
>  create mode 100644 t/lib-credential-helper.sh

The patch itself looks pretty reasonable to me.

One small thing I noticed:

> +	git -c "credential.helper=!\"$CREDENTIAL_HELPER\"" ls-remote $ORIGIN_URL &&

As you undoubtedly figured out, the helper path is fed to the shell, so
spaces in the trash directory are a problem. You've solved it here by
adding a layer of double quotes, which handles spaces. But you'd run
into problems if the absolute path that somebody is using for the test
suite has a backslash or a double quote in it.

I don't know how careful we want to be here (or how careful we already
are[1]), but one simple-ish solution is:

  export CREDENTIAL_HELPER
  git -c "credential.helper=!\"\$CREDENTIAL_HELPER\"" ...

I.e., letting the inner shell expand the variable itself. Another option
is to put the helper into $TRASH_DIRECTORY/bin and add that to the
$PATH.

I also wondered if it was worth having setup_credential_helper() just
stick it in $TRASH_DIRECTORY/.gitconfig so that individual tests don't
have to keep doing that ugly "-c" invocation. Or if you really want to
have each test enable it, perhaps have set_credential_reply() turn it on
via test_config (which will auto-remove it at the end of the test).

-Peff

[1] Curious, I tried cloning git into this directory:

      mkdir '/tmp/foo/"horrible \"path\"'

    and we do indeed already fail. The first breakage I saw was recent,
    but going further back, it looks like bin-wrappers don't correctly
    handle this case anyway. So maybe that's evidence that nobody would
    do something so ridiculous in practice.
Matthew John Cheetham Feb. 6, 2023, 7:18 p.m. UTC | #2
On 2023-01-26 03:25, Jeff King wrote:

> On Fri, Jan 20, 2023 at 10:08:50PM +0000, Matthew John Cheetham via GitGitGadget wrote:
> 
>> From: Matthew John Cheetham <mjcheetham@outlook.com>
>>
>> Add the value of the WWW-Authenticate response header to credential
>> requests. Credential helpers that understand and support HTTP
>> authentication and authorization can use this standard header (RFC 2616
>> Section 14.47 [1]) to generate valid credentials.
>>
>> WWW-Authenticate headers can contain information pertaining to the
>> authority, authentication mechanism, or extra parameters/scopes that are
>> required.
> 
> I'm definitely on board with sending these to the helpers. It does feel
> a bit weird that we don't parse them at all, and just foist that on the
> helpers.
> 
> If I understand the RFC correctly, you can have multiple challenges per
> header, but also multiple headers. So:
> 
>   WWW-Authenticate: Basic realm="foo", OtherAuth realm="bar"
>   WWW-Authenticate: YetAnotherScheme some-token

That is correct. It would be strange that server would respond with a mix
of styles, but I guess it's not forbidden.

> could be normalized as:
> 
>   www-auth-challenge=Basic realm="foo"
>   www-auth-challenge=OtherAuth realm="bar"
>   www-auth-challenge=YetAnotherScheme some-token
> 
> which saves each helper from having to do the same work. Likewise, we
> can do a _little_ more parsing to get:
> 
>   www-auth-basic=realm="foo"
>   www-auth-otherauth=realm="bar"
>   www-auth-yetanotherscheme=some-token
> 
> I don't think we can go beyond there, though, without understanding the
> syntax of individual schemes. Which is a shame, as one of the goals of
> the credential format was to let the helpers do as little as possible
> (so they can't get it wrong!). But helpers are stuck doing things like
> handling backslashed double-quotes, soaking up extra whitespace, etc.

This key format wouldn't make it obviously easier for simple helpers to
understand. Now they no longer have well-known keys but a key prefix.

My overall goal here is to have Git know less about auth, so it treats
all values as totally opaque. The only logic added is around reconstructing
folded headers, which is just HTTP and not auth specific.

> I'm not really sure what we expect to see in the real world. I guess for
> your purposes, you are working on an already-big helper that is happy to
> just get the raw values and process them according to the rfc. I'm just
> wondering if there are use cases where somebody might want to do
> something with this header, but in a quick shell script kind of way. For
> example, my credential config is still:
> 
>   [credential "https://github.com"]
>   username = peff
>   helper = "!f() { test $1 = get && echo password=$(pass ...); }; f"
> 
> That's an extreme example, but I'm wondering if there's _anything_
> useful somebody would want to do in a similar quick-and-dirty kind of
> way. For example, deciding which cred to use based on basic realm, like:
> 
>   realm=foo
>   while read line; do
>     case "$line" in
>     www-auth-basic=)
>         value=${line#*=}
> 	# oops, we're just assuming it's realm= here, and we're
> 	# not handling quotes at all. I think it could technically be
> 	# realm=foo or realm="foo"
> 	realm=${value#realm=}
> 	;;
>     esac
>   done
>   echo password=$(pass "pats-by-realm/$realm")
> 
> which could be made a lot easier if we did more parsing (e.g.,
> www-auth-basic-realm or something). I dunno. Maybe that is just opening
> up a can of worms, as we're stuffing structured data into a linearized
> key-value list. The nice thing about your proposal is that Git does not
> even have to know anything about these schemes; it's all the problem of
> the helper. My biggest fear is just that we'll want to shift that later,
> and we'll be stuck with this microformat forever.

I'm not sure there's such a continuous scale between simple and 'complex'
helpers that would mean there'd be a simple shell script generating
OAuth or DPoP credentials instead of a helper written in a higher-level
language where parsing the headers is one of the simpler challenges faced.

>> The current I/O format for credential helpers only allows for unique
>> names for properties/attributes, so in order to transmit multiple header
>> values (with a specific order) we introduce a new convention whereby a
>> C-style array syntax is used in the property name to denote multiple
>> ordered values for the same property.
> 
> I don't know if this is strictly necessary. The semantics of duplicate
> keys are not really defined anywhere, and just because the
> implementations of current readers happen to replace duplicates for the
> current set of keys doesn't mean everything has to. So you could just
> define "wwwauth" to behave differently. But I don't mind having a
> syntactic marker to indicate this new type.

I had considered another model whereby we forgo the key=value line model,
and hide another format behind the 'final' terminating new-line. However
I thought this would be even more distuptive.

> If you're at all convinced by what I said above, then we also might be
> able to get away with having unique keys anyway.
> 
>>  Documentation/git-credential.txt |  19 ++-
>>  credential.c                     |  11 ++
>>  t/lib-credential-helper.sh       |  27 ++++
>>  t/t5556-http-auth.sh             | 242 +++++++++++++++++++++++++++++++
>>  4 files changed, 298 insertions(+), 1 deletion(-)
>>  create mode 100644 t/lib-credential-helper.sh
> 
> The patch itself looks pretty reasonable to me.
> 
> One small thing I noticed:
> 
>> +	git -c "credential.helper=!\"$CREDENTIAL_HELPER\"" ls-remote $ORIGIN_URL &&
> 
> As you undoubtedly figured out, the helper path is fed to the shell, so
> spaces in the trash directory are a problem. You've solved it here by
> adding a layer of double quotes, which handles spaces. But you'd run
> into problems if the absolute path that somebody is using for the test
> suite has a backslash or a double quote in it.
> 
> I don't know how careful we want to be here (or how careful we already
> are[1]), but one simple-ish solution is:
> 
>   export CREDENTIAL_HELPER
>   git -c "credential.helper=!\"\$CREDENTIAL_HELPER\"" ...
> 
> I.e., letting the inner shell expand the variable itself. Another option
> is to put the helper into $TRASH_DIRECTORY/bin and add that to the
> $PATH.
> 
> I also wondered if it was worth having setup_credential_helper() just
> stick it in $TRASH_DIRECTORY/.gitconfig so that individual tests don't
> have to keep doing that ugly "-c" invocation. Or if you really want to
> have each test enable it, perhaps have set_credential_reply() turn it on
> via test_config (which will auto-remove it at the end of the test).

Good ideas! I shall try those.

> -Peff
> 
> [1] Curious, I tried cloning git into this directory:
> 
>       mkdir '/tmp/foo/"horrible \"path\"'
> 
>     and we do indeed already fail. The first breakage I saw was recent,
>     but going further back, it looks like bin-wrappers don't correctly
>     handle this case anyway. So maybe that's evidence that nobody would
>     do something so ridiculous in practice.
Jeff King Feb. 9, 2023, 1:08 p.m. UTC | #3
On Mon, Feb 06, 2023 at 11:18:03AM -0800, Matthew John Cheetham wrote:

> > could be normalized as:
> > 
> >   www-auth-challenge=Basic realm="foo"
> >   www-auth-challenge=OtherAuth realm="bar"
> >   www-auth-challenge=YetAnotherScheme some-token
> > 
> > which saves each helper from having to do the same work. Likewise, we
> > can do a _little_ more parsing to get:
> > 
> >   www-auth-basic=realm="foo"
> >   www-auth-otherauth=realm="bar"
> >   www-auth-yetanotherscheme=some-token
> > 
> > I don't think we can go beyond there, though, without understanding the
> > syntax of individual schemes. Which is a shame, as one of the goals of
> > the credential format was to let the helpers do as little as possible
> > (so they can't get it wrong!). But helpers are stuck doing things like
> > handling backslashed double-quotes, soaking up extra whitespace, etc.
> 
> This key format wouldn't make it obviously easier for simple helpers to
> understand. Now they no longer have well-known keys but a key prefix.

Yes, though I don't think that's particularly complicated to parse.
Either way we're just flattening a tuple of (a, b, c) from "a=b c" to
"a-b=c". The value is in normalizing the syntax, so that helpers don't
have to deal with both "a=b c d e" and ("a=b c", "a=d e") themselves.

Another way to do that normalization would be to have Git convert:

  WWW-Authenticate: Basic realm="foo" OtherAuth realm="bar"

into:

 WWW-Authenticate: Basic realm="foo"
 WWW-Authenticate: OtherAuth realm="bar"

which then becomes (at the credential level):

  www-auth[]=Basic realm="foo"
  www-auth[]=OtherAuth realm="bar"

And likewise to normalize whitespace, etc, so each individual helper
doesn't have to (or risk getting confused/exploited). That said...

> My overall goal here is to have Git know less about auth, so it treats
> all values as totally opaque. The only logic added is around reconstructing
> folded headers, which is just HTTP and not auth specific.

Yeah, in general I agree with the notion that Git is mostly just passing
around opaque tokens. We do have to understand some syntax (like
folding!) at the HTTP level, so I think some syntactic normalization /
simplification is reasonable.

BUT. I think you are right that embedding it into the schema of the
helper protocol is probably bad. If the point is that the two forms of
my Basic / OtherAuth example are semantically equivalent, then we can
always decide later to convert between one and the other as a favor to
helpers. Whereas baking it into the schema is a promise for Git to
always parse and understand the headers.

So let me retract my suggestion, and we can leave "maybe normalize
headers to save helpers some work" as a possible topic for later (if
indeed it ever even becomes a problem in practice).

> >   realm=foo
> >   while read line; do
> >     case "$line" in
> >     www-auth-basic=)
> >         value=${line#*=}
> > 	# oops, we're just assuming it's realm= here, and we're
> > 	# not handling quotes at all. I think it could technically be
> > 	# realm=foo or realm="foo"
> > 	realm=${value#realm=}
> > 	;;
> >     esac
> >   done
> >   echo password=$(pass "pats-by-realm/$realm")
> > 
> > which could be made a lot easier if we did more parsing (e.g.,
> > www-auth-basic-realm or something). I dunno. Maybe that is just opening
> > up a can of worms, as we're stuffing structured data into a linearized
> > key-value list. The nice thing about your proposal is that Git does not
> > even have to know anything about these schemes; it's all the problem of
> > the helper. My biggest fear is just that we'll want to shift that later,
> > and we'll be stuck with this microformat forever.
> 
> I'm not sure there's such a continuous scale between simple and 'complex'
> helpers that would mean there'd be a simple shell script generating
> OAuth or DPoP credentials instead of a helper written in a higher-level
> language where parsing the headers is one of the simpler challenges faced.

For the most part, yeah. I tried to form the above example as something
that was really just relying on "basic", but taking in more information
/ context than we currently provide (and that your patch would provide).
I admit it's a stretch, though. Are there any servers which actually use
a Basic realm to distinguish between two credential's you'd want to
provide? I don't think I've seen one.

(Not to mention that people scripting helpers like this is probably
pretty rare; I do, but you can probably consider me a special case. And
if things got more complicated I'd just turn to Perl anyway. ;) ).

> I had considered another model whereby we forgo the key=value line model,
> and hide another format behind the 'final' terminating new-line. However
> I thought this would be even more distuptive.

Yeah, if we can shoe-horn this into the existing key/value model, that's
much better. The original intent with the final newline is that you
could read multiple credentials in a list, though in the end I don't
recall that we ever used that feature anyway.

-Peff
diff mbox series

Patch

diff --git a/Documentation/git-credential.txt b/Documentation/git-credential.txt
index ac2818b9f66..50759153ef1 100644
--- a/Documentation/git-credential.txt
+++ b/Documentation/git-credential.txt
@@ -113,7 +113,13 @@  separated by an `=` (equals) sign, followed by a newline.
 The key may contain any bytes except `=`, newline, or NUL. The value may
 contain any bytes except newline or NUL.
 
-In both cases, all bytes are treated as-is (i.e., there is no quoting,
+Attributes with keys that end with C-style array brackets `[]` can have
+multiple values. Each instance of a multi-valued attribute forms an
+ordered list of values - the order of the repeated attributes defines
+the order of the values. An empty multi-valued attribute (`key[]=\n`)
+acts to clear any previous entries and reset the list.
+
+In all cases, all bytes are treated as-is (i.e., there is no quoting,
 and one cannot transmit a value with newline or NUL in it). The list of
 attributes is terminated by a blank line or end-of-file.
 
@@ -160,6 +166,17 @@  empty string.
 Components which are missing from the URL (e.g., there is no
 username in the example above) will be left unset.
 
+`wwwauth[]`::
+
+	When an HTTP response is received by Git that includes one or more
+	'WWW-Authenticate' authentication headers, these will be passed by Git
+	to credential helpers.
++
+Each 'WWW-Authenticate' header value is passed as a multi-valued
+attribute 'wwwauth[]', where the order of the attributes is the same as
+they appear in the HTTP response. This attribute is 'one-way' from Git
+to pass additional information to credential helpers.
+
 Unrecognised attributes are silently discarded.
 
 GIT
diff --git a/credential.c b/credential.c
index 897b4679333..9f39ebc3c7e 100644
--- a/credential.c
+++ b/credential.c
@@ -263,6 +263,16 @@  static void credential_write_item(FILE *fp, const char *key, const char *value,
 	fprintf(fp, "%s=%s\n", key, value);
 }
 
+static void credential_write_strvec(FILE *fp, const char *key,
+				    const struct strvec *vec)
+{
+	char *full_key = xstrfmt("%s[]", key);
+	for (size_t i = 0; i < vec->nr; i++) {
+		credential_write_item(fp, full_key, vec->v[i], 0);
+	}
+	free(full_key);
+}
+
 void credential_write(const struct credential *c, FILE *fp)
 {
 	credential_write_item(fp, "protocol", c->protocol, 1);
@@ -270,6 +280,7 @@  void credential_write(const struct credential *c, FILE *fp)
 	credential_write_item(fp, "path", c->path, 0);
 	credential_write_item(fp, "username", c->username, 0);
 	credential_write_item(fp, "password", c->password, 0);
+	credential_write_strvec(fp, "wwwauth", &c->wwwauth_headers);
 }
 
 static int run_credential_helper(struct credential *c,
diff --git a/t/lib-credential-helper.sh b/t/lib-credential-helper.sh
new file mode 100644
index 00000000000..8b0e4414234
--- /dev/null
+++ b/t/lib-credential-helper.sh
@@ -0,0 +1,27 @@ 
+setup_credential_helper() {
+	test_expect_success 'setup credential helper' '
+		CREDENTIAL_HELPER="$TRASH_DIRECTORY/credential-helper.sh" &&
+		export CREDENTIAL_HELPER &&
+		echo $CREDENTIAL_HELPER &&
+
+		write_script "$CREDENTIAL_HELPER" <<-\EOF
+		cmd=$1
+		teefile=$cmd-query.cred
+		catfile=$cmd-reply.cred
+		sed -n -e "/^$/q" -e "p" >> $teefile
+		if test "$cmd" = "get"; then
+			cat $catfile
+		fi
+		EOF
+	'
+}
+
+set_credential_reply() {
+	cat >"$TRASH_DIRECTORY/$1-reply.cred"
+}
+
+expect_credential_query() {
+	cat >"$TRASH_DIRECTORY/$1-expect.cred" &&
+	test_cmp "$TRASH_DIRECTORY/$1-expect.cred" \
+		 "$TRASH_DIRECTORY/$1-query.cred"
+}
diff --git a/t/t5556-http-auth.sh b/t/t5556-http-auth.sh
index 2c16c8f72a5..93b7c178da6 100755
--- a/t/t5556-http-auth.sh
+++ b/t/t5556-http-auth.sh
@@ -4,6 +4,7 @@  test_description='test http auth header and credential helper interop'
 
 TEST_NO_CREATE_REPO=1
 . ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-credential-helper.sh
 
 test_set_port GIT_TEST_HTTP_PROTOCOL_PORT
 
@@ -33,6 +34,8 @@  test_expect_success 'setup repos' '
 	git -C "$REPO_DIR" branch -M main
 '
 
+setup_credential_helper
+
 run_http_server_worker() {
 	(
 		cd "$REPO_DIR"
@@ -101,6 +104,7 @@  per_test_cleanup () {
 	stop_http_server &&
 	rm -f OUT.* &&
 	rm -f IN.* &&
+	rm -f *.cred &&
 	rm -f auth.config
 }
 
@@ -218,4 +222,242 @@  test_expect_success 'http auth anonymous no challenge' '
 	git ls-remote $ORIGIN_URL
 '
 
+test_expect_success 'http auth www-auth headers to credential helper basic valid' '
+	test_when_finished "per_test_cleanup" &&
+	# base64("alice:secret-passwd")
+	USERPASS64=YWxpY2U6c2VjcmV0LXBhc3N3ZA== &&
+	export USERPASS64 &&
+
+	cat >auth.config <<-EOF &&
+	[auth]
+		challenge = basic:realm=\"example.com\"
+		token = basic:$USERPASS64
+	EOF
+
+	start_http_server --auth-config="$TRASH_DIRECTORY/auth.config" &&
+
+	set_credential_reply get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=secret-passwd
+	EOF
+
+	git -c "credential.helper=!\"$CREDENTIAL_HELPER\"" ls-remote $ORIGIN_URL &&
+
+	expect_credential_query get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	wwwauth[]=basic realm="example.com"
+	EOF
+
+	expect_credential_query store <<-EOF
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=secret-passwd
+	EOF
+'
+
+test_expect_success 'http auth www-auth headers to credential helper ignore case valid' '
+	test_when_finished "per_test_cleanup" &&
+	# base64("alice:secret-passwd")
+	USERPASS64=YWxpY2U6c2VjcmV0LXBhc3N3ZA== &&
+	export USERPASS64 &&
+
+	cat >auth.config <<-EOF &&
+	[auth]
+		challenge = basic:realm=\"example.com\"
+		token = basic:$USERPASS64
+		extraHeader = wWw-aUtHeNtIcAtE: bEaRer auThoRiTy=\"id.example.com\"
+	EOF
+
+	start_http_server --auth-config="$TRASH_DIRECTORY/auth.config" &&
+
+	set_credential_reply get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=secret-passwd
+	EOF
+
+	git -c "credential.helper=!\"$CREDENTIAL_HELPER\"" ls-remote $ORIGIN_URL &&
+
+	expect_credential_query get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	wwwauth[]=basic realm="example.com"
+	wwwauth[]=bEaRer auThoRiTy="id.example.com"
+	EOF
+
+	expect_credential_query store <<-EOF
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=secret-passwd
+	EOF
+'
+
+test_expect_success 'http auth www-auth headers to credential helper continuation hdr' '
+	test_when_finished "per_test_cleanup" &&
+	# base64("alice:secret-passwd")
+	USERPASS64=YWxpY2U6c2VjcmV0LXBhc3N3ZA== &&
+	export USERPASS64 &&
+
+	cat >auth.config <<-EOF &&
+	[auth]
+		challenge = "bearer:authority=\"id.example.com\"\\n    q=1\\n \\t p=0"
+		challenge = basic:realm=\"example.com\"
+		token = basic:$USERPASS64
+	EOF
+
+	start_http_server --auth-config="$TRASH_DIRECTORY/auth.config" &&
+
+	set_credential_reply get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=secret-passwd
+	EOF
+
+	git -c "credential.helper=!\"$CREDENTIAL_HELPER\"" ls-remote $ORIGIN_URL &&
+
+	expect_credential_query get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	wwwauth[]=bearer authority="id.example.com" q=1 p=0
+	wwwauth[]=basic realm="example.com"
+	EOF
+
+	expect_credential_query store <<-EOF
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=secret-passwd
+	EOF
+'
+
+test_expect_success 'http auth www-auth headers to credential helper empty continuation hdrs' '
+	test_when_finished "per_test_cleanup" &&
+	# base64("alice:secret-passwd")
+	USERPASS64=YWxpY2U6c2VjcmV0LXBhc3N3ZA== &&
+	export USERPASS64 &&
+
+	cat >auth.config <<-EOF &&
+	[auth]
+		challenge = basic:realm=\"example.com\"
+		token = basic:$USERPASS64
+		extraheader = "WWW-Authenticate:"
+		extraheader = " "
+		extraheader = " bearer authority=\"id.example.com\""
+	EOF
+
+	start_http_server --auth-config="$TRASH_DIRECTORY/auth.config" &&
+
+	set_credential_reply get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=secret-passwd
+	EOF
+
+	git -c "credential.helper=!\"$CREDENTIAL_HELPER\"" ls-remote $ORIGIN_URL &&
+
+	expect_credential_query get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	wwwauth[]=basic realm="example.com"
+	wwwauth[]=bearer authority="id.example.com"
+	EOF
+
+	expect_credential_query store <<-EOF
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=secret-passwd
+	EOF
+'
+
+test_expect_success 'http auth www-auth headers to credential helper custom schemes' '
+	test_when_finished "per_test_cleanup" &&
+	# base64("alice:secret-passwd")
+	USERPASS64=YWxpY2U6c2VjcmV0LXBhc3N3ZA== &&
+	export USERPASS64 &&
+
+	cat >auth.config <<-EOF &&
+	[auth]
+		challenge = "foobar:alg=test widget=1"
+		challenge = "bearer:authority=\"id.example.com\" q=1 p=0"
+		challenge = basic:realm=\"example.com\"
+		token = basic:$USERPASS64
+	EOF
+
+	start_http_server --auth-config="$TRASH_DIRECTORY/auth.config" &&
+
+	set_credential_reply get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=secret-passwd
+	EOF
+
+	git -c "credential.helper=!\"$CREDENTIAL_HELPER\"" ls-remote $ORIGIN_URL &&
+
+	expect_credential_query get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	wwwauth[]=foobar alg=test widget=1
+	wwwauth[]=bearer authority="id.example.com" q=1 p=0
+	wwwauth[]=basic realm="example.com"
+	EOF
+
+	expect_credential_query store <<-EOF
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=secret-passwd
+	EOF
+'
+
+test_expect_success 'http auth www-auth headers to credential helper invalid' '
+	test_when_finished "per_test_cleanup" &&
+	# base64("alice:secret-passwd")
+	USERPASS64=YWxpY2U6c2VjcmV0LXBhc3N3ZA== &&
+	export USERPASS64 &&
+
+	cat >auth.config <<-EOF &&
+	[auth]
+		challenge = "bearer:authority=\"id.example.com\" q=1 p=0"
+		challenge = basic:realm=\"example.com\"
+		token = basic:$USERPASS64
+	EOF
+
+	start_http_server --auth-config="$TRASH_DIRECTORY/auth.config" &&
+
+	set_credential_reply get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=invalid-passwd
+	EOF
+
+	test_must_fail git -c "credential.helper=!\"$CREDENTIAL_HELPER\"" ls-remote $ORIGIN_URL &&
+
+	expect_credential_query get <<-EOF &&
+	protocol=http
+	host=$HOST_PORT
+	wwwauth[]=bearer authority="id.example.com" q=1 p=0
+	wwwauth[]=basic realm="example.com"
+	EOF
+
+	expect_credential_query erase <<-EOF
+	protocol=http
+	host=$HOST_PORT
+	username=alice
+	password=invalid-passwd
+	wwwauth[]=bearer authority="id.example.com" q=1 p=0
+	wwwauth[]=basic realm="example.com"
+	EOF
+'
+
 test_done