Showing posts with label MFA. Show all posts
Showing posts with label MFA. Show all posts

Thursday, August 24, 2017

Piggybacking on MSDYN365 PluginRegistrationTools ADAL implementation (plagiarizing Mikael Svenson)

My brilliant colleague, Mikael Svenson, wrote a cool blog post on piggybacking on the SharePoint Online Management Shell ADAL application
Inspired by this (relatively) dirty hack I tried to figure out which applications Microsoft has given to us that may support OAuth OOTB.
Turns out, the PluginRegistrationTool does!
I fired up the PluginRegistrationTool from the SDK, hooked fiddler on to it and hit the "create new connection" button in the tool.
Checked the query string in the initial authorize request and Voila!

Splitting up this query string we get the following two values:
client_id=2ad88395-b77d-4561-9441-d0e40824f9bc
redirect_uri=app%3A%2F%2F5d3e90d6-aa8e-48a8-8f2c-58b45cc67315%2F

Cleaning up the redirect_uri gives us this nice app id redirect uri we can use:
app://5d3e90d6-aa8e-48a8-8f2c-58b45cc67315/


This allows us to piggyback on Microsoft's own app registration, which doesn't require approval for users when you distribute an application. Example taken from my blog series on using the new admin api in PowerShell

using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

namespace MSDYN365AdminApiAndMore.Helpers
{
    public class AuthenticationHelper
    {
        private static string _clientId = "2ad88395-b77d-4561-9441-d0e40824f9bc";
        private static string _redirectUrl = "app://5d3e90d6-aa8e-48a8-8f2c-58b45cc67315/";

        private Uri _endpoint = null;
        private string _resource = null;
        private string _authority = null;
        private AuthenticationContext _authContext = null;
        private AuthenticationResult _authResult = null;

        public AuthenticationHelper(Uri endpoint)
        {
            _endpoint = endpoint;
        }

        public string Authority
        {
            get
            {
                if (_authority == null)
                {
                    DiscoverAuthority(_endpoint);
                }
                return _authority;
            }
        }

        public AuthenticationContext AuthContext
        {
            get
            {
                if (_authContext == null)
                {
                    _authContext = new AuthenticationContext(Authority, false);
                }
                return _authContext;
            }
        }

        public AuthenticationResult AuthResult
        {
            get
            {
                Authorize();
                return _authResult;
            }
        }

        public HttpMessageHandler Handler
        {
            get
            {
                return new OAuthMessageHandler(this, new HttpClientHandler());
            }
        }

        private void DiscoverAuthority(Uri discoveryUrl)
        {
            try
            {
                Task.Run(async () =>
                {
                    var ap = await AuthenticationParameters.CreateFromResourceUrlAsync(discoveryUrl);
                    _resource = ap.Resource;
                    _authority = ap.Authority;
                }).Wait();
            }
            catch (Exception e)
            {
                throw e;
            }
        }

        private void Authorize()
        {
            if (_authResult == null || _authResult.ExpiresOn.AddMinutes(-30) < DateTime.Now)
            {
                Task.Run(async () =>
                {
                    _authResult = await AuthContext.AcquireTokenAsync(_resource, _clientId, new Uri(_redirectUrl),
                    new PlatformParameters(PromptBehavior.Always));
                }).Wait();
            }
        }

        class OAuthMessageHandler : DelegatingHandler
        {
            AuthenticationHelper _auth = null;
            public OAuthMessageHandler(AuthenticationHelper auth, HttpMessageHandler innerHandler) : base(innerHandler)
            {
                _auth = auth;
            }
            protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
            {
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _auth.AuthResult.AccessToken);
                return base.SendAsync(request, cancellationToken);
            }
        }
    }
}

Final thoughts:

Should you use this? No, probably not. They might change it at any time, or they could introduce connection string inputs for the tool which requires you to register your own app.
Will I be using this? Great question!

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.

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[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: http://msdn.microsoft.com/en-us/library/windows/desktop/aa385328%28v=vs.85%29.aspx
        * INTERNET_OPTION_SUPPRESS_BEHAVIOR (81):
        *      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. 
        *      
        * INTERNET_SUPPRESS_COOKIE_PERSIST (3):
        *      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.

1
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 login.microsoftonline.com, 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.