NTLM 身份验证。无法让它在 IHttpModule 中工作。 AcceptSecurityContext 总是失败
这是设置。在 ASP.Net 站点上,我们希望在特定页面上进行 NTLM 身份验证。其工作方式是有一个模块仅响应这些页面,然后执行 NTLM 身份验证所需的来回请求/响应。
NTLM 并不是那么容易,所以经过一番挖掘,我发现 Cassini 实际上内置了这个功能:
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;
}
以下是 Cassini 如何使用该代码:
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 获取此字符串,并使用它来调用底层 AcceptSecurityContext WinAPI 调用。这会生成另一个令牌字符串,该字符串又被发送回客户端。然后,客户端发回另一个加密的令牌字符串,然后 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 在最后两个响应/请求期间需要相同的连接)。
我尝试过的其他事情:使用 Fiddler,我查看了从 Cassini 发回的标头,并尝试让我的模块发回相同的标头。运气不好。我尝试更改网站运行的用户,但这也没有帮助。
基本上,我的问题是,为什么它总是失败?为什么 Cassini 可以成功验证,但我的网站却不能?
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?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我也遇到了这个问题。当您查看文档 和 Cassini 使用的
Authenticate
方法的代码,您会看到它期望NtlmAuth
类的状态与步骤 2 和步骤 3 请求。根据 phContext(第二个)参数的文档:在第一次调用
AcceptSecurityContext
(NTLM) 时,此指针为 NULL。在后续调用中,phContext
是第一次调用在phNewContex
t 参数中返回的部分形成的上下文的句柄。从代码来看:当第一次调用
AcceptSecurityContext
成功时,它将布尔变量_securityContextAcquired
设置为 true,它会获取securitycontext
的句柄(_securityContext) 并创建一个需要在响应中发回的 blob。你有这个权利。但是,由于您在每个请求上实例化
NtlmAuth
,因此您会丢失状态,因此_securityContextAcquired
为 false,对于第 3 步请求,_securityContext
为 null,因此它会通过null 作为AcceptSecurityContext
的第二个参数,并且您永远不会获得身份验证。因此,您需要找到一种方法来缓存类的状态,或者至少缓存在步骤 2 请求中获取的securityContext
(当然,站点需要在完全信任的情况下运行)。I ran into this problem as well. When you review the documentation and the code of the
Authenticate
method Cassini uses, you see that it expects the state of theNtlmAuth
class to be the same for the step 2 and step 3 requests.From the documentation for the phContext (2nd) parameter: On the first call to
AcceptSecurityContext
(NTLM), this pointer is NULL. On subsequent calls,phContext
is the handle to the partially formed context that was returned in thephNewContex
t parameter by the first call.From the code: when the first call to
AcceptSecurityContext
succeeds it sets boolean variable_securityContextAcquired
to true, it gets a handle to thesecuritycontext
(_securityContext) and creates a blob that you need to send back in your response.You had that right. But since you instantiate
NtlmAuth
on every request you lose your state, hence_securityContextAcquired
is false,_securityContext
is null for your step 3 request, it passes null as 2nd parameter toAcceptSecurityContext
and you never get authenticated. So you need to find a way to cache the state of the class or at least cache thesecurityContext
obtained in the step 2 request (and off course the site needs to run under full trust).我认为这与操作系统级别权限有关。 Asp.net 通常作为 NetworkService 执行,但可能会作为 Inet_machine 进行非托管调用,而 Inet_machine 没有使用 API 调用的权限。
Cassini 在您的计算机帐户下运行,因此执行调用的方式也不同。
您可以尝试使用模拟配置指令或更改应用程序池执行的用户(取决于您的 IIS)。
另一个想法是,您是否考虑过使用 IIS 来阻止对受限文件的访问,而不是在 asp.net 中进行?
I think it's related to OS level permissions. Asp.net usually executes as NetworkService but may be making the unmanaged calls as Inet_machine, which doesn't have permission to use the API calls.
Cassini runs under your machine account, so is executing the calls differently.
You could try using the impersonate config directive or change the user the app pool executes as (dependant on your IIS).
Another thought, have you considered using IIS to block access to the the restricted files rather than doing it in asp.net?