Mailkit: NTLM with DefaultNetworkCredentials

Created on 29 Apr 2016  路  11Comments  路  Source: jstedfast/MailKit

Hello,

I鈥檓 trying to work out how to authenticate a sender on SMTP server using NTLM (Windows Authentication) thus avoiding the need to store any login credentials, just using CredentialCache.DefaultNetworkCredentials. But every time when I try it, I receive Authentication unsuccessful error. But when I call Authenticate function specifying user name and password it works, although user is the same.
What can be wrong?

For example this code fails:

``` c#
Client.AuthenticationMechanisms.Clear();
Client.AuthenticationMechanisms.Add("NTLM");
var Uri = new Uri(string.Format(@"{0}://{1}:{2}",
Settings.SecureSocketOptions == MailKit.Security.SecureSocketOptions.None ? "smtp" : "smtps",
Settings.Server,
Settings.Port));
ICredentials defCredentials = CredentialCache.DefaultNetworkCredentials;
NetworkCredential credential = defCredentials.GetCredential(Uri, "NTLM");
Client.Authenticate(credential);


This also:

``` c#
Client.AuthenticationMechanisms.Clear();
Client.AuthenticationMechanisms.Add("NTLM");
Client.Authenticate(CredentialCache.DefaultNetworkCredentials);

But this works:

``` c#
Client.AuthenticationMechanisms.Clear();
Client.AuthenticationMechanisms.Add("NTLM");
Client.Authenticate(new NetworkCredential(Settings.UserName, Settings.Password));


This also:

``` c#
Client.AuthenticationMechanisms.Clear();
Client.AuthenticationMechanisms.Add("NTLM");
Client.Authenticate(Settings.UserName, Settings.Password);

Thank you in advance.

question

Most helpful comment

I don't know if anyone has solved this issue yet.

I managed to get it working (tested against Exchange 2010), using the NSspi library for the SSPI auth part.

using NSspi;
using NSspi.Contexts;
using NSspi.Credentials;

/// <summary>
/// The NTLM Integrated Auth SASL mechanism.
/// </summary>
/// <remarks>
/// A SASL mechanism based on NTLM using the credentials of the current user 
/// via Windows Integrated Authentication (SSPI).
/// </remarks>
public class SaslMechanismNtlmIntegrated : SaslMechanism
{
    enum LoginState
    {
        Initial,
        Challenge
    }

    LoginState state;
    ClientContext sspiContext;

    /// <summary>
    /// Initializes a new instance of the <see cref="MailKit.Security.SaslMechanismNtlmIntegrated"/> class.
    /// </summary>
    /// <remarks>
    /// Creates a new NTLM Integrated Auth SASL context.
    /// </remarks>
    public SaslMechanismNtlmIntegrated() : base(string.Empty, string.Empty)
    {
    }

    /// <summary>
    /// Gets the name of the mechanism.
    /// </summary>
    /// <remarks>
    /// Gets the name of the mechanism.
    /// </remarks>
    /// <value>The name of the mechanism.</value>
    public override string MechanismName
    {
        get { return "NTLM"; }
    }

    /// <summary>
    /// Gets whether or not the mechanism supports an initial response (SASL-IR).
    /// </summary>
    /// <remarks>
    /// SASL mechanisms that support sending an initial client response to the server
    /// should return <value>true</value>.
    /// </remarks>
    /// <value><c>true</c> if the mechanism supports an initial response; otherwise, <c>false</c>.</value>
    public override bool SupportsInitialResponse
    {
        get { return true; }
    }

    /// <summary>
    /// Parses the server's challenge token and returns the next challenge response.
    /// </summary>
    /// <remarks>
    /// Parses the server's challenge token and returns the next challenge response.
    /// </remarks>
    /// <returns>The next challenge response.</returns>
    /// <param name="token">The server's challenge token.</param>
    /// <param name="startIndex">The index into the token specifying where the server's challenge begins.</param>
    /// <param name="length">The length of the server's challenge.</param>
    /// <exception cref="System.InvalidOperationException">
    /// The SASL mechanism is already authenticated.
    /// </exception>
    /// <exception cref="SaslException">
    /// An error has occurred while parsing the server's challenge token.
    /// </exception>
    protected override byte[] Challenge(byte[] token, int startIndex, int length)
    {
        if (IsAuthenticated)
            throw new InvalidOperationException();

        InitializeSSPIContext();

        byte[] serverResponse = null;
        SecurityStatus status;

        switch (state)
        {
            case LoginState.Initial:
                status = sspiContext.Init(null, out serverResponse);
                state = LoginState.Challenge;
                break;
            case LoginState.Challenge:
                status = sspiContext.Init(token, out serverResponse);
                IsAuthenticated = true;
                break;
            default:
                throw new IndexOutOfRangeException("state");
        }

        return serverResponse;
    }

    private void InitializeSSPIContext()
    {
        if (sspiContext != null)
        {
            return;
        }

        var credential = new ClientCurrentCredential(PackageNames.Ntlm);

        sspiContext = new ClientContext(
            credential,
            string.Empty,
            ContextAttrib.InitIntegrity |
            ContextAttrib.ReplayDetect |
            ContextAttrib.SequenceDetect |
            ContextAttrib.Confidentiality);
    }

    /// <summary>
    /// Resets the state of the SASL mechanism.
    /// </summary>
    /// <remarks>
    /// Resets the state of the SASL mechanism.
    /// </remarks>
    public override void Reset()
    {
        state = LoginState.Initial;
        base.Reset();
    }
}

Here's an example protocol log:

S: 220 EXCHANGE.mydomain.com Microsoft ESMTP MAIL Service ready at Tue, 19 Jun 2018 09:07:18 +0200
C: EHLO [127.0.0.1]
S: 250-EXCHANGE.mydomain.com Hello [10.10.10.2]
S: 250-SIZE
S: 250-PIPELINING
S: 250-DSN
S: 250-ENHANCEDSTATUSCODES
S: 250-STARTTLS
S: 250-X-ANONYMOUSTLS
S: 250-AUTH NTLM
S: 250-X-EXPS GSSAPI NTLM
S: 250-8BITMIME
S: 250-XEXCH50
S: 250-XRDST
S: 250 XSHADOW
C: AUTH NTLM TlRMTVNTUAABAAAA<rest of base64 removed>
S: 334 TlRMTVNTUAACAAAAFAAUA<rest of base64 removed>
C: TlRMTVNTUAADAAAAGAAYAIAAAA<rest of base64 removed>
S: 235 2.7.0 Authentication successful
C: QUIT
S: 221 2.0.0 Service closing transmission channel

All 11 comments

It seems that the reason is that in SaslMechanismNtlm.cs you read User name, Password and Domain from provided credentials, but in DefaultNetworkCredentials they are empty.

But I still has not found solution for my issue.
Is there something like SmtpClient.UseDefaultCredentials from System.Net.Mail?

There is nothing like SmtpClient.UseDefaultCredentials.

This is the way it should work:

Client.AuthenticationMechanisms.Clear();
Client.AuthenticationMechanisms.Add("NTLM");
Client.Authenticate(CredentialCache.DefaultNetworkCredentials);

If that doesn't work, then you need to figure out why it doesn't and fix that.

but in DefaultNetworkCredentials they are empty.

Most likely this means that the default credentials were never initialized (nothing MailKit can do about that).

As I understood CredentialCache.DefaultNetworkCredentials has empty UserName, Password and Domain by design. I just want to say, that most likely it is not correct to use CredentialCache.DefaultNetworkCredentials to authenticate as current user.

Have you ever tested the scenario when DefaultNetworkCredentials is used? Unfortunately for me it doesn't work.

I have examined the source code of System.Net.Mail
They also use CredentialCache.DefaultNetworkCredentials

``` C#
public bool UseDefaultCredentials {
get {
return (transport.Credentials is SystemNetworkCredential) ? true : false;
}
set {
if (InCall) {
throw new InvalidOperationException(SR.GetString(SR.SmtpInvalidOperationDuringSend));
}
transport.Credentials = value ? CredentialCache.DefaultNetworkCredentials : null;
}
}


But when DefaultNetworkCredentials  are used, they prepare m_CredentialsHandle  different way without reading User name and Password directly from provided credentials 

``` C#
//
// check if we're using DefaultCredentials
//
if (credential is SystemNetworkCredential)
{
          GlobalLog.Print("NTAuthentication#" + ValidationHelper.HashString(this) + "::.ctor(): using DefaultCredentials");
          m_CredentialsHandle = SSPIWrapper.AcquireDefaultCredential(
                                                GlobalSSPI.SSPIAuth,
                                                package,
                                                (m_IsServer? CredentialUse.Inbound: CredentialUse.Outbound));

          m_UniqueUserId = "/S"; // save off for unique connection marking ONLY used by HTTP client
}

....

``` C#
//
// we're not using DefaultCredentials, we need a
// AuthIdentity struct to contain credentials
// SECREVIEW:
// we'll save username/domain in temp strings, to avoid decrypting multiple times.
// password is only used once
//
string username = credential.InternalGetUserName();

string domain = credential.InternalGetDomain();
// ATTN:
// NetworkCredential class does not differentiate between null and "" but SSPI packages treat these cases differently
// For NTLM we want to keep "" for Wdigest.Dll we should use null.
AuthIdentity authIdentity = new AuthIdentity(username, credential.InternalGetPassword(), (object)package == (object)NegotiationInfoClass.WDigest && (domain == null || domain.Length == 0)? null: domain);

m_UniqueUserId = domain + "/" + username + "/U"; // save off for unique connection marking ONLY used by HTTP client

GlobalLog.Print("NTAuthentication#" + ValidationHelper.HashString(this) + "::.ctor(): using authIdentity:" + authIdentity.ToString());

m_CredentialsHandle = SSPIWrapper.AcquireCredentialsHandle(
GlobalSSPI.SSPIAuth,
package,
(m_IsServer? CredentialUse.Inbound: CredentialUse.Outbound),
ref authIdentity
);
```

I was going to suggest taking a look at the referencesource, but it looks like you did already :)

As I suspected, they are using internal API's that MailKit does not have access to.

This means you are out of luck trying to use CredentialCache.DefaultNetworkCredentials.

Sorry.

I don't know if anyone has solved this issue yet.

I managed to get it working (tested against Exchange 2010), using the NSspi library for the SSPI auth part.

using NSspi;
using NSspi.Contexts;
using NSspi.Credentials;

/// <summary>
/// The NTLM Integrated Auth SASL mechanism.
/// </summary>
/// <remarks>
/// A SASL mechanism based on NTLM using the credentials of the current user 
/// via Windows Integrated Authentication (SSPI).
/// </remarks>
public class SaslMechanismNtlmIntegrated : SaslMechanism
{
    enum LoginState
    {
        Initial,
        Challenge
    }

    LoginState state;
    ClientContext sspiContext;

    /// <summary>
    /// Initializes a new instance of the <see cref="MailKit.Security.SaslMechanismNtlmIntegrated"/> class.
    /// </summary>
    /// <remarks>
    /// Creates a new NTLM Integrated Auth SASL context.
    /// </remarks>
    public SaslMechanismNtlmIntegrated() : base(string.Empty, string.Empty)
    {
    }

    /// <summary>
    /// Gets the name of the mechanism.
    /// </summary>
    /// <remarks>
    /// Gets the name of the mechanism.
    /// </remarks>
    /// <value>The name of the mechanism.</value>
    public override string MechanismName
    {
        get { return "NTLM"; }
    }

    /// <summary>
    /// Gets whether or not the mechanism supports an initial response (SASL-IR).
    /// </summary>
    /// <remarks>
    /// SASL mechanisms that support sending an initial client response to the server
    /// should return <value>true</value>.
    /// </remarks>
    /// <value><c>true</c> if the mechanism supports an initial response; otherwise, <c>false</c>.</value>
    public override bool SupportsInitialResponse
    {
        get { return true; }
    }

    /// <summary>
    /// Parses the server's challenge token and returns the next challenge response.
    /// </summary>
    /// <remarks>
    /// Parses the server's challenge token and returns the next challenge response.
    /// </remarks>
    /// <returns>The next challenge response.</returns>
    /// <param name="token">The server's challenge token.</param>
    /// <param name="startIndex">The index into the token specifying where the server's challenge begins.</param>
    /// <param name="length">The length of the server's challenge.</param>
    /// <exception cref="System.InvalidOperationException">
    /// The SASL mechanism is already authenticated.
    /// </exception>
    /// <exception cref="SaslException">
    /// An error has occurred while parsing the server's challenge token.
    /// </exception>
    protected override byte[] Challenge(byte[] token, int startIndex, int length)
    {
        if (IsAuthenticated)
            throw new InvalidOperationException();

        InitializeSSPIContext();

        byte[] serverResponse = null;
        SecurityStatus status;

        switch (state)
        {
            case LoginState.Initial:
                status = sspiContext.Init(null, out serverResponse);
                state = LoginState.Challenge;
                break;
            case LoginState.Challenge:
                status = sspiContext.Init(token, out serverResponse);
                IsAuthenticated = true;
                break;
            default:
                throw new IndexOutOfRangeException("state");
        }

        return serverResponse;
    }

    private void InitializeSSPIContext()
    {
        if (sspiContext != null)
        {
            return;
        }

        var credential = new ClientCurrentCredential(PackageNames.Ntlm);

        sspiContext = new ClientContext(
            credential,
            string.Empty,
            ContextAttrib.InitIntegrity |
            ContextAttrib.ReplayDetect |
            ContextAttrib.SequenceDetect |
            ContextAttrib.Confidentiality);
    }

    /// <summary>
    /// Resets the state of the SASL mechanism.
    /// </summary>
    /// <remarks>
    /// Resets the state of the SASL mechanism.
    /// </remarks>
    public override void Reset()
    {
        state = LoginState.Initial;
        base.Reset();
    }
}

Here's an example protocol log:

S: 220 EXCHANGE.mydomain.com Microsoft ESMTP MAIL Service ready at Tue, 19 Jun 2018 09:07:18 +0200
C: EHLO [127.0.0.1]
S: 250-EXCHANGE.mydomain.com Hello [10.10.10.2]
S: 250-SIZE
S: 250-PIPELINING
S: 250-DSN
S: 250-ENHANCEDSTATUSCODES
S: 250-STARTTLS
S: 250-X-ANONYMOUSTLS
S: 250-AUTH NTLM
S: 250-X-EXPS GSSAPI NTLM
S: 250-8BITMIME
S: 250-XEXCH50
S: 250-XRDST
S: 250 XSHADOW
C: AUTH NTLM TlRMTVNTUAABAAAA<rest of base64 removed>
S: 334 TlRMTVNTUAACAAAAFAAUA<rest of base64 removed>
C: TlRMTVNTUAADAAAAGAAYAIAAAA<rest of base64 removed>
S: 235 2.7.0 Authentication successful
C: QUIT
S: 221 2.0.0 Service closing transmission channel

Hi @UriHendler
I have tried to implement your example but can't get it working.
Could you create an example of how to authenticate using your class?
I tried:
SaslMechanismNtlmIntegrated sasl = new SaslMechanismNtlmIntegrated(); SmtpClient client = new SmtpClient(); client.Connect("smtp.office365.com", 587, SecureSocketOptions.StartTls); client.Authenticate(sasl);
but it does not work..

@sorenaa65 That's how you would do it, but AFAIK, Office 365 doesn't support NTLM integrated authentication. That only works for Exchange on-premises.

See here: https://serverfault.com/questions/949471/office365-ntlm-authentication

Hi @UriHendler
Thank you for your answer. That actually make sense in hindsight :-)

interesting topic

Was this page helpful?
0 / 5 - 0 ratings