Skip to content

Commit ff123e8

Browse files
authored
Merge pull request dotnet#26979 from Priya91/newhttpauth
Add test infra for auth testing.
2 parents ebcc568 + 3fe3995 commit ff123e8

File tree

8 files changed

+615
-67
lines changed

8 files changed

+615
-67
lines changed
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Security.Cryptography;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
11+
namespace System.Net.Test.Common
12+
{
13+
public sealed partial class LoopbackServer
14+
{
15+
internal enum AuthenticationProtocols
16+
{
17+
Basic,
18+
Digest,
19+
None
20+
}
21+
22+
public async Task<List<string>> AcceptConnectionPerformAuthenticationAndCloseAsync(string authenticateHeaders)
23+
{
24+
List<string> lines = null;
25+
await AcceptConnectionAsync(async connection =>
26+
{
27+
await connection.ReadRequestHeaderAndSendResponseAsync(HttpStatusCode.Unauthorized, authenticateHeaders);
28+
29+
lines = await connection.ReadRequestHeaderAsync();
30+
Debug.Assert(lines.Count > 0);
31+
32+
int index = lines[0] != null ? lines[0].IndexOf(' ') : -1;
33+
string requestMethod = null;
34+
if (index != -1)
35+
{
36+
requestMethod = lines[0].Substring(0, index);
37+
}
38+
39+
// Read the authorization header from client.
40+
AuthenticationProtocols protocol = AuthenticationProtocols.None;
41+
string clientResponse = null;
42+
for (int i = 1; i < lines.Count; i++)
43+
{
44+
if (lines[i].StartsWith("Authorization"))
45+
{
46+
clientResponse = lines[i];
47+
if (lines[i].Contains(nameof(AuthenticationProtocols.Basic)))
48+
{
49+
protocol = AuthenticationProtocols.Basic;
50+
break;
51+
}
52+
else if (lines[i].Contains(nameof(AuthenticationProtocols.Digest)))
53+
{
54+
protocol = AuthenticationProtocols.Digest;
55+
break;
56+
}
57+
}
58+
}
59+
60+
bool success = false;
61+
switch (protocol)
62+
{
63+
case AuthenticationProtocols.Basic:
64+
success = IsBasicAuthTokenValid(clientResponse, _options);
65+
break;
66+
67+
case AuthenticationProtocols.Digest:
68+
// Read the request content.
69+
success = IsDigestAuthTokenValid(clientResponse, requestMethod, _options);
70+
break;
71+
}
72+
73+
if (success)
74+
{
75+
await connection.SendResponseAsync();
76+
}
77+
else
78+
{
79+
await connection.SendResponseAsync(HttpStatusCode.Unauthorized, authenticateHeaders);
80+
}
81+
});
82+
83+
return lines;
84+
}
85+
86+
internal static bool IsBasicAuthTokenValid(string clientResponse, LoopbackServer.Options options)
87+
{
88+
string clientHash = clientResponse.Substring(clientResponse.IndexOf(nameof(AuthenticationProtocols.Basic), StringComparison.OrdinalIgnoreCase) +
89+
nameof(AuthenticationProtocols.Basic).Length).Trim();
90+
string userPass = string.IsNullOrEmpty(options.Domain) ? options.Username + ":" + options.Password : options.Domain + "\\" + options.Username + ":" + options.Password;
91+
return clientHash == Convert.ToBase64String(Encoding.UTF8.GetBytes(userPass));
92+
}
93+
94+
internal static bool IsDigestAuthTokenValid(string clientResponse, string requestMethod, LoopbackServer.Options options)
95+
{
96+
string clientHash = clientResponse.Substring(clientResponse.IndexOf(nameof(AuthenticationProtocols.Digest), StringComparison.OrdinalIgnoreCase) +
97+
nameof(AuthenticationProtocols.Digest).Length).Trim();
98+
string[] values = clientHash.Split(',');
99+
100+
string username = null, uri = null, realm = null, nonce = null, response = null, algorithm = null, cnonce = null, opaque = null, qop = null, nc = null;
101+
bool userhash = false;
102+
for (int i = 0; i < values.Length; i++)
103+
{
104+
string trimmedValue = values[i].Trim();
105+
if (trimmedValue.Contains(nameof(username)))
106+
{
107+
// Username is a quoted string.
108+
int startIndex = trimmedValue.IndexOf('"');
109+
110+
if (startIndex != -1)
111+
{
112+
startIndex += 1;
113+
username = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
114+
}
115+
116+
// Username is mandatory.
117+
if (string.IsNullOrEmpty(username))
118+
return false;
119+
}
120+
else if (trimmedValue.Contains(nameof(userhash)) && trimmedValue.Contains("true"))
121+
{
122+
userhash = true;
123+
}
124+
else if (trimmedValue.Contains(nameof(uri)))
125+
{
126+
int startIndex = trimmedValue.IndexOf('"');
127+
if (startIndex != -1)
128+
{
129+
startIndex += 1;
130+
uri = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
131+
}
132+
133+
// Request uri is mandatory.
134+
if (string.IsNullOrEmpty(uri))
135+
return false;
136+
}
137+
else if (trimmedValue.Contains(nameof(realm)))
138+
{
139+
// Realm is a quoted string.
140+
int startIndex = trimmedValue.IndexOf('"');
141+
if (startIndex != -1)
142+
{
143+
startIndex += 1;
144+
realm = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
145+
}
146+
147+
// Realm is mandatory.
148+
if (string.IsNullOrEmpty(realm))
149+
return false;
150+
}
151+
else if (trimmedValue.Contains(nameof(cnonce)))
152+
{
153+
// CNonce is a quoted string.
154+
int startIndex = trimmedValue.IndexOf('"');
155+
if (startIndex != -1)
156+
{
157+
startIndex += 1;
158+
cnonce = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
159+
}
160+
}
161+
else if (trimmedValue.Contains(nameof(nonce)))
162+
{
163+
// Nonce is a quoted string.
164+
int startIndex = trimmedValue.IndexOf('"');
165+
if (startIndex != -1)
166+
{
167+
startIndex += 1;
168+
nonce = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
169+
}
170+
171+
// Nonce is mandatory.
172+
if (string.IsNullOrEmpty(nonce))
173+
return false;
174+
}
175+
else if (trimmedValue.Contains(nameof(response)))
176+
{
177+
// response is a quoted string.
178+
int startIndex = trimmedValue.IndexOf('"');
179+
if (startIndex != -1)
180+
{
181+
startIndex += 1;
182+
response = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
183+
}
184+
185+
// Response is mandatory.
186+
if (string.IsNullOrEmpty(response))
187+
return false;
188+
}
189+
else if (trimmedValue.Contains(nameof(algorithm)))
190+
{
191+
int startIndex = trimmedValue.IndexOf('=');
192+
if (startIndex != -1)
193+
{
194+
startIndex += 1;
195+
algorithm = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex).Trim();
196+
}
197+
}
198+
else if (trimmedValue.Contains(nameof(opaque)))
199+
{
200+
// Opaque is a quoted string.
201+
int startIndex = trimmedValue.IndexOf('"');
202+
if (startIndex != -1)
203+
{
204+
startIndex += 1;
205+
opaque = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
206+
}
207+
}
208+
else if (trimmedValue.Contains(nameof(qop)))
209+
{
210+
int startIndex = trimmedValue.IndexOf('"');
211+
if (startIndex != -1)
212+
{
213+
startIndex += 1;
214+
qop = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
215+
}
216+
else if ((startIndex = trimmedValue.IndexOf('=')) != -1)
217+
{
218+
startIndex += 1;
219+
qop = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex).Trim();
220+
}
221+
}
222+
else if (trimmedValue.Contains(nameof(nc)))
223+
{
224+
int startIndex = trimmedValue.IndexOf('=');
225+
if (startIndex != -1)
226+
{
227+
startIndex += 1;
228+
nc = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex).Trim();
229+
}
230+
}
231+
}
232+
233+
// Verify username.
234+
if (userhash && ComputeHash(options.Username + ":" + realm, algorithm) != username)
235+
{
236+
return false;
237+
}
238+
239+
if (!userhash && options.Username != username)
240+
{
241+
return false;
242+
}
243+
244+
if (string.IsNullOrEmpty(algorithm))
245+
algorithm = "sha-256";
246+
247+
// Calculate response and compare with the client response hash.
248+
string a1 = options.Username + ":" + realm + ":" + options.Password;
249+
if (algorithm.Contains("sess"))
250+
{
251+
a1 = ComputeHash(a1, algorithm) + ":" + nonce;
252+
253+
if (cnonce != null)
254+
a1 += ":" + cnonce;
255+
}
256+
257+
string a2 = requestMethod + ":" + uri;
258+
if (!string.IsNullOrEmpty(qop) && qop.Equals("auth-int"))
259+
{
260+
// Request content is empty.
261+
a2 = a2 + ":" + ComputeHash(string.Empty, algorithm);
262+
}
263+
264+
string serverResponseHash = ComputeHash(a1, algorithm) + ":" + nonce + ":";
265+
266+
if (nc != null)
267+
serverResponseHash += nc + ":";
268+
269+
if (cnonce != null)
270+
serverResponseHash += cnonce + ":";
271+
272+
if (qop != null)
273+
serverResponseHash += qop + ":";
274+
275+
serverResponseHash += ComputeHash(a2, algorithm);
276+
serverResponseHash = ComputeHash(serverResponseHash, algorithm);
277+
278+
return response == serverResponseHash;
279+
}
280+
281+
private static string ComputeHash(string data, string algorithm)
282+
{
283+
// Disable MD5 insecure warning.
284+
#pragma warning disable CA5351
285+
using (HashAlgorithm hash = algorithm.Contains("SHA-256") ? SHA256.Create() : (HashAlgorithm)MD5.Create())
286+
#pragma warning restore CA5351
287+
{
288+
Encoding enc = Encoding.UTF8;
289+
byte[] result = hash.ComputeHash(enc.GetBytes(data));
290+
291+
StringBuilder sb = new StringBuilder(result.Length * 2);
292+
foreach (byte b in result)
293+
sb.Append(b.ToString("x2"));
294+
295+
return sb.ToString();
296+
}
297+
}
298+
}
299+
}

0 commit comments

Comments
 (0)