Wednesday, August 9, 2017

Connect to Dynamics 365 using MFA without app tokens, part 2

In part 1 of this series I explained how to use my slightly modified version of the SharePoint-Sites-PnP webauthentication.
The complete code and updates to the powershell module can be found on my github repository

In this post I'm going to dive into getting cookie authentication to work with OrganizationWebProxyClient

First off, our goal for this is to get an IOrganizationService inherited connection that we can use to execute MSDYN365 requests. To do that we have to use one of the connection classes available which allows us to use cookies for authentication.
Unfortunately, the SDK does not contain any constructors which enables us to set cookies programmatically, so we have to look for alternative ways to get around that limitation.
After navigating through the most common connection alternatives I found that the OrganizationWebProxyClient inherits the WebProxyClient, which again inherits the System.ServiceModel.ClientBase class. This is our best way in because the ClientBase class exposes the endpoint used as a public property, and will limit the amount of "hacking" needed to inject our cookies into the request.
So to start with we simply instantiate a new OrganizationWebProxyClient using the organization service endpoint address.

1
var service = new OrganizationWebProxyClient(new Uri("https://myOrganization.crm4.dynamics.com/XRMServices/2011/Organization.svc/web"), false);

I added a break point to debug the connection created and look at the behaviors defined for the underlying endpoint. My hope was to find somewhere to inject my cookies and test the connection, but unfortunately there was nothing I could take advantage of


Yet another minor snafu in the quest for a simple solution. At this point I do what any decent developer should, and that is to google "endpointbehavior cookiecollection". As usual I found a lot of discussions, degrading comments and unrelated answers, but I also found this little brilliant snippet from Markus Wildgruber in a thread on StackOverflow
Adding this into a little helper class allowed me to inject my cookies into the service endpoint without any more issues. A few lines later and I'm sitting with this pretty little thing:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var webAuthCookies = WebAuthentication.GetAuthenticatedCookies("https://myOrganization.crm4.dynamics.com", Models.AuthenticationType.O365);
var service = new OrganizationWebProxyClient(new Uri("https://myOrganization.crm4.dynamics.com/XRMServices/2011/Organization.svc/web"), false);
var cookieContainer = new CookieContainer();
foreach (Cookie cookie in webAuthCookies)
{
    cookieContainer.Add(cookie);
}
var cookieBehavior = new CookieBehavior(cookieContainer);
service.Endpoint.EndpointBehaviors.Add(cookieBehavior);

service.Execute(new WhoAmIRequest());

Unfortunately, my joy was short-lived. Executing this request presented me with a 401 error saying the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
System.ServiceModel.Security.MessageSecurityException occurred
  HResult=0x80131501
  Message=The HTTP request is unauthorized with client authentication scheme 'Anonymous'. The authentication header received from the server was 'Bearer redirect_uri=https%3a%2f%2flogin.windows.net%2fcommon%2fwsfed, realm=Microsoft.CRM'.
  Source=mscorlib
  StackTrace:
   at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
   at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
   at Microsoft.Xrm.Sdk.IOrganizationService.Execute(OrganizationRequest request)
   at Microsoft.Xrm.Sdk.WebServiceClient.OrganizationWebProxyClient.<>c__DisplayClassd.<ExecuteCore>b__c()
   at Microsoft.Xrm.Sdk.WebServiceClient.WebProxyClient`1.ExecuteAction[TResult](Func`1 action)
   at Microsoft.Xrm.Sdk.WebServiceClient.OrganizationWebProxyClient.ExecuteCore(OrganizationRequest request)
   at Microsoft.Xrm.Sdk.WebServiceClient.OrganizationWebProxyClient.Execute(OrganizationRequest request)
   at <this line has been omitted on purpose>

Inner Exception 1:
WebException: The remote server returned an error: (401) Unauthorized.

I have to admit, I scratched my head for a long time, trying all kinds of silly things, before I remembered that I was dealing with web requests here, and that meant that the next natural step was to try and use fiddler.
I set an early break point, started up fiddler and hooked it to my process to try and see what was happening. I noticed that when sending the request the cookies seemed to be added to the header correctly, but I also noticed that there was a security header there that I didn't add myself.

This reminded me that when I checked the endpoint behaviors earlier there was one behavior for the client credentials. I fired up the debugger again and dived into the endpoint behaviors, and as I suspected there was a header there with no value since I was doing cookie injection instead of traditional authentication. I went into my cookie behavior and added this little snippet to get rid of the unnecessary header:

1
2
3
4
if (httpRequestMsg.Headers.AllKeys.Contains("Authorization"))
{
    httpRequestMsg.Headers.Remove("Authorization");
}

Firing up the debugger again, I set went to my break point and hooked up fiddler again. This time the Authorization header was missing, and to my pleasant surprise the WhoAmIRequest returned a 200 OK. Going into the results view I could see that I had successfully retrieved the userid, businessunitid and organizationid.

Finally! A working OrganizationWebProxyClient that I could use, and all of this without having to register the application with Azure AD.

Now I can finally go back to writing my Dynamics365-PoSh module without having to worry about authentication until version 9.1 comes out and removes the endpoint I used.

Finally, I would like to give a huge shout-out to Mikael Svenson. I would have spent considerably longer to figure out all of this if he hadn't let me bounce some ideas off him.

2 comments:

  1. Great article! Curious if you've checked out our module (on powershell gallery and on GitHub). https://GitHub.com/seanmcne/Microsoft.Xrm.Data.PowerShell/

    Would be happy to hear your feedback. Thanks!

    ReplyDelete
  2. Hi, Sean!
    I've taken a look into it now, and it looks pretty good.
    My focus is to simplify administration tasks for administrators who aren't familiar with Dynamics, which is something I often get requested. That means basically doing what you guys are making, but making the commandlets even easier to use by storing connections for reuse in sessions, pre-registered AAD application with multi-tenant support (with possibility to override for self-hosted), etc. I want to be able to do stuff like this:
    Get-XrmMailbox -Email "mail@domain.com" -Org "myorganization" -Location EMEA | Set-XrmMailbox -Approve;

    If that sounds like something you might want to do then maybe we should have a chat instead of making two similar wheels :)

    ReplyDelete