This repository was archived by the owner on Jul 9, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 639
/
Copy pathWinAuthHandler.cs
294 lines (235 loc) · 11.6 KB
/
WinAuthHandler.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Extensions;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
using Titanium.Web.Proxy.Network.WinAuth;
using Titanium.Web.Proxy.Network.WinAuth.Security;
namespace Titanium.Web.Proxy;
public partial class ProxyServer
{
/// <summary>
/// possible header names.
/// </summary>
private static readonly HashSet<string> authHeaderNames = new(StringComparer.OrdinalIgnoreCase)
{
"WWW-Authenticate",
// IIS 6.0 messed up names below
"WWWAuthenticate",
"NTLMAuthorization",
"NegotiateAuthorization",
"KerberosAuthorization"
};
private static readonly HashSet<string> proxyAuthHeaderNames = new(StringComparer.OrdinalIgnoreCase)
{
"Proxy-Authenticate"
};
/// <summary>
/// supported authentication schemes.
/// </summary>
private static readonly HashSet<string> authSchemes = new(StringComparer.OrdinalIgnoreCase)
{
"NTLM",
"Negotiate",
"Kerberos"
};
/// <summary>
/// Handle windows NTLM/Kerberos authentication.
/// Note: NTLM/Kerberos cannot do a man in middle operation
/// we do for HTTPS requests.
/// As such we will be sending local credentials of current
/// User to server to authenticate requests.
/// To disable this set ProxyServer.EnableWinAuth to false.
/// </summary>
private async Task Handle401UnAuthorized(SessionEventArgs args)
{
string? headerName = null;
HttpHeader? authHeader = null;
var response = args.HttpClient.Response;
// check in non-unique headers first
var header = response.Headers.NonUniqueHeaders.FirstOrDefault(x => authHeaderNames.Contains(x.Key));
if (!header.Equals(new KeyValuePair<string, List<HttpHeader>>())) headerName = header.Key;
if (headerName != null)
authHeader = response.Headers.NonUniqueHeaders[headerName]
.FirstOrDefault(
x => authSchemes.Any(y => x.Value.StartsWith(y, StringComparison.OrdinalIgnoreCase)));
// check in unique headers
if (authHeader == null)
{
headerName = null;
// check in non-unique headers first
var uHeader = response.Headers.Headers.FirstOrDefault(x => authHeaderNames.Contains(x.Key));
if (!uHeader.Equals(new KeyValuePair<string, HttpHeader>())) headerName = uHeader.Key;
if (headerName != null)
authHeader = authSchemes.Any(x => response.Headers.Headers[headerName].Value
.StartsWith(x, StringComparison.OrdinalIgnoreCase))
? response.Headers.Headers[headerName]
: null;
}
if (authHeader != null)
{
var scheme = authSchemes.Contains(authHeader.Value) ? authHeader.Value : null;
var expectedAuthState =
scheme == null ? State.WinAuthState.InitialToken : State.WinAuthState.Unauthorized;
if (!WinAuthEndPoint.ValidateWinAuthState(args.HttpClient.Data, expectedAuthState))
{
// Invalid state, create proper error message to client
await RewriteUnauthorizedResponse(args);
return;
}
var request = args.HttpClient.Request;
// clear any existing headers to avoid confusing bad servers
request.Headers.RemoveHeader(KnownHeaders.Authorization);
// initial value will match exactly any of the schemes
if (scheme != null)
{
var clientToken = WinAuthHandler.GetInitialAuthToken(request.Host!, scheme, args.HttpClient.Data);
var auth = string.Concat(scheme, clientToken);
// replace existing authorization header if any
request.Headers.SetOrAddHeaderValue(KnownHeaders.Authorization, auth);
// don't need to send body for Authorization request
if (request.HasBody) request.ContentLength = 0;
}
else
{
// challenge value will start with any of the scheme selected
scheme = authSchemes.First(x =>
authHeader.Value.StartsWith(x, StringComparison.OrdinalIgnoreCase) &&
authHeader.Value.Length > x.Length + 1);
var serverToken = authHeader.Value.Substring(scheme.Length + 1);
var clientToken = WinAuthHandler.GetFinalAuthToken(request.Host!, serverToken, args.HttpClient.Data);
var auth = string.Concat(scheme, clientToken);
// there will be an existing header from initial client request
request.Headers.SetOrAddHeaderValue(KnownHeaders.Authorization, auth);
// send body for final auth request
if (request.OriginalHasBody) request.ContentLength = request.Body.Length;
args.HttpClient.Connection.IsWinAuthenticated = true;
}
// Need to revisit this.
// Should we cache all Set-Cookie headers from server during auth process
// and send it to client after auth?
// Let ResponseHandler send the updated request
args.ReRequest = true;
}
}
/// <summary>
/// Handle windows NTLM/Kerberos proxy authentication.
/// Note: NTLM/Kerberos cannot do a man in middle operation
/// we do for HTTPS requests.
/// As such we will be sending local credentials of current
/// User to server to authenticate requests.
/// To disable this set ProxyServer.EnableWinAuth to false.
/// </summary>
private async Task Handle407ProxyAuthorization(SessionEventArgs args)
{
string? headerName = null;
HttpHeader? authHeader = null;
var response = args.HttpClient.Response;
// check in non-unique headers first
var header = response.Headers.NonUniqueHeaders.FirstOrDefault(x => proxyAuthHeaderNames.Contains(x.Key));
if (!header.Equals(new KeyValuePair<string, List<HttpHeader>>())) headerName = header.Key;
if (headerName != null)
authHeader = response.Headers.NonUniqueHeaders[headerName]
.FirstOrDefault(
x => authSchemes.Any(y => x.Value.StartsWith(y, StringComparison.OrdinalIgnoreCase)));
// check in unique headers
if (authHeader == null)
{
headerName = null;
// check in non-unique headers first
var uHeader = response.Headers.Headers.FirstOrDefault(x => proxyAuthHeaderNames.Contains(x.Key));
if (!uHeader.Equals(new KeyValuePair<string, HttpHeader>())) headerName = uHeader.Key;
if (headerName != null)
authHeader = authSchemes.Any(x => response.Headers.Headers[headerName].Value
.StartsWith(x, StringComparison.OrdinalIgnoreCase))
? response.Headers.Headers[headerName]
: null;
}
if (authHeader != null)
{
var scheme = authSchemes.Contains(authHeader.Value) ? authHeader.Value : null;
var expectedAuthState =
scheme == null ? State.WinAuthState.InitialToken : State.WinAuthState.FinalToken;
if (!WinAuthEndPoint.ValidateWinAuthState(args.HttpClient.Data, expectedAuthState))
{
// Invalid state, create proper error message to client
await RewriteUnauthorizedResponse(args);
return;
}
var request = args.HttpClient.Request;
// clear any existing headers to avoid confusing bad servers
request.Headers.RemoveHeader(KnownHeaders.ProxyAuthorization);
// initial value will match exactly any of the schemes
if (scheme != null)
{
var clientToken = WinAuthHandler.GetInitialProxyAuthToken(args.CustomUpStreamProxyUsed!.HostName, scheme, args.HttpClient.Data);
var auth = string.Concat(scheme, clientToken);
// replace existing authorization header if any
request.Headers.SetOrAddHeaderValue(KnownHeaders.ProxyAuthorization, auth);
// don't need to send body for Authorization request
if (request.HasBody) request.ContentLength = 0;
}
else
{
// challenge value will start with any of the scheme selected
scheme = authSchemes.First(x =>
authHeader.Value.StartsWith(x, StringComparison.OrdinalIgnoreCase) &&
authHeader.Value.Length > x.Length + 1);
var serverToken = authHeader.Value.Substring(scheme.Length + 1);
var clientToken = WinAuthHandler.GetFinalProxyAuthToken(args.CustomUpStreamProxyUsed!.HostName, serverToken, args.HttpClient.Data);
var auth = string.Concat(scheme, clientToken);
// there will be an existing header from initial client request
request.Headers.SetOrAddHeaderValue(KnownHeaders.ProxyAuthorization, auth);
// send body for final auth request
if (request.OriginalHasBody) request.ContentLength = request.Body.Length;
args.HttpClient.Connection.IsWinAuthenticated = true;
}
// Need to revisit this.
// Should we cache all Set-Cookie headers from server during auth process
// and send it to client after auth?
// Let ResponseHandler send the updated request
args.ReRequest = true;
}
}
/// <summary>
/// Rewrites the response body for failed authentication
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private async Task RewriteUnauthorizedResponse(SessionEventArgs args)
{
var response = args.HttpClient.Response;
// Strip authentication headers to avoid credentials prompt in client web browser
foreach (var authHeaderName in authHeaderNames) response.Headers.RemoveHeader(authHeaderName);
foreach (var proxyAuthHeaderName in proxyAuthHeaderNames) response.Headers.RemoveHeader(proxyAuthHeaderName);
// Add custom div to body to clarify that the proxy (not the client browser) failed authentication
var authErrorMessage =
"<div class=\"inserted-by-proxy\"><h2>NTLM authentication through Titanium.Web.Proxy (" +
args.ClientLocalEndPoint +
") failed. Please check credentials.</h2></div>";
var originalErrorMessage =
"<div class=\"inserted-by-proxy\"><h3>Response from remote web server below.</h3></div><br/>";
var body = await args.GetResponseBodyAsString(args.CancellationTokenSource.Token);
var idx = body.IndexOfIgnoreCase("<body>");
if (idx >= 0)
{
var bodyPos = idx + "<body>".Length;
body = body.Insert(bodyPos, authErrorMessage + originalErrorMessage);
}
else
{
// Cannot parse response body, replace it
body =
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" +
"<html xmlns=\"http://www.w3.org/1999/xhtml\">" +
"<body>" +
authErrorMessage +
"</body>" +
"</html>";
}
args.SetResponseBodyString(body);
}
}