MVP Article – Working with Application Permissions (App-Only Auth) in SharePoint Online and the Microsoft Graph

SharePoint team

When working with SharePoint Online or the Microsoft Graph, there are many scenarios in which we need to read or write data without a user context. It might be a scheduled process, or it might be an operation that requires elevated permissions. In such scenarios, it is quite common for the solution to use “Application permissions” (a.k.a. App-Only Authentication). This lets the solution have its own identity which can be used to grant the required permissions.

When working with Application permissions in Office 365, there are a lot of moving pieces to deal with like Client Ids, Client Secrets, Azure AD App Registrations, Certificates, Add-In Registrations, AppRegNew.aspx, AppInv.aspx etc.

What I want to do in this post is to explore different options for configuring and granting application permissions. There are a few combinations possible with the different moving pieces. My aim in this post is to explore them and determine which combination might be suitable for certain scenarios. We will also see some sample code which demonstrates how to authenticate with SPO and the Microsoft Graph using the different authentication options.

Here is a table I have put together which summarises the different options for working with applications permissions in SPO and the Microsoft Graph API. We will go through each on them in detail.

Interact with data from SharePoint Online with an Azure AD App Registration

If your solution uses an Azure AD App Registration created from the Azure AD portal and you want to read or write data to SharePoint Online: You will need to use a Client Id and Certificate. Have a look at this link for details on how to create an AAD App Registration as well as the certificate: https://docs.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azuread.

If you try to use a Client Id and Client Secret created through AAD portal you will get the following error: Microsoft.SharePoint.Client.ServerUnauthorizedAccessException: ‘Access denied. You do not have permission to perform this action or access this resource.’

You will also get the “Access Denied” error if you try to write to the User Profile service. Reading from the User Profile service will work. If your solution needs write User Profile service access, your only option would be to use an Add-In registration (see the next section). Writing to the SPO Taxonomy Service will not work either through AAD App Registration or Add-In Registration. Read operations will work. See notes at the end of this post.

Here is some sample code to demo how to use a Client Id and Certificate with the AAD App Registration. You will need the SharePointPnPCoreOnline NuGet package:

using Microsoft.SharePoint.Client;
using OfficeDevPnP.Core;
using System;
using System.Security.Cryptography.X509Certificates;

namespace AppPermissionsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            string siteUrl = "https://yourtenant.sharepoint.com/sites/yoursite/";
            string clientId = "<client-id-of-aad-app-registration>"; //e.g. 01e54f9a-81bc-4dee-b15d-e661ae13f382
            string tenantDomain = "yourtenant.onmicrosoft.com";
            
            //Demo values
            string certStoreName = "My";
            string certStoreLocation = "CurrentUser";
            string certThumprint = "<your-cert-thumbprint>"; // e.g. CE20E000D53A4C968ED8BA3EFC92C40A2692AE98

            var authManager = new AuthenticationManager();
            X509Certificate2 appOnlyCert = GetAppOnlyCertificate(certStoreName, certStoreLocation, certThumprint);

            //PnP Core method. You can also use other overloads of this method to access the certificate e.g. using the cert password
            ClientContext clientContext = authManager.GetAzureADAppOnlyAuthenticatedContext(siteUrl, clientId, tenantDomain, appOnlyCert);

            Web web = clientContext.Web;
            clientContext.Load(web);
            clientContext.ExecuteQueryRetry();

            Console.WriteLine(web.Title);
        }

        private static X509Certificate2 GetAppOnlyCertificate(string certStoreName, string certStoreLocation, string certThumprint)
        {

            X509Certificate2 appOnlyCertificate = null;

            StoreName storeName;
            StoreLocation storeLocation;

            Enum.TryParse(certStoreName, out storeName);
            Enum.TryParse(certStoreLocation, out storeLocation);

            X509Store certStore = new X509Store(storeName, storeLocation);
            certStore.Open(OpenFlags.ReadOnly);

            X509Certificate2Collection certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, certThumprint, false);

            // Get the first cert with the thumbprint
            if (certCollection.Count > 0)
            {
                appOnlyCertificate = certCollection[0];
            }

            certStore.Close();
            return appOnlyCertificate;
        }
    }
}

Interact with data from SharePoint Online with a SharePoint Add-In Registration:

If your solution uses a SharePoint Add-In Registration (created through the /_layouts/15/AppRegNew.aspx page) and you want to read/write data to SharePoint Online:

You will need a Client Id and Client Secret created through the /_layouts/15/AppRegNew.aspx page and permissions granted from the /_layouts/15/AppInv.aspx page

See this link for details on how to create as well as assign permissions to the Add-In Registration: https://docs.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azureacs

Here is a sample of how to use the Add-In registration to interact with data from SharePoint. You will need the SharePointPnPCoreOnline NuGet package:

using Microsoft.SharePoint.Client;
using OfficeDevPnP.Core;
using System;

namespace AppPermissionsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            string siteUrl = "https://yourtenant.sharepoint.com/sites/yoursite/";
            
            //Client id and Client secret created through appregnew.aspx and permissions granted through appinv.aspx
            string clientId = "<client-id>"; //e.g. 15107f17-5230-422b-873c-b3846211cba7
            string clientSecret = "<client-secret>"; //e.g. XeGMHUxRPOg0o1LeKqfWVYTzO0blGfXBPKvNiCQwHtc=

            var authManager = new AuthenticationManager();

            //PnP Core method
            ClientContext clientContext = authManager.GetAppOnlyAuthenticatedContext(siteUrl, clientId, clientSecret);

            Web web = clientContext.Web;
            clientContext.Load(web);
            clientContext.ExecuteQueryRetry();

            Console.WriteLine(web.Title);
        }

    }
}

Interact with data from the Microsoft Graph with an Azure AD App Registration

If your solution needs to interact with the Microsoft Graph, the only option is to have an Azure AD App Registration. However, within the Azure AD App Registration you can either use a Client Id, Client Secret pair or you can use the Client Id, Certificate pair as well.

Using a Client Id and Certificate:

The process to create the AAD App Registration and Certificate is the same as described above in the first chapter. The only difference would be that instead of selecting SharePoint Online permissions, the App Registration will have to be granted the relevant permission to the Microsoft Graph.

Once that is done, here is the sample code to use the Client Id and Certificate to get data from the Microsoft Graph:

using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;

namespace AppPermissionsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            string clientId = "<client-id-of-aad-app-registration>"; //e.g. 01e54f9a-81bc-4dee-b15d-e661ae13f382
            string tenantDomain = "yourtenant.onmicrosoft.com";
            
            //Demo values
            string certStoreName = "My";
            string certStoreLocation = "CurrentUser";
            string certThumprint = "<your-cert-thumbprint>"; // e.g. CE20E000D53A4C968ED8BA3EFC92C40A2692AE98

            //Client Id of App created through the Azure AD App Registration portal. Cert uploaded in the portal as well.
            X509Certificate2 appOnlyCert = GetAppOnlyCertificate(certStoreName, certStoreLocation, certThumprint);
            string accessToken = GetMSGraphApplicationAccessToken(clientId, tenantDomain, appOnlyCert);

            HttpClient httpClient = new HttpClient();
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            //Sample request to get all groups. The AAD App registration will need at least Group.Read.All application permission granted.
            var request = new HttpRequestMessage(HttpMethod.Get, $"https://graph.microsoft.com/v1.0/groups");
            var response = httpClient.SendAsync(request).Result;
            var content = response.Content.ReadAsStringAsync().Result;

            Console.WriteLine(content);
        }

        public static string GetMSGraphApplicationAccessToken(string clientId, string tenantDomain, X509Certificate2 cert)
        {
            string authority = string.Format(CultureInfo.InvariantCulture, "{0}/{1}/", "https://login.windows.net", tenantDomain);
            var authContext = new AuthenticationContext(authority);
            var clientAssertionCert = new ClientAssertionCertificate(clientId, cert);
            var result = authContext.AcquireTokenAsync("https://graph.microsoft.com", clientAssertionCert).Result;

            var accessToken = result.AccessToken;
            return accessToken;
        }

        private static X509Certificate2 GetAppOnlyCertificate(string certStoreName, string certStoreLocation, string certThumprint)
        {
            X509Certificate2 appOnlyCertificate = null;

            StoreName storeName;
            StoreLocation storeLocation;

            Enum.TryParse(certStoreName, out storeName);
            Enum.TryParse(certStoreLocation, out storeLocation);

            X509Store certStore = new X509Store(storeName, storeLocation);
            certStore.Open(OpenFlags.ReadOnly);

            X509Certificate2Collection certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, certThumprint, false);

            // Get the first cert with the thumbprint
            if (certCollection.Count > 0)
            {
                appOnlyCertificate = certCollection[0];
            }

            certStore.Close();
            return appOnlyCertificate;
        }
    }
}

Using a Client Id and Client Secret:

The only change in this approach is using a Client Secret (Password) instead of a certificate. See this link to see how to generate a client secret for the AAD App Registration: https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#get-application-id-and-authentication-key

Once you have the Client Id and Client Secret, you can use the sample code to get data from the Microsoft Graph:

using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;

namespace AppPermissionsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            string clientId = "<client-id-of-aad-app-registration>"; //e.g. 01e54f9a-81bc-4dee-b15d-e661ae13f382
            string clientSecret = "<client-secret-of-aad-app-registration>"; //e.g @FgQR{Q4I0.+^4v+/k{!.]&I/+j
            string tenantDomain = "yourtenant.onmicrosoft.com";

            string accessToken = GetMSGraphApplicationAccessToken(clientId, clientSecret, tenantDomain);

            HttpClient httpClient = new HttpClient();
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            //Sample request to get all groups. The AAD App registration will need at least Group.Read.All application permission granted.
            var request = new HttpRequestMessage(HttpMethod.Get, $"https://graph.microsoft.com/v1.0/groups");
            var response = httpClient.SendAsync(request).Result;
            var content = response.Content.ReadAsStringAsync().Result;

            Console.WriteLine(content);
        }

        public static string GetMSGraphApplicationAccessToken(string clientId, string clientSecret, string tenantDomain)
        {
            string authority = string.Format(CultureInfo.InvariantCulture, "{0}/{1}/", "https://login.windows.net", tenantDomain);
            var authContext = new AuthenticationContext(authority);

            var clientCredential = new ClientCredential(clientId, clientSecret);
            var result = authContext.AcquireTokenAsync("https://graph.microsoft.com", clientCredential).Result;

            var accessToken = result.AccessToken;
            return accessToken;
        }

    }
}

In conclusion:

Considering all factors, I would personally go with one of these two options:

#1 – If the solution is strictly going to deal with SharePoint Online data and not any other part of Office 365, you might want to consider the SharePoint Add-In Registration approach with a Client Id and Client Secret. That way you don’t have to mess around with certificates. But remember that in the future if the same solution is going to read/write data from the Microsoft Graph, you might have to create another App Registration in Azure AD.

#2 – Another option would be to use an Azure AD App Registration with a Client Id and a Certificate. This allows us to interact with most of Office 365 data (including SharePoint Online and the Microsoft Graph) without maintaining separate applications. The caveats to this approach being the added complication of generating and managing certificates and also the fact that writing data to SharePoint Online Taxonomy and User Profile will not work (Reading data will be possible)

Notes:

  1. Writing to the SPO Taxonomy Service with Application Permissions does not work from either AAD Portal or Add-In Registration. Read operations work. See more details here: https://docs.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly#what-are-the-limitations-when-using-app-only
  2. For the purpose of this post, I have only considered Azure AD v1.0 endpoint as we are only concerned with organisational accounts and not personal accounts: https://docs.microsoft.com/en-gb/azure/active-directory/develop/azure-ad-endpoint-comparison
  3. Technically, Add-In registrations created from the AppRegNew.aspx page are also registered in Azure AD. They are not visible through the AAD portal, but you can list them via PowerShell.
  4. It is also possible to create an App Registration in Azure AD and then use the AppInv.aspx page in SharePoint Online to assign it SharePoint specific permissions. You can also use this approach to assign a client secret which never expires to the Add-In registration. For more details on this, you can see this post by the very talented Sergei Sergeevhttps://spblog.net/post/2018/08/24/SharePoint-lifehacks-create-SharePoint-app-registration-with-client-secret-which-never-expires

Hope you’ve found the post helpful!

About the Author

Vardhaman is an Office Development MVP and works as a Technical Architect at Content and Code, London. His primary areas of focus being SharePoint, Office 365, Microsoft Graph and Azure. He has been passionate about technology since a very early age. In his opinion, the fast-paced nature at which Enterprise Software is moving is what makes it so thrilling and exciting.

He is an active participant in the SharePoint Patterns and Practices (PnP) initiative where he has spoken on various topics in the community calls. In his spare time, he is an avid gamer and likes to travel to various European cities. He maintains a blog at https://www.vrdmn.com. You can follow him on twitter https://twitter.com/vrdmn and find him on GitHub at https://github.com/vman

 

What is the MVP article series?

We have opened up our SharePoint developer blog also for the Microsoft MVPs who would like to share their articles around SharePoint development topics also through this channel. All articles written by MVPs are clearly indicated by using the “MVP article” prefix in the title of the blog post. If you are an MVP and would like to get some additional exposure, please send your suggestions around the article to spdevblogcommunity@microsoft.com email address and we can start a discussion on the suggestion and possible publishing schedule.

You can absolutely also do cross-posting of the article through your own blog, but we would prefer the material which is submitted as a suggestion, to be fresh and new, rather than something which has been released months ago. We will use both SharePoint and OfficeDev social media channels to promote these articles, so it’s a nice way to get you additional exposure for the community around the work you do.

 


SharePoint Team, Microsoft – 10th of January 2019

Feedback usabilla icon