これがセットアップです。ASP.Netサイトでは、特定のページでNTLM認証を使用する必要があります。これが機能する方法は、それらのページにのみ応答し、NTLM認証に必要な要求/応答を行ったり来たりするモジュールが存在することです。
NTLMはそれほど簡単ではないので、少し掘り下げてみると、カッシーニには実際にこの機能が組み込まれていることがわかりました。
http://cassinidev.codeplex.com/SourceControl/changeset/view/70631#1365123
関連する方法は次のとおりです。
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;
}
カッシーニがそのコードを使用する方法は次のとおりです。
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;
}
これが基本的なワークフローです。初めて、ヘッダーに「WWW-Authenticate:NTLM」を追加するだけです。クライアントはNTLMで応答します:いくつかのトークン文字列。この時点で、Cassiniはこの文字列を受け取り、それを使用して基になるAcceptSecurityContextWinAPI呼び出しを呼び出します。これにより、別のトークン文字列が生成され、クライアントに返送されます。次に、クライアントは別の暗号化されたトークン文字列を送り返し、CassiniはそれをAcceptSecurityContextメソッドに再度渡します。カッシーニアプリのこの時点で、認証は成功し、私たちは皆元気です。
モジュールでこれを再現しようとしましたが、何らかの理由で、最後のハンドシェイクで認証に失敗しました。
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();
}
}
呼び出すたびに、「SEC_E_INVALID_TOKEN」という応答が返されます。これは、ドキュメントによると、 「関数が失敗しました。関数に渡されたトークンが無効です。」という意味です。私のテストサイトはIISで実行されており、このモジュールはこの時点ですべての要求に対して実行されます。ヘッダーにKeep-Aliveを設定しています(NTLMは、最後の2つの応答/要求中に同じ接続を必要とします)。
私が試した他のこと:Fiddlerを使用して、カッシーニから返送されているヘッダーを確認し、モジュールに同じヘッダーを返送させてみました。運がない。サイトを運営しているユーザーを変更しようとしましたが、それも役に立ちませんでした。
基本的に、私の質問は、なぜそれが失敗し続けるのかということです。カッシーニが正常に認証できるのに、私のWebサイトが認証できないのはなぜですか?