NTLM 身份验证。无法让它在 IHttpModule 中工作。 AcceptSecurityContext 总是失败

发布于 2024-11-16 23:19:50 字数 8651 浏览 2 评论 0原文

这是设置。在 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

淡淡の花香 2024-11-23 23:19:50

我也遇到了这个问题。当您查看文档 和 Cassini 使用的 Authenticate 方法的代码,您会看到它期望 NtlmAuth 类的状态与步骤 2 和步骤 3 请求。

根据 phContext(第二个)参数的文档:在第一次调用 AcceptSecurityContext (NTLM) 时,此指针为 NULL。在后续调用中,phContext 是第一次调用在 phNewContext 参数中返回的部分形成的上下文的句柄。

从代码来看:当第一次调用 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 the NtlmAuth 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 the phNewContext 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 the securitycontext (_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 to AcceptSecurityContext and you never get authenticated. So you need to find a way to cache the state of the class or at least cache the securityContext obtained in the step 2 request (and off course the site needs to run under full trust).

窗影残 2024-11-23 23:19:50

我认为这与操作系统级别权限有关。 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?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文