Monday, August 21, 2017

A workaround to support switching logins between tenants in Windows apps or PowerShell – Piggyback the Microsoft SharePoint Online Management Shell ADAL application

image

If you write Windows Applications or PowerShell scripts to connect to multiple tenants you have probably experienced the “That didn’t work” dialog where the web based login tries to log you in with some other tenant credential as cookies are persisted from an Internet Explorer or Edge session. This was a typical problem for the SharePoint Query Tool, and I managed to add some funky code to ensure cookies were ignored when the login window popped up.

However, recently I’ve had a couple of issues where people have domain/aad joined pc’s with single sign-on to Office 365. With this in place cookies doesn’t matter. The login dialog will always try to log you in with your current user when using IE/Edge, which happens to be the browser control available when programming in Windows. Hence, using the query tool was hard to use in these scenarios.

At the moment the Query Tool uses web browser login and a FedAuth cookie for authorization. The obvious workaround is to use an ADAL app and a Bearer token instead. This is quite easy to implement, but having people register a custom ADAL app per tenant to support this login flow seemed too cumbersome. And for those who know me, I always try to find an easy way out, and a clever work around came to mind.

When using the good old SharePoint Online Management Shell, it prompt for credentials when you login.

image

This works fine as they have an ADAL application automatically registered in all tenants. What if we could just re-use and piggyback on this application? And you can! Monitoring some network traffic this application has a client id of

9bc3ab49-b65d-410a-85ad-de819febfddc

and the return URL of

https://oauth.spops.microsoft.com/

And this is all you need to get started. The permissions scopes for this app are listed as:

  • AllProfiles.Manage
  • Sites.FullControl.All
  • User.Read.All

..and it’s already admin consented for you, ripe for the picking.

Below is the code of a sample C# console application using the Microsoft.Identity.Clients.ActiveDirectory nuget package for ADAL login. Look out for an updated query tool using this shortly, and you should of course improve the code to try to acquire a token silently first etc. to re-use the refresh token, something the Query Tool code will have.

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

namespace O365LoginTest
{
    class Program
    {
        static void Main(string[] args)
        {
            RunAsync().Wait();
        }

        static async Task RunAsync()
        {
            // The O365 login URL
            string authorityUri = "https://login.windows.net/common/oauth2/authorize";

            // App id for Microsoft SharePoint Online Management Shell
            const string clientId = "9bc3ab49-b65d-410a-85ad-de819febfddc";
            
            // Replu URL for the Microsoft SharePoint Online Management Shell
            const string redirectUri = "https://oauth.spops.microsoft.com/";
            
            // The DOMAIN you want to do API calls against
            string resourceUri = "https://techmikael.sharepoint.com";

            IntPtr handle = Process.GetCurrentProcess().MainWindowHandle;

            var authCtx = new ADAL.AuthenticationContext(authorityUri);
            // Always prompt to allow login to more than one tenant
            var authParam = new ADAL.PlatformParameters(ADAL.PromptBehavior.Always, handle);
            var authenticationResult = await authCtx.AcquireTokenAsync(resourceUri, clientId, new Uri(redirectUri), authParam);

            // Sample search query using the returned Bearer token
            string queryUrl = resourceUri + "/_api/search/query?querytext='*'&clienttype='ContentSearchRegular'";
            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
            var result = await client.GetStringAsync(queryUrl);
            Console.WriteLine(result);
        }
    }
}