Last active
January 22, 2020 16:55
-
-
Save abodalevsky/8aa1ddbcfd6ba75d3fb517d98bca1956 to your computer and use it in GitHub Desktop.
NTLM/Kerberos auth via SSPI and SPN generation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Logging.Interfaces; | |
using NSspi; | |
using NSspi.Contexts; | |
using NSspi.Credentials; | |
using System; | |
using System.IO; | |
using System.Net.Sockets; | |
using WebSocketSharp; | |
namespace WebSocket.Client.WSShImpl.AuthenticationStrategy | |
{ | |
/// <summary> | |
/// Implements SSPI based authentication algotithms | |
/// - NTLM | |
/// - NEGOTIATE | |
/// </summary> | |
internal abstract class AuthenticationStrategySspi : AuthenticationStrategyBase | |
{ | |
private readonly string packageName; | |
private readonly string spn = string.Empty; | |
protected string challengeMethod; | |
internal AuthenticationStrategySspi(string packageName, Uri proxyUri, ILogger logger) | |
: base(proxyUri, logger) | |
{ | |
this.spn = AuthenticationStrategySspi.CreateSpn(proxyUri, logger); | |
this.logger.Info($"SPN name: {this.spn}"); | |
this.packageName = packageName; | |
this.logger.Info("Created"); | |
} | |
public override AuthenticationStatus Try(ref TcpClient tcpClient, ref Stream stream, HttpRequest req, ref HttpResponse res) | |
{ | |
this.logger.Info("Try negotiate"); | |
if (this.IsTried) | |
{ | |
this.logger.Warn("Already has been tried, leaving."); | |
return AuthenticationStatus.FailedTryNext; | |
} | |
this.IsTried = true; | |
try | |
{ | |
if (res.HasConnectionClose) | |
{ | |
this.logger.Info("Server closed connection."); | |
ReleaseClientResources(ref tcpClient, ref stream); | |
tcpClient = new TcpClient(proxyUri.DnsSafeHost, proxyUri.Port); | |
stream = tcpClient.GetStream(); | |
} | |
this.logger.Info("Starting negotiation"); | |
var cred = new ClientCurrentCredential(packageName); | |
var context = new ClientContext(cred, this.spn, 0); | |
byte[] outBuf; | |
var status = context.Init(null, out outBuf); | |
this.logger.Info($"Status: {status}"); | |
if (!(status == SecurityStatus.OK || status == SecurityStatus.ContinueNeeded)) | |
{ | |
this.logger.Error($"Cannot continue negotiation, SecurityStatus: {status}"); | |
return AuthenticationStatus.FailedTryNext; | |
} | |
var token = Convert.ToBase64String(outBuf); | |
req.Headers["Proxy-Connection"] = @"keep-alive"; | |
req.Headers["Proxy-Authorization"] = this.GetAuthHeader(token); | |
res = SendHttpRequest(req, stream); | |
// if security status is SecurityStatus.ContinueNeeded then we should wait for response from server | |
while (status == SecurityStatus.ContinueNeeded && res.IsProxyAuthenticationRequired) | |
{ | |
// analyzing response | |
var serverToken = GetServerToken(res.Headers["Proxy-Authenticate"]); | |
if (string.IsNullOrEmpty(serverToken)) | |
{ | |
this.logger.Warn("Cannot get valid security token form server response, aborting"); | |
return AuthenticationStatus.FailedReset; | |
} | |
this.logger.Trace($"SERVER_TOKEN >{serverToken}<"); | |
this.logger.Info("Generating challenge response..."); | |
var inBuffer = Convert.FromBase64String(serverToken); | |
status = context.Init(inBuffer, out outBuf); | |
this.logger.Info($"Status: {status}"); | |
if (!(status == SecurityStatus.OK || status == SecurityStatus.ContinueNeeded)) | |
{ | |
this.logger.Error($"Cannot continue negotiation, SecurityStatus: {status}"); | |
return AuthenticationStatus.FailedTryNext; | |
} | |
var challengeResponse = Convert.ToBase64String(outBuf); | |
req.Headers["Proxy-Connection"] = @"keep-alive"; | |
req.Headers["Proxy-Authorization"] = this.GetAuthHeader(challengeResponse); | |
res = SendHttpRequest(req, stream); | |
} | |
if (res.IndicatesSuccess) | |
{ | |
this.logger.Info("Negotiation completed successfully"); | |
return AuthenticationStatus.Succsess; | |
} | |
else | |
{ | |
this.logger.Error($"Negotiation completed unsuccessfully"); | |
return AuthenticationStatus.FailedTryNext; | |
} | |
} | |
catch (Exception e) | |
{ | |
this.logger.Error($"Negotiation completed unsuccessfully: {e}"); | |
return AuthenticationStatus.FailedTryNext; | |
} | |
} | |
// SPN generation is based on article | |
// http://blog.michelbarneveld.nl/michel/archive/2009/11/14/the-reason-why-kb911149-and-kb908209-are-not-the-soluton.aspx | |
internal static string CreateSpn(Uri resource, ILogger logger) | |
{ | |
try | |
{ | |
logger.Info($"Creates SPN for: {resource}"); | |
if (resource.HostNameType == UriHostNameType.IPv6 || | |
resource.HostNameType == UriHostNameType.IPv4) | |
{ | |
logger.Trace("URI cannot be converted to SPN, not supported..."); | |
return string.Empty; | |
} | |
var spn = resource.Host; | |
if (string.IsNullOrEmpty(spn)) | |
{ | |
logger.Warn("Cannot get host for address"); | |
return string.Empty; | |
} | |
if (spn.IndexOf('.') == -1) | |
{ | |
logger.Trace("Short name detected, request FQN"); | |
spn = System.Net.Dns.GetHostEntry(spn).HostName; | |
if (string.IsNullOrEmpty(spn)) | |
{ | |
logger.Warn("Cannot get FQN for address"); | |
return string.Empty; | |
} | |
} | |
return $"HTTP/{spn}"; | |
} | |
catch (Exception e) | |
{ | |
logger.Error($"Error during SPN geenration: {e}"); | |
return string.Empty; | |
} | |
} | |
/// <summary> | |
/// Generates Proxy-Authorization headers depends on challenge method | |
/// </summary> | |
/// <param name="challenge"></param> | |
/// <returns></returns> | |
private string GetAuthHeader(string challenge) | |
{ | |
return $"{this.challengeMethod} {challenge}"; | |
} | |
private string GetServerToken(string authHeader) | |
{ | |
var chunks = authHeader.Trim().Split(new[] { ' ' }, 2); | |
if (chunks.Length != 2) | |
return string.Empty; | |
var schm = chunks[0].ToUpper(); | |
return schm == this.challengeMethod ? chunks[1] : string.Empty; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment