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.

Comments (3)

  1. David says:

    Not relevant to this post, but did not see a way to email or contact you.  I need some help to find code you wrote some time back for a help desk, it looked really awesome.

    There are a bunch of people in your youtube page that are interested, seems that things stalled in the release as a teched question had it going up in Aug of last year to code gallery.  I am in need of a ticketing system, we have SharePoint via 365 and would love to start with what you had in place.

    Can you point me to where to get more info on it?

    Thanks,

    David

  2. Peppe says:

    Hi richard, thanks for this post.

    I tried your code in my tenant but inside search results i had site collections in which the current user doesn't have permissions.

    So the question is: are search results trimmed by current user permissions?

  3. Mark Stokes says:

    I cannot get this working.

    I keep getting the following error:

    Failed to acquire token silently. Call method AcquireToken

    Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

    Exception Details: Microsoft.IdentityModel.Clients.ActiveDirectory.AdalSilentTokenAcquisitionException: Failed to acquire token silently. Call method AcquireToken

    Source Error:

    Line 38:             DiscoveryClient discovery = new DiscoveryClient(SettingsHelper.DiscoveryServiceEndpointUri,

    Line 39:                 async () => {

    Line 40:                     var authResult = await authContext.AcquireTokenSilentAsync(SettingsHelper.DiscoveryServiceResourceId, clientCredential, new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));

    Line 41:                     return authResult.AccessToken;

    Line 42:                 });

Skip to main content