Here's the setup. On an ASP.Net site, we want to have NTLM authentication on specific pages. The way this will work is there will be a module that will only respond to those pages, and then do the back and forth request/response required for NTLM Authentication.
NTLM is not all that easy, so after some digging, I found that Cassini actually has this functionality built into it:
http://cassinidev.codeplex.com/SourceControl/changeset/view/70631#1365123
Here's the relevant method:
public unsafe bool Authenticate(string blobString)
{
_blob = null;
byte[] buffer = Convert.FromBase64String(blobString);
byte[] inArray = new byte[0x4000];
fixed (void* ptrRef = &_securityContext)
{
fixed (void* ptrRef2 = &_inputBuffer)
{
fixed (void* ptrRef3 = &_outputBuffer)
{
fixed (void* ptrRef4 = buffer)
{
fixed (void* ptrRef5 = inArray)
{
IntPtr zero = IntPtr.Zero;
if (_securityContextAcquired)
{
zero = (IntPtr) ptrRef;
}
_inputBufferDesc.ulVersion = 0;
_inputBufferDesc.cBuffers = 1;
_inputBufferDesc.pBuffers = (IntPtr) ptrRef2;
_inputBuffer.cbBuffer = (uint) buffer.Length;
_inputBuffer.BufferType = 2;
_inputBuffer.pvBuffer = (IntPtr) ptrRef4;
_outputBufferDesc.ulVersion = 0;
_outputBufferDesc.cBuffers = 1;
_outputBufferDesc.pBuffers = (IntPtr) ptrRef3;
_outputBuffer.cbBuffer = (uint) inArray.Length;
_outputBuffer.BufferType = 2;
_outputBuffer.pvBuffer = (IntPtr) ptrRef5;
int num = Interop.AcceptSecurityContext(ref _credentialsHandle, zero,
ref _inputBufferDesc, 20,
0, ref _securityContext, ref _outputBufferDesc,
ref _securityContextAttributes, ref _timestamp);
if (num == 0x90312)
{
_securityContextAcquired = true;
_blob = Convert.ToBase64String(inArray, 0, (int) _outputBuffer.cbBuffer);
}
else
{
if (num != 0)
{
return false;
}
IntPtr phToken = IntPtr.Zero;
if (Interop.QuerySecurityContextToken(ref _securityContext, ref phToken) != 0)
{
return false;
}
try
{
using (WindowsIdentity identity = new WindowsIdentity(phToken))
{
_sid = identity.User;
}
}
finally
{
Interop.CloseHandle(phToken);
}
_completed = true;
}
}
}
}
}
}
return true;
}
Here's how Cassini uses that code:
http://cassinidev.codeplex.com/SourceControl/changeset/view/70631#1365119
private bool TryNtlmAuthenticate()
{
try
{
using (var auth = new NtlmAuth())
{
do
{
string blobString = null;
string extraHeaders = _knownRequestHeaders[0x18];
if ((extraHeaders != null) && extraHeaders.StartsWith("NTLM ", StringComparison.Ordinal))
{
blobString = extraHeaders.Substring(5);
}
if (blobString != null)
{
if (!auth.Authenticate(blobString))
{
_connection.WriteErrorAndClose(0x193);
return false;
}
if (auth.Completed)
{
goto Label_009A;
}
extraHeaders = "WWW-Authenticate: NTLM " + auth.Blob + "\r\n";
}
else
{
extraHeaders = "WWW-Authenticate: NTLM\r\n";
}
SkipAllPostedContent();
_connection.WriteErrorWithExtraHeadersAndKeepAlive(0x191, extraHeaders);
} while (TryParseRequest());
return false;
Label_009A:
if (_host.GetProcessSid() != auth.SID)
{
_connection.WriteErrorAndClose(0x193);
return false;
}
}
}
catch
{
try
{
_connection.WriteErrorAndClose(500);
}
// ReSharper disable EmptyGeneralCatchClause
catch
// ReSharper restore EmptyGeneralCatchClause
{
}
return false;
}
return true;
}
Here's the basic workflow. First time around, it just adds "WWW-Authenticate: NTLM" to the header. The client responsds with NTLM: some token string. At this point Cassini takes this string, and uses it to call the underlying AcceptSecurityContext WinAPI call. That generates another token string, which is in turn sent back to the client. The client then sends back another encrypted token string and Cassini then passes that off to the AcceptSecurityContext method again. At this point in the Cassini app, the authentication succeeds, and we're all good.
I've tried reproducing this in my Module, but for some reason, on the final handshake, I fail to authenticate:
public class TestModule : IHttpModule
{
public void Dispose()
{
}
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(context_BeginRequest);
}
void context_BeginRequest(object sender, EventArgs e)
{
var context = HttpContext.Current;
var headers = context.Request.Headers;
if (String.IsNullOrEmpty(headers.Get("Authorization")))
{
context.Response.StatusCode = 401;
context.Response.AddHeader("WWW-Authenticate", "NTLM");
}
else
{
Step2(context);
}
}
private void Step2(HttpContext httpContext)
{
using (var auth = new NtlmAuth())
{
var header = httpContext.Request.Headers["Authorization"].Substring(5);
var result = auth.Authenticate(header); //third time around, this returns false. AcceptSecurityContext in NtmlAuth fails....
if (!result)
{
ReturnUnauthorized(httpContext);
}
else if (!auth.Completed)
{
HttpContext.Current.Response.Charset = null;
HttpContext.Current.Response.ContentType = null;
httpContext.Response.StatusCode = 401;
httpContext.Response.AddHeader("WWW-Authenticate", "NTLM " + auth.Blob);
httpContext.Response.End();
}
else
{
httpContext.Response.StatusCode = 200;
httpContext.Response.Write("Yay!");
httpContext.Response.End();
}
}
}
private void ReturnUnauthorized(HttpContext httpContext)
{
httpContext.Response.StatusCode = 403;
httpContext.Response.End();
}
}
Every time I call it, I get a response of: "SEC_E_INVALID_TOKEN" which according to the documentation means: "The function failed. The token passed to the function is not valid.". My test site is running in IIS, and this module runs for all requests at this point. I have Keep-Alive being set in the headers (NTLM needs the same connection during the final two response/request).
Other things I've tried: using Fiddler, I looked at the headers being sent back from Cassini, and tried having my module send those same headers back. No luck. I've tried changing the user that the site runs under, but that didn't help either.
Basically, my question is, why does it keep failing? Why can Cassini successfully authenticate, but my web site can't?