EWS Managed API Double Hop

1.2k views Asked by At

I'm developing an intranet site that will use in on-premise. In corporate, users can use this site like OWA they can see their inbox, send mails etc. To achive this I use EWS Managed Api 2.2 to connect Exchange Server (2010_sp1).I am developing with ASPNet MVC 5. I am developing on my computer with IIS 10.0 installed.While I'm developing with IISExpress, there were no problem to connect with default credentials >

ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2010_SP1);
service.UseDefaultCredentials = true;

I understand that, this is because IIS Express use my credentials as default credentials so there were no error to connect service. But when I run this code with IIS on my computer(with my credentials in Application Pool Identity, using ASPNet Impersonation enabled, Windows Authentication enabled and NEGOTIATE/NTML providers) identities load correct

HttpContext.User.Identity.Name > xxx\billgates
WindowsIdentity.GetCurrent().Name> xxx\billgates

but Exchange service returns in trace these >

<Trace Tag="EwsRequestHttpHeaders" Tid="29" Time="2017-01-02 08:03:04Z">
POST /EWS/Exchange.asmx HTTP/1.1
Content-Type: text/xml; charset=utf-8
Accept: text/xml
User-Agent: ExchangeServicesClient/15.00.0913.015
Accept-Encoding: gzip,deflate 
</Trace>

<Trace Tag="EwsRequest" Tid="29" Time="2017-01-02 08:03:04Z" Version="15.00.0913.015">
  <?xml version="1.0" encoding="utf-8"?>
  <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Header>
      <t:RequestServerVersion Version="Exchange2010_SP1" />
    </soap:Header>
    <soap:Body>
      <m:GetFolder>
        <m:FolderShape>
          <t:BaseShape>AllProperties</t:BaseShape>
        </m:FolderShape>
        <m:FolderIds>
          <t:DistinguishedFolderId Id="inbox" />
        </m:FolderIds>
      </m:GetFolder>
    </soap:Body>
  </soap:Envelope>
</Trace>

<Trace Tag="EwsResponseHttpHeaders" Tid="29" Time="2017-01-02 08:03:04Z">
HTTP/1.1 401 Unauthorized
Server: Microsoft-IIS/7.5
WWW-Authenticate: Negotiate,NTLM
X-Powered-By: ASP.NET
Date: Mon, 02 Jan 2017 08:03:04 GMT
Content-Length: 0 
</Trace>

When I use EWEditor(https://ewseditor.codeplex.com/) I saw there is no error with connect with default credentials and when run klist in powershell there are kerberos tickes like>

Server: krbtgt/[email protected] KerbTicket Encryption Type: RSADSI RC4-HMAC(NT) End Time: 1/2/2017 21:35:37 Renew Time: 1/9/2017 11:35:37

Server: krbtgt/[email protected] KerbTicket Encryption Type: Unknown (18) End Time: 1/2/2017 21:35:37 Renew Time: 1/9/2017 11:35:37

Server: HTTP/[email protected] KerbTicket Encryption Type: RSADSI RC4-HMAC(NT) End Time: 1/2/2017 21:35:37 Renew Time: 1/9/2017 11:35:37

Server: HTTP/[email protected] KerbTicket Encryption Type: RSADSI RC4-HMAC(NT) End Time: 1/2/2017 21:35:37 Renew Time: 1/9/2017 11:35:37

Server: HTTP/[email protected] KerbTicket Encryption Type: Unknown (18) End Time: 1/2/2017 21:35:37 Renew Time: 1/9/2017 11:35:37

Server: ldap/[email protected] KerbTicket Encryption Type: Unknown (18) End Time: 1/2/2017 21:35:37 Renew Time: 1/9/2017 11:35:37

with these tickets I assume that kerberos works, my first question is that am I right?Can I be sure that these tickets guarantee that keberos works with client access servers?

While search for this problem I saw that this problem very like double hop problem like this blog says>https://blogs.msdn.microsoft.com/dhruvkh/2012/04/15/the-double-hop-dogma/ .Actually my problem is exactly like this post. Like this post recommends I use my account as service account and add SPNS to my account and write that in Application Pool Identity in IIS

SPNs that I used

http/autodiscover.xxxxxx.xxx.xx

http/posta.xxxxxx.xxx.xx

NOTE : I am sure these SPNs not used.

After add spns I also check "Trust this computer for delegation to any service (Kerberos only)." in Delegation tab of my user account.

My other question is that like post suggest on 3. step https://technet.microsoft.com/en-us/library/ff808312(v=exchg.141).aspx should I create an Alternate Service Account instead of using my account?If I have to use ASA,should I add Alternate Service Account Credentials in IIS Application Pool Identity?

Thanks for helping.

1

There are 1 answers

0
Chris Barnes On

Update from previous post. I have gotten this to work and my current settings are not what I would have expected to produce results. I'm using IIS 8.5 on Win 2012 R2

  1. AppPool: set to integrated pipeline
  2. AppPool: Load User Profile: False
  3. AppPool: IIS running under a domain service account configured for delegation
  4. In the App > Authentication. ASP.Net Impersonation: Disabled. Windows Authentication: Enabled w/kernel mode off. Providers: Negotiate:Keberos.
  5. Configuration Editor > system.webServer/security/authentication/windowsAuthentication useAppPoolCredentials: True. useKernelMode: False
  6. Local Security Policy > Local Policies > User Rights Assignment: The service account must be in a group listed in the policy "Impersonate a client after authentication"

Modification to Aspnet.config (C:\Windows\Microsoft.NET\Framework64\v4.0.30319)

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <runtime>
        <legacyUnhandledExceptionPolicy enabled="false" />
        <legacyImpersonationPolicy enabled="false"/>
        <alwaysFlowImpersonationPolicy enabled="true"/>
        <SymbolReadingPolicy enabled="1" />
        <shadowCopyVerifyByTimestamp enabled="true"/>
    </runtime>
    <startup useLegacyV2RuntimeActivationPolicy="true" />
</configuration>

Below is the service source that is working. I left in all the commented out code so you can see everything that I have been trying. Note only some of the active code is needed.

     //using(HostingEnvironment.Impersonate())
     //{
        try
        {
           //DC = new PrincipalContext(ContextType.Domain, myDomain);
           //GP = GroupPrincipal.FindByIdentity(DC, IdentityType.Name, Constant.QMS_GROUP_PRINCIPAL);
           //ImpersonationContext = ((System.Security.Principal.WindowsIdentity)System.Threading.Thread.CurrentPrincipal.Identity).Impersonate();

           //the service creation gets called regularly and 
           //autodiscovery is very slow
           if(PerformanceHelper.GetMailService() != null)
           {
              return PerformanceHelper.GetMailService();
           }

           //create a service
           ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2010_SP2);

           service.TraceListener = new TraceListener();
           service.TraceEnabled = true;
           service.TraceFlags = TraceFlags.All;
           service.CookieContainer = new CookieContainer(1);
           service.PreAuthenticate = true;

           //CredentialCache credCache = new CredentialCache();

           //credCache.Add(new Uri(Constant.QMS_EXCHANGE_SVC), "Negotiate",

           //       CredentialCache.DefaultNetworkCredentials);

           //HttpWebRequest req = (HttpWebRequest)WebRequest.Create(new Uri(Constant.QMS_EXCHANGE_SVC));

           //req.Credentials = credCache;

           //service.Credentials = credCache.GetCredential(new Uri(Constant.QMS_EXCHANGE_SVC), "Negotiate");

           NetworkCredential nc = new NetworkCredential();
           nc = CredentialCache.DefaultNetworkCredentials;
           //nc = (NetworkCredential)CredentialCache.DefaultCredentials;

           service.Credentials = nc;

           ErrorHelper.AddErrorMsg("makeExchangeConnection IsWindowsIdentityFlowSuppressed() (info): " 
              + Convert.ToString(System.Security.SecurityContext.IsWindowsIdentityFlowSuppressed()));

           //WebCredentials wc = new WebCredentials(System.Net.CredentialCache.DefaultCredentials);

           //WebCredentials wc = new WebCredentials(System.Net.CredentialCache.DefaultCredentials);
           //service.Credentials = wc;

           ErrorHelper.AddErrorMsg("makeExchangeConnection (info): " + System.Threading.Thread.CurrentPrincipal.Identity.Name);

           //use the windows login credential
           service.UseDefaultCredentials = true;

           //the autodiscovery has been failing in dev. 
           //the catch should rescue the connetcion.
           //I'm disabling this for now and just
           //declaring the service via constant. The failing
           //auto discover is a big performance hit
           //especially since it can't work behind the 
           //firewall
           //try
           //{
           //   service.AutodiscoverUrl(eMailAddress, redirectionUrlValidationCallback);
           //}
           //catch
           //{
           //   service.Url = new Uri(Constant.QMS_EXCHANGE_SVC);
           //}

           service.Url = new Uri(Constant.QMS_EXCHANGE_SVC);

           /* 01/6/2017 Enable below to allow for impersonation on
            * on the exchange server. This is pending an exchange
            * adminstrator granting access for the service accounts.            
            */
           //if(ui.Email != null)
           //   service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress, ui.Email);

           //cache the service
           PerformanceHelper.AddMailService(service);


           return service;
        }

When calling the exchange server API, wrap in an impersonation block.

     public bool SendReply(string messageId, string response){
     UserInfoHelper ui = new UserInfoHelper();

     using(HostingEnvironment.Impersonate())
     {

        try
        {

           DC = new PrincipalContext(ContextType.Domain, Constant.QMS_DOMAIN);
           GP = GroupPrincipal.FindByIdentity(DC, IdentityType.Name, "Domain Users");
           ImpersonationContext = ((System.Security.Principal.WindowsIdentity)System.Threading.Thread.CurrentPrincipal.Identity).Impersonate();

           //do stuff
        }
        catch(Exception e)
        {
           ErrorHelper.AddErrorMsg("SendReply: " + e);
        }
     }

     return false;
  }

Lastly, and this is a important, the client browsers have to be set up for delegation on the client machines. In Chrome you need to edit the registry. In FF you need to set up the uris keys in about:config. Out of the box behavior will fail with kerberos. See this for more info (https://answers.laserfiche.com/questions/50123/Does-single-sign-on-or-authentication-negotiation-not-work-on-Chrome-in-Weblink and https://dev.chromium.org/administrators/policy-list-3#AuthNegotiateDelegateWhitelist).

Registry Modification for Chrome

Below was my original post.

I have been troubleshooting the same thing for a long time. Take a look at this (Unable to authenticate to ASP.NET Web Api service with HttpClient). I have modified the Aspnet.config file to no avail. From what I have read so far, it looks like System.Net.HttpWebRequest.GetResponse() is on a new thread and the impersonated credentials aren't transferred to the new object. I have set IIS authentication provider to negotiate:kerberos to make sure that delegation should/can happen.

should I create an Alternate Service Account instead of using my account?

Depending on your configuration i.e. IIS is sitting behind load balancers and a firewall, you might need a custom service account e.g. mydomain\my_service_account which will need a SPN http/my.webserver.com. However, I have this set up and it hasn't been the secret sauce that makes this work. I'm starting to think that the only way this can work is by setting up impersonation on an exchange account (https://msdn.microsoft.com/en-us/library/office/dn722376(v=exchg.150).aspx). Under this plan you would call something like below when you setup the service. If the service account on IIS doesn't have the right to impersonate on the Exchange server this won't work either. Also you need to pass the identity of the service account not that of the impersonated user.

if(UserInfo.Email != null)
      service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress, UserInfo.Email);

I hope that someone can provide a much better answer than this. Below is what I'm currently seeing in my trace.

9: <Trace Tag="EwsResponseHttpHeaders" Tid="38" Time="2017-01-20 20:59:29Z"> HTTP/1.1 401 Unauthorized Server: Microsoft-IIS/7.5 WWW-Authenticate: Negotiate,NTLM X-Powered-By: ASP.NET Date: Fri, 20 Jan 2017 20:59:29 GMT Content-Length: 0

</Trace>

10: GetUnreadMail ee: Microsoft.Exchange.WebServices.Data.ServiceRequestException: The request failed. The remote server returned an error: (401) Unauthorized. ---> System.Net.WebException: The remote server returned an error: (401) Unauthorized. at System.Net.HttpWebRequest.GetResponse() at Microsoft.Exchange.WebServices.Data.EwsHttpWebRequest.Microsoft.Exchange.WebServices.Data.IEwsHttpWebRequest.GetResponse() at Microsoft.Exchange.WebServices.Data.ServiceRequestBase.GetEwsHttpWebResponse(IEwsHttpWebRequest request) --- End of inner exception stack trace --- at Microsoft.Exchange.WebServices.Data.ServiceRequestBase.GetEwsHttpWebResponse(IEwsHttpWebRequest request) at Microsoft.Exchange.WebServices.Data.ServiceRequestBase.ValidateAndEmitRequest(IEwsHttpWebRequest& request) at Microsoft.Exchange.WebServices.Data.MultiResponseServiceRequest`1.Execute() at Microsoft.Exchange.WebServices.Data.ExchangeService.FindItems(FolderId parentFolderId, SearchFilter searchFilter, ViewBase view) at Microsoft.Exchange.WebServices.Data.ExchangeService.FindItems(WellKnownFolderName parentFolderName, SearchFilter searchFilter, ViewBase view) at QDoc.Helpers.ExchangeHelper.GetUnreadMail(ExchangeParam ExchangeQueryParam, MailItemList ExchangeResults, String myDomain) in c:\Inet\QDoc\QDoc\Helpers\ExchangeHelper.cs:line 186