Searching with the Office 365 APIs

Today, Microsoft quietly launched the “search” permission scope in Azure AD Applications. This is huge step in delivering new scenarios with the Office 365 APIs that leverage SharePoint search and the Office Graph. In this post, I’ll illustrate how to leverage search with the Office 365 APIs and use it to query site collections and modern groups a user has access to (a difficult/impossible task with the Office 365 APIs until today). The video below illustrates some of the key components of the post.

[View:https://www.youtube.com/watch?v=LD6SYmMmIVA]

Background

Apps for SharePoint and Office are considered “contextual” since they are launched from the platform they extend (often with contextual detail such as the host site URL). In contrast, applications built with the Office 365 APIs can stand on their own and connect INTO Office 365. Because these modern applications lack contextual launch parameters, the Office 365 APIs use a Discovery Service to get information on the resources available to the user/tenant. The Discovery Service provides high-level resource details such as Mail, Contacts, Calendar and OneDrive. It does not provide detail on every site collection a user has access to (a frequent ask by developers). The new search permission scope for Azure AD Applications can be used to query more granular details.

Adding the Search Permission

Configuring an application with permissions to Office 365 is usually accomplished through the “Add Connect Service” wizard of Visual Studio. Behind the scenes, this wizard registers an application in Azure AD and configures its “permission to other applications”. The ability for Azure AD to manage permissions to multiple apps/services is often referred to as “common consent”. Search is a brand new permission we can add through common consent. At the time of this post, the Visual Studio tooling does not surface this new permission scope. However, it can easily be added by going to the “Configure” tab of the application in Azure AD.

Manually Adding Search Permission in Azure

Performing Search in Code

The Office 365 APIs provide a comprehensive set of libraries for authentication, discovery, and performing operations against resources in Office 365. Although strongly-typed APIs exist for many operations, search is brand new to the Office 365 APIs so no strongly-typed functions yet exists. That doesn’t mean we can’t use the Office 365 APIs to get an access token that we can use with the REST endpoints for SharePoint search. To do this, we need a resource-specific access token. I was able to perform a successful search using the “MyFiles” resource. I suspect the “MyFiles” service resource URI is specialized for file access only. However, the Discovery Service should have a new “RootSite” resource that will work. The “RootSite” represents the root site collection in SharePoint Online (https://tenant.sharepoint.com) and not to be confused with the OneDrive root site or “My Site Host” (https://tenant-my.sharepoint.com).

Using Discovery Service to get "RootSite" Access Token

private async Task<string> getAccessToken() {    // fetch from stuff user claims    var signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;    var userObjectId = ClaimsPrincipal.Current.FindFirst(SettingsHelper.ClaimTypeObjectIdentifier).Value;

    // setup app info for AuthenticationContext    var clientCredential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.ClientSecret);    var userIdentifier = new UserIdentifier(userObjectId, UserIdentifierType.UniqueId);

    // create auth context (note: no token cache leveraged)    authContext = new AuthenticationContext(SettingsHelper.AzureADAuthority);

    // create O365 discovery client     DiscoveryClient discovery = new DiscoveryClient(new Uri(SettingsHelper.O365DiscoveryServiceEndpoint),        async () => {            var authResult = await authContext.AcquireTokenSilentAsync(SettingsHelper.O365DiscoveryResourceId, clientCredential, userIdentifier);            return authResult.AccessToken;    });

    // query discovery service for endpoint for 'RootSite' endpoint    dcr = await discovery.DiscoverCapabilityAsync("RootSite");

    // get access token for RootSite    return authContext.AcquireToken(dcr.ServiceResourceId, clientCredential, new UserAssertion(userObjectId, UserIdentifierType.UniqueId.ToString())).AccessToken;}

 

With our resource-specific access token, we can add it as a bearer token in the authorization header of our REST calls. The code below illustrates the full REST call and the parsing of JSON into strongly-typed classes.

Performing REST Search Query and Parsing Results

private static async Task<List<SearchResult>> getSearchResults(string query){    List<SearchResult> results = new List<SearchResult>();    SearchModel model = new SearchModel();    var token = await model.getAccessToken();    HttpClient client = new HttpClient();    client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);    client.DefaultRequestHeaders.Add("Accept", "application/json; odata=verbose");    using (HttpResponseMessage response = await client.GetAsync(new Uri(model.dcr.ServiceEndpointUri.ToString() + query, UriKind.Absolute)))    {        if (response.IsSuccessStatusCode)        {            XElement root = Json2Xml(await response.Content.ReadAsStringAsync());            var items = root.Descendants("RelevantResults").Elements("Table").Elements("Rows").Elements("results").Elements("item");            foreach (var item in items)            {                //loop through the properties returned for this item                var newItem = new SearchResult();                foreach (var prop in item.Descendants("item"))                {                    if (prop.Element("Key").Value == "Title")                        newItem.Title = prop.Element("Value").Value;                    else if (prop.Element("Key").Value == "Path")                        newItem.Path = prop.Element("Value").Value;                    else if (prop.Element("Key").Value == "SiteLogo")                        newItem.SiteLogo = prop.Element("Value").Value;                }                                        //only return site collections in primary domain...not the onedrive or public domains                //this would probably be better placed in the original search query                if (newItem.Path.ToLower().Contains(model.dcr.ServiceResourceId.ToLower()))                    results.Add(newItem);            }        }    }

    return results;}

 

The getSearchResults method takes a search string in REST format. The sample provides two simple examples…one that queries all site collections for the user (contentclass:sts_site) and one that queries all modern groups for a user (contentclass:sts_site WebTemplate:GROUP). It is generic enough to take just about any search string formatted for REST, but these scenarios have specific interest to me and my customers. Notice that all search string start with /search. This is because the RootSite resource endpoint URI ends with _api.

Passing in Queries

public static async Task<List<SearchResult>> GetSiteCollections(){    return await getSearchResults("/search/query?querytext='contentclass:sts_site'&trimduplicates=true&rowlimit=50&SelectProperties='WebTemplate,Title,Path,SiteLogo'");}

public static async Task<List<SearchResult>> GetModernGroups(){    return await getSearchResults("/search/query?querytext='contentclass:sts_site WebTemplate:GROUP'&trimduplicates=true&rowlimit=50&SelectProperties='WebTemplate,Title,Path,SiteLogo'");}

 

Final Thoughts

Microsoft is really doubling down on the Office 365 APIs and the incorporating the search permission is a great example. I’m really excited to see what additional permissions 2015 will bring. You can download the sample MVC app used in the post and video HERE.