credential: add WWW-Authenticate header to cred requests

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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Matthew John Cheetham committed Feb 27, 2023 at 17:20 UTC 5f2117b24f568ecc789c677748d70ccd538b16ba
3 files changed +263 -1
Documentation/git-credential.txt
+18 -1
@@ -113,7 +113,13 @@ separated by an `=` (equals) sign, followed by a newline.
113 The key may contain any bytes except `=`, newline, or NUL. The value may
114 contain any bytes except newline or NUL.
115
116 -In both cases, all bytes are treated as-is (i.e., there is no quoting,
116 +Attributes with keys that end with C-style array brackets `[]` can have
117 +multiple values. Each instance of a multi-valued attribute forms an
118 +ordered list of values - the order of the repeated attributes defines
119 +the order of the values. An empty multi-valued attribute (`key[]=\n`)
120 +acts to clear any previous entries and reset the list.
121 +
122 +In all cases, all bytes are treated as-is (i.e., there is no quoting,
123 and one cannot transmit a value with newline or NUL in it). The list of
124 attributes is terminated by a blank line or end-of-file.
125
@@ -160,6 +166,17 @@ empty string.
166 Components which are missing from the URL (e.g., there is no
167 username in the example above) will be left unset.
168
169 +`wwwauth[]`::
170 +
171 + When an HTTP response is received by Git that includes one or more
172 + 'WWW-Authenticate' authentication headers, these will be passed by Git
173 + to credential helpers.
174 ++
175 +Each 'WWW-Authenticate' header value is passed as a multi-valued
176 +attribute 'wwwauth[]', where the order of the attributes is the same as
177 +they appear in the HTTP response. This attribute is 'one-way' from Git
178 +to pass additional information to credential helpers.
179 +
180 Unrecognised attributes are silently discarded.
181
182 GIT
credential.c
+3
@@ -270,6 +270,9 @@ void credential_write(const struct credential *c, FILE *fp)
270 credential_write_item(fp, "path", c->path, 0);
271 credential_write_item(fp, "username", c->username, 0);
272 credential_write_item(fp, "password", c->password, 0);
273 + for (size_t i = 0; i < c->wwwauth_headers.nr; i++)
274 + credential_write_item(fp, "wwwauth[]", c->wwwauth_headers.v[i],
275 + 0);
276 }
277
278 static int run_credential_helper(struct credential *c,
t/t5563-simple-http-auth.sh
+242
@@ -70,6 +70,248 @@ test_expect_success 'access using basic auth' '
70 expect_credential_query get <<-EOF &&
71 protocol=http
72 host=$HTTPD_DEST
73 + wwwauth[]=Basic realm="example.com"
74 + EOF
75 +
76 + expect_credential_query store <<-EOF
77 + protocol=http
78 + host=$HTTPD_DEST
79 + username=alice
80 + password=secret-passwd
81 + EOF
82 +'
83 +
84 +test_expect_success 'access using basic auth invalid credentials' '
85 + test_when_finished "per_test_cleanup" &&
86 +
87 + set_credential_reply get <<-EOF &&
88 + username=baduser
89 + password=wrong-passwd
90 + EOF
91 +
92 + # Basic base64(alice:secret-passwd)
93 + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
94 + Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
95 + EOF
96 +
97 + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
98 + WWW-Authenticate: Basic realm="example.com"
99 + EOF
100 +
101 + test_config_global credential.helper test-helper &&
102 + test_must_fail git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
103 +
104 + expect_credential_query get <<-EOF &&
105 + protocol=http
106 + host=$HTTPD_DEST
107 + wwwauth[]=Basic realm="example.com"
108 + EOF
109 +
110 + expect_credential_query erase <<-EOF
111 + protocol=http
112 + host=$HTTPD_DEST
113 + username=baduser
114 + password=wrong-passwd
115 + wwwauth[]=Basic realm="example.com"
116 + EOF
117 +'
118 +
119 +test_expect_success 'access using basic auth with extra challenges' '
120 + test_when_finished "per_test_cleanup" &&
121 +
122 + set_credential_reply get <<-EOF &&
123 + username=alice
124 + password=secret-passwd
125 + EOF
126 +
127 + # Basic base64(alice:secret-passwd)
128 + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
129 + Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
130 + EOF
131 +
132 + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
133 + WWW-Authenticate: FooBar param1="value1" param2="value2"
134 + WWW-Authenticate: Bearer authorize_uri="id.example.com" p=1 q=0
135 + WWW-Authenticate: Basic realm="example.com"
136 + EOF
137 +
138 + test_config_global credential.helper test-helper &&
139 + git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
140 +
141 + expect_credential_query get <<-EOF &&
142 + protocol=http
143 + host=$HTTPD_DEST
144 + wwwauth[]=FooBar param1="value1" param2="value2"
145 + wwwauth[]=Bearer authorize_uri="id.example.com" p=1 q=0
146 + wwwauth[]=Basic realm="example.com"
147 + EOF
148 +
149 + expect_credential_query store <<-EOF
150 + protocol=http
151 + host=$HTTPD_DEST
152 + username=alice
153 + password=secret-passwd
154 + EOF
155 +'
156 +
157 +test_expect_success 'access using basic auth mixed-case wwwauth header name' '
158 + test_when_finished "per_test_cleanup" &&
159 +
160 + set_credential_reply get <<-EOF &&
161 + username=alice
162 + password=secret-passwd
163 + EOF
164 +
165 + # Basic base64(alice:secret-passwd)
166 + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
167 + Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
168 + EOF
169 +
170 + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
171 + www-authenticate: foobar param1="value1" param2="value2"
172 + WWW-AUTHENTICATE: BEARER authorize_uri="id.example.com" p=1 q=0
173 + WwW-aUtHeNtIcAtE: baSiC realm="example.com"
174 + EOF
175 +
176 + test_config_global credential.helper test-helper &&
177 + git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
178 +
179 + expect_credential_query get <<-EOF &&
180 + protocol=http
181 + host=$HTTPD_DEST
182 + wwwauth[]=foobar param1="value1" param2="value2"
183 + wwwauth[]=BEARER authorize_uri="id.example.com" p=1 q=0
184 + wwwauth[]=baSiC realm="example.com"
185 + EOF
186 +
187 + expect_credential_query store <<-EOF
188 + protocol=http
189 + host=$HTTPD_DEST
190 + username=alice
191 + password=secret-passwd
192 + EOF
193 +'
194 +
195 +test_expect_success 'access using basic auth with wwwauth header continuations' '
196 + test_when_finished "per_test_cleanup" &&
197 +
198 + set_credential_reply get <<-EOF &&
199 + username=alice
200 + password=secret-passwd
201 + EOF
202 +
203 + # Basic base64(alice:secret-passwd)
204 + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
205 + Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
206 + EOF
207 +
208 + # Note that leading and trailing whitespace is important to correctly
209 + # simulate a continuation/folded header.
210 + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF &&
211 + WWW-Authenticate: FooBar param1="value1"
212 + param2="value2"
213 + WWW-Authenticate: Bearer authorize_uri="id.example.com"
214 + p=1
215 + q=0
216 + WWW-Authenticate: Basic realm="example.com"
217 + EOF
218 +
219 + test_config_global credential.helper test-helper &&
220 + git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
221 +
222 + expect_credential_query get <<-EOF &&
223 + protocol=http
224 + host=$HTTPD_DEST
225 + wwwauth[]=FooBar param1="value1" param2="value2"
226 + wwwauth[]=Bearer authorize_uri="id.example.com" p=1 q=0
227 + wwwauth[]=Basic realm="example.com"
228 + EOF
229 +
230 + expect_credential_query store <<-EOF
231 + protocol=http
232 + host=$HTTPD_DEST
233 + username=alice
234 + password=secret-passwd
235 + EOF
236 +'
237 +
238 +test_expect_success 'access using basic auth with wwwauth header empty continuations' '
239 + test_when_finished "per_test_cleanup" &&
240 +
241 + set_credential_reply get <<-EOF &&
242 + username=alice
243 + password=secret-passwd
244 + EOF
245 +
246 + # Basic base64(alice:secret-passwd)
247 + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
248 + Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
249 + EOF
250 +
251 + CHALLENGE="$HTTPD_ROOT_PATH/custom-auth.challenge" &&
252 +
253 + # Note that leading and trailing whitespace is important to correctly
254 + # simulate a continuation/folded header.
255 + printf "">$CHALLENGE &&
256 + printf "WWW-Authenticate: FooBar param1=\"value1\"\r\n" >$CHALLENGE &&
257 + printf " \r\n" >>$CHALLENGE &&
258 + printf " param2=\"value2\"\r\n" >>$CHALLENGE &&
259 + printf "WWW-Authenticate: Bearer authorize_uri=\"id.example.com\"\r\n" >>$CHALLENGE &&
260 + printf " p=1\r\n" >>$CHALLENGE &&
261 + printf " \r\n" >>$CHALLENGE &&
262 + printf " q=0\r\n" >>$CHALLENGE &&
263 + printf "WWW-Authenticate: Basic realm=\"example.com\"\r\n" >>$CHALLENGE &&
264 +
265 + test_config_global credential.helper test-helper &&
266 + git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
267 +
268 + expect_credential_query get <<-EOF &&
269 + protocol=http
270 + host=$HTTPD_DEST
271 + wwwauth[]=FooBar param1="value1" param2="value2"
272 + wwwauth[]=Bearer authorize_uri="id.example.com" p=1 q=0
273 + wwwauth[]=Basic realm="example.com"
274 + EOF
275 +
276 + expect_credential_query store <<-EOF
277 + protocol=http
278 + host=$HTTPD_DEST
279 + username=alice
280 + password=secret-passwd
281 + EOF
282 +'
283 +
284 +test_expect_success 'access using basic auth with wwwauth header mixed line-endings' '
285 + test_when_finished "per_test_cleanup" &&
286 +
287 + set_credential_reply get <<-EOF &&
288 + username=alice
289 + password=secret-passwd
290 + EOF
291 +
292 + # Basic base64(alice:secret-passwd)
293 + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF &&
294 + Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA==
295 + EOF
296 +
297 + CHALLENGE="$HTTPD_ROOT_PATH/custom-auth.challenge" &&
298 +
299 + # Note that leading and trailing whitespace is important to correctly
300 + # simulate a continuation/folded header.
301 + printf "">$CHALLENGE &&
302 + printf "WWW-Authenticate: FooBar param1=\"value1\"\r\n" >$CHALLENGE &&
303 + printf " \r\n" >>$CHALLENGE &&
304 + printf "\tparam2=\"value2\"\r\n" >>$CHALLENGE &&
305 + printf "WWW-Authenticate: Basic realm=\"example.com\"" >>$CHALLENGE &&
306 +
307 + test_config_global credential.helper test-helper &&
308 + git ls-remote "$HTTPD_URL/custom_auth/repo.git" &&
309 +
310 + expect_credential_query get <<-EOF &&
311 + protocol=http
312 + host=$HTTPD_DEST
313 + wwwauth[]=FooBar param1="value1" param2="value2"
314 + wwwauth[]=Basic realm="example.com"
315 EOF
316
317 expect_credential_query store <<-EOF