开发者

NTLM Authentication. Can't get it to work in an IHttpModule. AcceptSecurityContext always fails

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 = i开发者_运维问答nArray)
                        {
                            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?


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).


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?

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜