Skip to content

Instantly share code, notes, and snippets.

@abodalevsky
Last active January 22, 2020 16:55
Show Gist options
  • Save abodalevsky/8aa1ddbcfd6ba75d3fb517d98bca1956 to your computer and use it in GitHub Desktop.
Save abodalevsky/8aa1ddbcfd6ba75d3fb517d98bca1956 to your computer and use it in GitHub Desktop.
NTLM/Kerberos auth via SSPI and SPN generation
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