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

Handle 407 Proxy Authentication Required #946

Draft
wants to merge 1 commit into
base: beta
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
110 changes: 110 additions & 0 deletions src/Titanium.Web.Proxy/Handlers/WinAuthHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public partial class ProxyServer
"KerberosAuthorization"
};

private static readonly HashSet<string> proxyAuthHeaderNames = new(StringComparer.OrdinalIgnoreCase)
{
"Proxy-Authenticate"
};

/// <summary>
/// supported authentication schemes.
/// </summary>
Expand Down Expand Up @@ -141,6 +146,110 @@ private async Task Handle401UnAuthorized(SessionEventArgs args)
}
}

/// <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>
Expand All @@ -152,6 +261,7 @@ private async Task RewriteUnauthorizedResponse(SessionEventArgs args)

// 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 =
Expand Down
23 changes: 8 additions & 15 deletions src/Titanium.Web.Proxy/Network/WinAuth/Security/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,12 @@ internal class Common
internal static uint NewContextAttributes = 0;
internal static SecurityInteger NewLifeTime = new(0);

#region Private constants
#region Internal constants

private const int IscReqReplayDetect = 0x00000004;
private const int IscReqSequenceDetect = 0x00000008;
private const int IscReqConfidentiality = 0x00000010;
private const int IscReqConnection = 0x00000800;

#endregion

#region internal constants

internal const int StandardContextAttributes =
IscReqConfidentiality | IscReqReplayDetect | IscReqSequenceDetect | IscReqConnection;
internal const int IscReqReplayDetect = 0x00000004;
internal const int IscReqSequenceDetect = 0x00000008;
internal const int IscReqConfidentiality = 0x00000010;
internal const int IscReqConnection = 0x00000800;

internal const int SecurityNativeDataRepresentation = 0x10;
internal const int MaximumTokenSize = 12288;
Expand Down Expand Up @@ -160,13 +153,13 @@ internal SecurityBuffer(byte[] secBufferBytes, SecurityBufferType bufferType)
}

[StructLayout(LayoutKind.Sequential)]
internal struct SecurityBufferDesciption
internal struct SecurityBufferDescription
{
internal int ulVersion;
internal int cBuffers;
internal IntPtr pBuffers; // Point to SecBuffer

internal SecurityBufferDesciption(int bufferSize)
internal SecurityBufferDescription(int bufferSize)
{
ulVersion = (int)SecurityBufferType.SecbufferVersion;
cBuffers = 1;
Expand All @@ -175,7 +168,7 @@ internal SecurityBufferDesciption(int bufferSize)
Marshal.StructureToPtr(thisSecBuffer, pBuffers, false);
}

internal SecurityBufferDesciption(byte[] secBufferBytes)
internal SecurityBufferDescription(byte[] secBufferBytes)
{
ulVersion = (int)SecurityBufferType.SecbufferVersion;
cBuffers = 1;
Expand Down
35 changes: 21 additions & 14 deletions src/Titanium.Web.Proxy/Network/WinAuth/Security/WinAuthEndPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ internal class WinAuthEndPoint
/// <param name="hostname"></param>
/// <param name="authScheme"></param>
/// <param name="data"></param>
/// <param name="attributes"></param>
/// <returns></returns>
internal static byte[]? AcquireInitialSecurityToken(string hostname, string authScheme, InternalDataStore data)
internal static byte[]? AcquireInitialSecurityToken(string hostname, string authScheme, InternalDataStore data, int attributes)
{
byte[]? token;

// null for initial call
var serverToken = new SecurityBufferDesciption();
var serverToken = new SecurityBufferDescription();

var clientToken = new SecurityBufferDesciption(MaximumTokenSize);
var clientToken = new SecurityBufferDescription(MaximumTokenSize);

try
{
Expand All @@ -49,7 +50,7 @@ internal class WinAuthEndPoint
result = InitializeSecurityContext(ref state.Credentials,
IntPtr.Zero,
hostname,
StandardContextAttributes,
attributes,
0,
SecurityNativeDataRepresentation,
ref serverToken,
Expand Down Expand Up @@ -80,15 +81,16 @@ internal class WinAuthEndPoint
/// <param name="hostname"></param>
/// <param name="serverChallenge"></param>
/// <param name="data"></param>
/// <param name="attributes"></param>
/// <returns></returns>
internal static byte[]? AcquireFinalSecurityToken(string hostname, byte[] serverChallenge, InternalDataStore data)
internal static byte[]? AcquireFinalSecurityToken(string hostname, byte[] serverChallenge, InternalDataStore data, int attributes)
{
byte[]? token;

// user server challenge
var serverToken = new SecurityBufferDesciption(serverChallenge);
var serverToken = new SecurityBufferDescription(serverChallenge);

var clientToken = new SecurityBufferDesciption(MaximumTokenSize);
var clientToken = new SecurityBufferDescription(MaximumTokenSize);

try
{
Expand All @@ -99,7 +101,7 @@ internal class WinAuthEndPoint
var result = InitializeSecurityContext(ref state.Credentials,
ref state.Context,
hostname,
StandardContextAttributes,
attributes,
0,
SecurityNativeDataRepresentation,
ref serverToken,
Expand All @@ -123,7 +125,7 @@ internal class WinAuthEndPoint
return token;
}

private static void DisposeToken(SecurityBufferDesciption clientToken)
private static void DisposeToken(SecurityBufferDescription clientToken)
{
if (clientToken.pBuffers != IntPtr.Zero)
{
Expand Down Expand Up @@ -186,6 +188,11 @@ internal static bool ValidateWinAuthState(InternalDataStore data, State.WinAuthS
(state!.AuthState == State.WinAuthState.InitialToken ||
state.AuthState ==
State.WinAuthState.Authorized); // Server may require re-authentication on an open connection

if (expectedAuthState == State.WinAuthState.FinalToken)
return !stateExists ||
(state!.AuthState == State.WinAuthState.FinalToken ||
state.AuthState == State.WinAuthState.Authorized);

throw new Exception("Unsupported validation of WinAuthState");
}
Expand All @@ -212,24 +219,24 @@ internal static void AuthenticatedResponse(InternalDataStore data)
int fContextReq,
int reserved1,
int targetDataRep,
ref SecurityBufferDesciption pInput, // PSecBufferDesc SecBufferDesc
ref SecurityBufferDescription pInput, // PSecBufferDesc SecBufferDesc
int reserved2,
out SecurityHandle phNewContext, // PCtxtHandle
out SecurityBufferDesciption pOutput, // PSecBufferDesc SecBufferDesc
out SecurityBufferDescription pOutput, // PSecBufferDesc SecBufferDesc
out uint pfContextAttr, // managed ulong == 64 bits!!!
out SecurityInteger ptsExpiry); // PTimeStamp

[DllImport("secur32", CharSet = CharSet.Auto, SetLastError = true)]
[DllImport("secur32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int InitializeSecurityContext(ref SecurityHandle phCredential, // PCredHandle
ref SecurityHandle phContext, // PCtxtHandle
string pszTargetName,
int fContextReq,
int reserved1,
int targetDataRep,
ref SecurityBufferDesciption secBufferDesc, // PSecBufferDesc SecBufferDesc
ref SecurityBufferDescription secBufferDesc, // PSecBufferDesc SecBufferDesc
int reserved2,
out SecurityHandle phNewContext, // PCtxtHandle
out SecurityBufferDesciption pOutput, // PSecBufferDesc SecBufferDesc
out SecurityBufferDescription pOutput, // PSecBufferDesc SecBufferDesc
out uint pfContextAttr, // managed ulong == 64 bits!!!
out SecurityInteger ptsExpiry); // PTimeStamp

Expand Down
54 changes: 51 additions & 3 deletions src/Titanium.Web.Proxy/Network/WinAuth/WinAuthHandler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Network.WinAuth.Security;
using static Titanium.Web.Proxy.Network.WinAuth.Security.Common;

namespace Titanium.Web.Proxy.Network.WinAuth;

Expand All @@ -21,7 +22,10 @@ internal static class WinAuthHandler
/// <returns></returns>
internal static string GetInitialAuthToken(string serverHostname, string authScheme, InternalDataStore data)
{
var tokenBytes = WinAuthEndPoint.AcquireInitialSecurityToken(serverHostname, authScheme, data);
var tokenBytes = WinAuthEndPoint.AcquireInitialSecurityToken(serverHostname,
authScheme,
data,
IscReqConfidentiality | IscReqReplayDetect | IscReqSequenceDetect | IscReqConnection);
return string.Concat(" ", Convert.ToBase64String(tokenBytes));
}

Expand All @@ -35,8 +39,52 @@ internal static string GetInitialAuthToken(string serverHostname, string authSch
internal static string GetFinalAuthToken(string serverHostname, string serverToken, InternalDataStore data)
{
var tokenBytes =
WinAuthEndPoint.AcquireFinalSecurityToken(serverHostname, Convert.FromBase64String(serverToken),
data);
WinAuthEndPoint.AcquireFinalSecurityToken(serverHostname,
Convert.FromBase64String(serverToken),
data,
IscReqConfidentiality | IscReqReplayDetect | IscReqSequenceDetect | IscReqConnection);

return string.Concat(" ", Convert.ToBase64String(tokenBytes));
}

// NTLM authentication with the proxy server works in a different way and different ISC_REQ_* flags need to be passed
// Chromium sets ISC_REQ_DELEGATE | ISC_REQ_MUTUAL_AUTH as seen in https://chromium.googlesource.com/chromium/src/net/+/b8c947c21ffb46f616ece1948ba0545e671cf23e/http/http_auth_sspi_win.cc#546
// cURL uses no flags since commit https://github.com/curl/curl/commit/8ee182288af1bd828613fdcab2e7e8b551e91901 (now moved in lib/vauth/ntlm_sspi.c)
// .NET since 6.0.4 has chosen ISC_REQ_CONNECTION https://github.com/dotnet/runtime/pull/66305/files
// CNTLM (the new maintained version) instead passes ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION https://github.com/versat/cntlm/blob/d6a47bb5c2489503e3d97e52685b8dc10300da96/sspi.c#L239
// This is Microsoft documentation for the InitializeSecurityContext function https://learn.microsoft.com/en-us/windows/win32/secauthn/initializesecuritycontext--general
// The whole thing seems pretty randomic...

/// <summary>
/// Get the initial client token for proxy server
/// using credentials of user running the proxy server process
/// </summary>
/// <param name="proxyHostname"></param>
/// <param name="authScheme"></param>
/// <param name="data"></param>
/// <returns></returns>
internal static string GetInitialProxyAuthToken(string proxyHostname, string authScheme, InternalDataStore data)
{
var tokenBytes = WinAuthEndPoint.AcquireInitialSecurityToken(proxyHostname,
authScheme,
data,
0);
return string.Concat(" ", Convert.ToBase64String(tokenBytes));
}

/// <summary>
/// Get the final token given the proxy server challenge token
/// </summary>
/// <param name="proxyHostname"></param>
/// <param name="serverToken"></param>
/// <param name="data"></param>
/// <returns></returns>
internal static string GetFinalProxyAuthToken(string proxyHostname, string serverToken, InternalDataStore data)
{
var tokenBytes = WinAuthEndPoint.AcquireFinalSecurityToken(proxyHostname,
Convert.FromBase64String(serverToken),
data,
0);

return string.Concat(" ", Convert.ToBase64String(tokenBytes));
}
Expand Down
2 changes: 1 addition & 1 deletion src/Titanium.Web.Proxy/RequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ private async Task OnBeforeRequest(SessionEventArgs args)
/// <summary>
/// Invoke before request handler if it is set.
/// </summary>
/// <param name="request">The COONECT request.</param>
/// <param name="request">The CONNECT request.</param>
/// <returns></returns>
internal async Task OnBeforeUpStreamConnectRequest(ConnectRequest request)
{
Expand Down