Monday, September 14, 2015

Authenticate to Office 365 and SharePoint Online option #3 - REST using Azure AD from C#

When working on SharePoint Online as a new developer, there is always some confusion how to connect and authenticate to the SharePoint site. I’ve summarized two ACL options based implementation in previous blogs. Both implementations are using users’ O365 use name and password to communicate with SharePoint online.


The limitation of ACS approach is you are using end user’s credential that is configured to have limited access to restricted systems. It would be difficult leveraging multiple services secured by Azure AD for some use case like background bulk process. This blog will demonstrate the C# code to leverage Azure AD making app-only calls into SharePoint Online. This provides a more secure way of performing background operations against Office 365 services. In this blog you would need to have a certificate deployed to Azure AD application for the authenticate, I’ll have a different approach in future blog to leverage Azure AD without using certificate.

In order to leverage Azure AD to connect to SharePoint Online, you could need to create a Azure AD application. You could follow the steps Richard diZerega’s blog on the details. I’m using the procedure I’m using in the session “Application registration in Azure AD” from Office 365 Management APIs starting guide.


Here is the code sample to use the Azure AD application that is based on RicharddiZerega’s blog. 

using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Newtonsoft.Json;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace RESTAzureToken
{
    class Program
    {
        private static string CLIENT_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";   // Azure AD application client ID. It should be a ~35 character string
        private static string PRIVATE_KEY_PASSWORD = "MyCertPassword";  // I’ll have different example in the future to eliminate the password
        static void Main(string[] args)
        {
            doStuffInOffice365().Wait();
        }

        private async static Task doStuffInOffice365()
        {
            //set the authentication context
            string authority = "https://login.windows.net/mycompany.onmicrosoft.com/";
            AuthenticationContext authenticationContext = new AuthenticationContext(authority, false);

            //read the certificate private key from the executing location
            var certPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
            certPath = certPath.Substring(0, certPath.LastIndexOf('\\')) + "\\QCMAPICert.pfx"; // The Cert you uploaded to Azure AD application
            var certfile = System.IO.File.OpenRead(certPath);
            var certificateBytes = new byte[certfile.Length];
            certfile.Read(certificateBytes, 0, (int)certfile.Length);
            var cert = new X509Certificate2(
                certificateBytes,
                PRIVATE_KEY_PASSWORD,
                X509KeyStorageFlags.Exportable |
                X509KeyStorageFlags.MachineKeySet |
                X509KeyStorageFlags.PersistKeySet);
            ClientAssertionCertificate cac = new ClientAssertionCertificate(CLIENT_ID, cert);

            //get the access token to SharePoint using the ClientAssertionCertificate
            Console.WriteLine("Getting app-only access token to SharePoint Online");
            var authenticationResult = await authenticationContext.AcquireTokenAsync("https:// mycompany.sharepoint.com/", cac); // Do not use the site "https:// mycompany.sharepoint.com/sites/MyTestSite"
            var token = authenticationResult.AccessToken;
            Console.WriteLine("App-only access token retreived");

            //perform a post using the app-only access token to add SharePoint list item in Attendee list
            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
            client.DefaultRequestHeaders.Add("Accept", "application/json;odata=verbose");

            const string siteURL = "https://mycompany.sharepoint.com/sites/MyTestSite";

            // Get list from title
            const string listAction = siteURL + "/_api/web/lists/getbytitle('TestList')";
            HttpResponseMessage listResult = await client.GetAsync(listAction).ConfigureAwait(false);
            listResult.EnsureSuccessStatusCode();
            string listJsonData = await listResult.Content.ReadAsStringAsync();
            Console.WriteLine(listJsonData);

            // Get all items from list
            const string itemAction = siteURL + "/_api/web/lists/getbytitle('TestList')/items";
            HttpResponseMessage response1 = await client.GetAsync(itemAction).ConfigureAwait(false);
            response1.EnsureSuccessStatusCode();
            string itemJsonData = await response1.Content.ReadAsStringAsync();
            Console.WriteLine(itemJsonData);

        }
    }
}

The result of the items isin json format and here is the screenshot of the data from json viewer.



If you are new to O365 development, you should be aware of the following configurations in order to avoid some common errors.

1. Do not use the user friendly Microsoft new O365 URL like “mycompany.sharepoint.com”. Use the https://login.windows.net/mycompany.onmicrosoft.com/ as authority string. Otherwise you will get the error below.

Tracing:TraceError: "9/2/2015 11:57:02 PM: 542878bc-c518-4339-be97-41fe5f6ed523 - <RunAsync>d__0: Microsoft.IdentityModel.Clients.ActiveDirectory.AdalServiceException: AADSTS90002: No service namespace named mycompany.sharepoint.com' was found in the data store.
Trace ID: ca2c8bba-655a-4b01-89b3-23367373c274
Correlation ID: 542878bc-c518-4339-be97-41fe5f6ed523
Timestamp: 2015-09-02 23:57:01Z ---> System.Net.WebException: The remote server returned an error: (400) Bad Request.

2. Use the O365 tennant entry point URL instead of the site to acquire token.
var authenticationResult = await authenticationContext.AcquireTokenAsync("https://qualcomm.sharepoint.com/", cac);

If you pass the site URL 'https://mycompany.sharepoint.com/sites/SPDEV/', you will get the error below.

Exception:Caught: "AADSTS50001: Resource 'https://mycompany.sharepoint.com/sites/SPDEV/' is not registered for the account.
Trace ID: 702ef6fd-c156-4d6c-99b6-f5bb18eae6c8
Correlation ID: 0b25a990-c74c-4244-934a-90d138f66b40
Timestamp: 2015-09-02 16:41:16Z" (Microsoft.IdentityModel.Clients.ActiveDirectory.AdalServiceException)
A Microsoft.IdentityModel.Clients.ActiveDirectory.AdalServiceException was caught: "AADSTS50001: Resource 'https://qualcomm.sharepoint.com/sites/SPDEV/' is not registered for the account.
Trace ID: 702ef6fd-c156-4d6c-99b6-f5bb18eae6c8
Correlation ID: 0b25a990-c74c-4244-934a-90d138f66b40
Timestamp: 2015-09-02 16:41:16Z"
Time: 9/2/2015 9:41:16 AM
Thread:Worker Thread[10072]

3. When you are generating the X.509 certificate, make sure the key length is at least 2048. Shorter key lengths are not accepted as valid keys.

4. Manifest cannot not be re-uploaded unless you set variable $base64Value to null. If you change the certs, I'm not sure how to replace it to the exiating Azure AD application. There might be a powershell to do that but I've not find it.

5. The following packages you would need to ad to the references.
  • Active Directory Application Library
  • Json.NET package
  • System.Net.Http package

6. Be aware of the O365 MFA configuration and you might need to run this app inside your company firewall as I explained in previous blog.

1 comment: