Skip to content
This repository was archived by the owner on Jul 9, 2023. It is now read-only.

Commit 5d988a0

Browse files
authored
Merge pull request #398 from bjowes/WinAuth_unauthorized_state_fix
State handling of WinAuth, fixes issue with failed authentication
2 parents fce1aa5 + f679b82 commit 5d988a0

File tree

4 files changed

+123
-3
lines changed

4 files changed

+123
-3
lines changed

Titanium.Web.Proxy/Network/WinAuth/Security/State.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,24 @@ namespace Titanium.Web.Proxy.Network.WinAuth.Security
77
/// </summary>
88
internal class State
99
{
10+
/// <summary>
11+
/// States during Windows Authentication
12+
/// </summary>
13+
public enum WinAuthState
14+
{
15+
UNAUTHORIZED,
16+
INITIAL_TOKEN,
17+
FINAL_TOKEN,
18+
AUTHORIZED
19+
};
20+
1021
internal State()
1122
{
1223
Credentials = new Common.SecurityHandle(0);
1324
Context = new Common.SecurityHandle(0);
1425

1526
LastSeen = DateTime.Now;
27+
AuthState = WinAuthState.UNAUTHORIZED;
1628
}
1729

1830
/// <summary>
@@ -30,10 +42,16 @@ internal State()
3042
/// </summary>
3143
internal DateTime LastSeen;
3244

45+
/// <summary>
46+
/// Current state of the authentication process
47+
/// </summary>
48+
internal WinAuthState AuthState;
49+
3350
internal void ResetHandles()
3451
{
3552
Credentials.Reset();
3653
Context.Reset();
54+
AuthState = WinAuthState.UNAUTHORIZED;
3755
}
3856

3957
internal void UpdatePresence()

Titanium.Web.Proxy/Network/WinAuth/Security/WinAuthEndPoint.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ internal static byte[] AcquireInitialSecurityToken(string hostname, string authS
7979
return null;
8080
}
8181

82+
state.AuthState = State.WinAuthState.INITIAL_TOKEN;
8283
token = clientToken.GetBytes();
8384
authStates.Add(requestId, state);
8485
}
@@ -135,7 +136,7 @@ internal static byte[] AcquireFinalSecurityToken(string hostname, byte[] serverC
135136
return null;
136137
}
137138

138-
authStates.Remove(requestId);
139+
state.AuthState = State.WinAuthState.FINAL_TOKEN;
139140
token = clientToken.GetBytes();
140141
}
141142
finally
@@ -166,6 +167,51 @@ internal static async void ClearIdleStates(int stateCacheTimeOutMinutes)
166167
await Task.Delay(1000 * 60);
167168
}
168169

170+
/// <summary>
171+
/// Validates that the current WinAuth state of the connection matches the
172+
/// expectation, used to detect failed authentication
173+
/// </summary>
174+
/// <param name="requestId"></param>
175+
/// <param name="expectedAuthState"></param>
176+
/// <returns></returns>
177+
internal static bool ValidateWinAuthState(Guid requestId, State.WinAuthState expectedAuthState)
178+
{
179+
State state;
180+
var stateExists = authStates.TryGetValue(requestId, out state);
181+
182+
if (expectedAuthState == State.WinAuthState.UNAUTHORIZED)
183+
{
184+
// Validation before initial token
185+
return (stateExists == false ||
186+
state.AuthState == State.WinAuthState.UNAUTHORIZED ||
187+
state.AuthState == State.WinAuthState.AUTHORIZED); // Server may require re-authentication on an open connection
188+
}
189+
190+
if (expectedAuthState == State.WinAuthState.INITIAL_TOKEN)
191+
{
192+
// Validation before final token
193+
return (stateExists &&
194+
(state.AuthState == State.WinAuthState.INITIAL_TOKEN ||
195+
state.AuthState == State.WinAuthState.AUTHORIZED)); // Server may require re-authentication on an open connection
196+
}
197+
198+
throw new Exception("Unsupported validation of WinAuthState");
199+
}
200+
201+
/// <summary>
202+
/// Set the AuthState to authorized and update the connection state lifetime
203+
/// </summary>
204+
/// <param name="requestId"></param>
205+
internal static void AuthenticatedResponse(Guid requestId)
206+
{
207+
State state;
208+
if (authStates.TryGetValue(requestId, out state))
209+
{
210+
state.AuthState = State.WinAuthState.AUTHORIZED;
211+
state.UpdatePresence();
212+
}
213+
}
214+
169215
#region Native calls to secur32.dll
170216

171217
[DllImport("secur32.dll", SetLastError = true)]

Titanium.Web.Proxy/ResponseHandler.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Titanium.Web.Proxy.EventArguments;
88
using Titanium.Web.Proxy.Exceptions;
99
using Titanium.Web.Proxy.Extensions;
10+
using Titanium.Web.Proxy.Network.WinAuth.Security;
1011

1112
namespace Titanium.Web.Proxy
1213
{
@@ -31,9 +32,16 @@ private async Task HandleHttpSessionResponse(SessionEventArgs args)
3132
args.ReRequest = false;
3233

3334
//check for windows authentication
34-
if (isWindowsAuthenticationEnabledAndSupported && response.StatusCode == (int)HttpStatusCode.Unauthorized)
35+
if (isWindowsAuthenticationEnabledAndSupported)
3536
{
36-
await Handle401UnAuthorized(args);
37+
if (response.StatusCode == (int)HttpStatusCode.Unauthorized)
38+
{
39+
await Handle401UnAuthorized(args);
40+
}
41+
else
42+
{
43+
WinAuthEndPoint.AuthenticatedResponse(args.WebSession.RequestId);
44+
}
3745
}
3846

3947
response.OriginalHasBody = response.HasBody;

Titanium.Web.Proxy/WinAuthHandler.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Titanium.Web.Proxy.Http;
77
using Titanium.Web.Proxy.Models;
88
using Titanium.Web.Proxy.Network.WinAuth;
9+
using Titanium.Web.Proxy.Network.WinAuth.Security;
910

1011
namespace Titanium.Web.Proxy
1112
{
@@ -84,6 +85,14 @@ internal async Task Handle401UnAuthorized(SessionEventArgs args)
8485
{
8586
string scheme = authSchemes.FirstOrDefault(x => authHeader.Value.Equals(x, StringComparison.OrdinalIgnoreCase));
8687

88+
if ((scheme != null && !WinAuthEndPoint.ValidateWinAuthState(args.WebSession.RequestId, State.WinAuthState.UNAUTHORIZED)) ||
89+
(scheme == null && !WinAuthEndPoint.ValidateWinAuthState(args.WebSession.RequestId, State.WinAuthState.INITIAL_TOKEN)))
90+
{
91+
// Invalid state, create proper error message to client
92+
await RewriteUnauthorizedResponse(args);
93+
return;
94+
}
95+
8796
var request = args.WebSession.Request;
8897

8998
//clear any existing headers to avoid confusing bad servers
@@ -134,5 +143,44 @@ internal async Task Handle401UnAuthorized(SessionEventArgs args)
134143
args.ReRequest = true;
135144
}
136145
}
146+
147+
/// <summary>
148+
/// Rewrites the response body for failed authentication
149+
/// </summary>
150+
/// <param name="args"></param>
151+
/// <returns></returns>
152+
internal async Task RewriteUnauthorizedResponse(SessionEventArgs args)
153+
{
154+
var response = args.WebSession.Response;
155+
// Strip authentication headers to avoid credentials prompt in client web browser
156+
foreach (var authHeaderName in authHeaderNames)
157+
{
158+
response.Headers.RemoveHeader(authHeaderName);
159+
}
160+
161+
// Add custom div to body to clarify that the proxy (not the client browser) failed authentication
162+
string authErrorMessage = "<div class=\"inserted-by-proxy\"><h2>NTLM authentication through Titanium.Web.Proxy (" +
163+
args.ProxyClient.TcpClient.Client.LocalEndPoint +
164+
") failed. Please check credentials.</h2></div>";
165+
string originalErrorMessage = "<div class=\"inserted-by-proxy\"><h3>Response from remote web server below.</h3></div><br/>";
166+
string body = await args.GetResponseBodyAsString();
167+
if (body.ToLower().Contains("<body>"))
168+
{
169+
var bodyPos = body.ToLower().IndexOf("<body>") + ("<body>").Length;
170+
body = body.Insert(bodyPos, authErrorMessage + originalErrorMessage);
171+
}
172+
else
173+
{
174+
// Cannot parse response body, replace it
175+
body = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" +
176+
"<html xmlns=\"http://www.w3.org/1999/xhtml\">" +
177+
"<body>" +
178+
authErrorMessage +
179+
"</body>" +
180+
"</html>";
181+
}
182+
183+
args.SetResponseBodyString(body);
184+
}
137185
}
138186
}

0 commit comments

Comments
 (0)