Wednesday, August 9, 2017

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

Lately I've been working on a PowerShell module for MSDYN365 which is meant to simplify administration and mundane tasks like approving mailboxes and adding document locations. As part of this effort I wanted to support MFA authentication as many of the functions I'm creating requires tenant admin, and tenant admins (should) always have MFA enabled for their logins.

Usually when you build an app that authenticates against microsoftonline you need to add an Azure AD application, and use the id and key in your app to enable users to log in. Alternatively you can ask the user for credentials and do the authentication in the background, but this might be difficult in cases where MFA is enabled. Luckily for me, I'm writing PowerShell modules, which means the code is running in the users' context. That means I have access to any credentials or cookies that are added, and I can reuse them without having to register anything in Azure AD.
The reason why I want to avoid AAD registration is because I want to make this easy to use and as portable as possible. Downloading a PS-module and then having to modify it with AAD tenantid, app id and app key is not very user friendly, even if you only have to do it once.

Luckily for me, the guys working on the SharePoint-PnP framework had already done a lot of work on getting authorization working by capturing the cookies after a log on session in a browser, and reusing that for their own purpose. By borrowing their code and making some very minor adjustments to it I got authentication working with redirect for MSDYN365 Online, and I put it into a little helper class named WebAuthentication.
What this code does is that it loads wininit.dll to prevent using any existing authenticated sessions.
In addition, it sets the persistcookie option to false, which means that none of the cookies collected will be persisted.

[System.Runtime.InteropServices.DllImport("wininet.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)]
public static extern bool InternetSetOption(int hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength);

private static unsafe void SuppressWininetBehavior()
    /* SOURCE:
        *      A general purpose option that is used to suppress behaviors on a process-wide basis. 
        *      The lpBuffer parameter of the function must be a pointer to a DWORD containing the specific behavior to suppress. 
        *      This option cannot be queried with InternetQueryOption. 
        *      Suppresses the persistence of cookies, even if the server has specified them as persistent.
        *      Version:  Requires Internet Explorer 8.0 or later.

    int option = (int)3/* INTERNET_SUPPRESS_COOKIE_PERSIST*/;
    int* optionPtr = &option;

    bool success = InternetSetOption(0, 81/*INTERNET_OPTION_SUPPRESS_BEHAVIOR*/, new IntPtr(optionPtr), sizeof(int));
    if (!success)
        MessageBox.Show("Something went wrong");

To use the code simply call the static GetAuthenticatedCookies method, which takes the crm-server url as an input, and an authentication type. In the Dynamics365-PoSh project I've hard-coded it to use o365, which means it will collect cookies from microsoftonline instead of using claims authentication with ADFS.

var webAuthCookies = WebAuthentication.GetAuthenticatedCookies(ServerUrl, Models.AuthenticationType.O365);

What happens next is that a windows forms window will pop up and navigate to the URL given. This will redirect to, and ask you for your credentials like you are used to. After you've logged in you will get the redirect to the MSDYN365 home page. When this happens "Navigated" event triggers, and then the "ClaimsWebBrowser_Navigated" method will close the form as the authentication is ended.
What we're left with is a cookiecollection which allows us to send authenticated web requests to MSDYN365

In the next post we'll explore how to use this to instantiate an IOrganizationService connection to MSDYN365 using the cookies collected.

No comments:

Post a Comment