Quantcast
Channel: analogies – ThreeWill
Viewing all articles
Browse latest Browse all 20

Building an iOS App to access Office 365 and SharePoint – Part 3

0
0

Overview

This blog post outlines the steps needed to utilize the SharePoint Search REST API within an iOS application using the ADALiOS library and a custom ASP.NET Web API 2.0 project. It is the final post in a series of three blog posts revolving around creating a native iOS application that connects to O365 and SharePoint Online. If you followed along from the first blog post, then you should have a project that builds successfully with the ADALiOS library added and authenticates a user to access their content from Office 365 and SharePoint Online (previous installments can be found here and here).

The Problem with OAuth and the SharePoint Search REST API,

If there is one big issue with the SharePoint Search REST API it is that it is disabled for anonymous users by default. The problem rears its head when trying to use the OAuth 2.0 access token given to your application by the ADALiOS library to authenticate to the Search REST API. There are many examples of why you would want to use the search API within your native application. For example, we needed to search and find all of the webs and sites available to the current user for our application. There are ways to enable anonymous search for your site collections, but if you are trying to build a multi-tenant application, this may not be possible to accomplish. Luckily, there is a way to solve this issue without enabling anonymous search: creating a custom ASP.NET Web API to allow User Impersonation to the SharePoint Search REST API.

A Custom ASP.NET Web API 2.0 to allow User Impersonation

To start, you need to create a new Web API project from within Visual Studio 2013. The API needs to be configured for User Impersonation within Azure Active Directory. For complete details on creating the application and configuring it correctly, refer to Kirk Evan’s blog post on calling Office 365 APIs from your Web API on behalf of a user. You can follow this blog post all the way through if you just want to run his example; however, if you want to follow the SharePoint search example then stop once you have installed the Active Directory Authentication Library in the “Using ADAL” section of his post. I will only quickly recap these steps here:

  • Click your Tenant directory in the Azure Portal
  • Click Applications,then click your “Native client application” entry
  • Click Configure in the menu bar (top of page)
  • Download the manifest and add the appPermissions
appPermissions

appPermissions

You can now create a new class within the API; I called mine SharepointOnlineSearch. This is the class that will take the access token provided by the native application and create a user impersonation access token to Office 365 from it within the Web API. It will also be used to call the SharePoint Search Web API and get the list of all webs and sites for the tenant.

using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Xml.Linq; 

namespace TWSharepointAppWebApi.Models
{
    public class SharepointOnlineSearch
    {
        /// <summary>
        /// Get the access token
        /// </summary>
        /// <param name="clientId">Client ID of the Web API app</param>
        /// <param name="appKey">Client secret for the Web API app</param>
        /// <param name="aadInstance">The login URL for AAD</param>
        /// <param name="tenant">Your tenant (eg kirke.onmicrosoft.com)</param>
        /// <param name="resource">The resource being accessed</param>
        /// <returns>string containing the access token</returns>
        public static async Task<string> GetAccessToken(
            string clientId,
            string appKey,
            string aadInstance,
            string tenant,
            string resource)
        {
            string accessToken = null;
            AuthenticationResult result = null; 

            System.Diagnostics.Trace.WriteLine("Getting access token..."); 

            ClientCredential clientCred = new ClientCredential(clientId, appKey);
            string authHeader = HttpContext.Current.Request.Headers["Authorization"]; 

            System.Diagnostics.Trace.WriteLine("Client Cred : " + clientCred.ClientId); 

            System.Diagnostics.Trace.WriteLine("Auth header : " + authHeader); 

            string userAccessToken = authHeader.Substring(authHeader.LastIndexOf(' ')).Trim();
            UserAssertion userAssertion = new UserAssertion(userAccessToken); 

            System.Diagnostics.Trace.WriteLine("User Assertion : " + userAssertion.Assertion); 

            string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant); 

            System.Diagnostics.Trace.WriteLine("Authority : " + authority); 

            AuthenticationContext authContext = new AuthenticationContext(authority); 

            System.Diagnostics.Trace.WriteLine("Auth Context : " + authContext.Authority);
            try {
                result = await authContext.AcquireTokenAsync(resource, userAssertion, clientCred);
                accessToken = result.AccessToken;
            }
            catch (Exception e) {
                System.Diagnostics.Trace.WriteLine("Error : " + e.Message);
            } 

            System.Diagnostics.Trace.WriteLine("Got access token..."); 

            return accessToken;
        } 

        /// <summary>
        /// Get all sites
        /// </summary>
        /// <param name="siteURL">The URL of the root SharePoint site</param>
        /// <param name="accessToken">The access token</param>
        /// <returns>Http response from SharePoint</returns>
        public static async Task<HttpResponseMessage> GetAllSites(
            string siteURL,
            string accessToken)
        {
            //
            // Call the O365 API and retrieve the user's profile.
            //
            string requestUrl = siteURL + "_api/search/query?querytext='path:" + siteURL + "*'&refinementfilters='contentclass:or(STS_Web,STS_Site)'&rowLimit=100&trimDuplicates=false"; 

            System.Diagnostics.Trace.WriteLine("Building API request...");
            System.Diagnostics.Trace.WriteLine("Request Url : " + requestUrl); 

            HttpClient client = new HttpClient();
            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl); 

            System.Diagnostics.Trace.WriteLine("Request : " + request.RequestUri.AbsolutePath); 

            request.Headers.Add("Accept", "application/json; odata=verbose"); 

            System.Diagnostics.Trace.WriteLine("First header added..."); 

            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); 

            System.Diagnostics.Trace.WriteLine("Second header added..."); 

            try
            {
                HttpResponseMessage response = await client.SendAsync(request); 

                if (response.IsSuccessStatusCode)
                {
                    //string responseString = await response.Content.ReadAsStringAsync(); 

                    System.Diagnostics.Trace.WriteLine("API Request successful"); 

                    return response;
                }
            }
            catch (Exception e)
            {
                System.Diagnostics.Trace.WriteLine("Error : " + e.Message);
            } 

            // An unexpected error occurred calling the O365 API.  Return a null value.
            return (null);
        }
    }
}

Now from within the ValuesController, we can utilize our newly created class within the GET method to return the data from the SharePoint Search REST API call.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using TWSharepointAppWebApi.Filters;
using System.Configuration;
using System.Threading.Tasks; 

namespace TWSharepointAppWebApi.Controllers
{
    [Authorize]
    [ImpersonationScopeFilter]
    public class ValuesController : ApiController
    {
        // GET api/values
        public async Task<HttpResponseMessage> Get()
        {
            string clientID = ConfigurationManager.AppSettings["ida:ClientID"];
            string appKey = ConfigurationManager.AppSettings["ida:AppKey"]; 

            string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
            string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
            string resource = ConfigurationManager.AppSettings["ida:Resource"]; 

            string accessToken = await Models.SharepointOnlineSearch.GetAccessToken(
                clientID,
                appKey,
                aadInstance,
                tenant,
                resource);
            var ret = await Models.SharepointOnlineSearch.GetAllSites (
                resource,
                accessToken); 

            System.Diagnostics.Trace.WriteLine("Ret : "  + ret.Content.ToString());
            return ret;
        } 

        // POST api/values
        public void Post([FromBody]string value)
        {
        } 

        // PUT api/values/5
        public void Put(int id, [FromBody]string value)
        {
        } 

        // DELETE api/values/5
        public void Delete(int id)
        {
        }
    }
}

I went ahead and appended the tenant to the end of the “ida:AADInstance” key in the web.config file instead of leaving it as a formatted string as Kirk Evans does in his example. If you choose not to do this, then you will need to add a little logic to create the full AADInstance value from the current value and the ida:Tenant value. With this code, you should be able to run locally with no issues. If you try accessing the values API with a GET request, an unauthorized message should be returned because you do not have the OAuth 2.0 access token.

You can now give your native application permissions to access your custom web API from within the configuration screen on Azure, but at this point, it will only be hitting the localhost API.

image 1 localhost

 

Publishing to Azure “Gotchas”

At this point, you should have a working API locally. But what you really want is a published API on Azure that can be accessed from your iOS application easily. Here are a few “gotchas” you may run into when publishing your web API to Azure.

  1. Make sure you check Enable Organizational Authentication from within the publishing “Settings” menu within Visual Studio. When this is selected, Visual Studio will prompt you to sign in to your tenant. This will also allow Visual Studio to create the new application in Azure Active Directory that points to the published Azure Website instead of localhost.
  2. Make sure you also copy the APP ID URI from the VS created Active Directory application that points to the published Azure Website into the web.config file as the new Audience for the Web API. Then you need to re-publish the app to have the new Audience URL. This should be in the form of “https://<tenant>.onmicrosoft.com/WebApp-XXXXXX.azurewebsites.net”.
    AppIdUri

    AppIdUri

  3. In the VS created Active Directory application, you need to go through the same steps to add the User Impersonation permissions as you did with the application pointed to local host. This means downloading the app manifest, adding the impersonation code, and uploading it back to Azure.

Using the Multi-Resource Refresh Token

Now that you have a published custom Web API that connects to Office 365 and SharePoint Online with User Impersonation, you need to give your native client application permissions to access the API on Azure. From the management portal, select your native client applications and add your newly created permission from the configuration tab.

AzureApiPermissions

AzureApiPermissions

Given that the native client application has the correct permissions, you should be able to authenticate to the API and access the SharePoint Search results.

If you have followed from the previous blog post, then your application is already handling authentication to Office 365 and SharePoint Online. With your newly created application permissions, once you have authenticated to one resource, you now receive a “multi-resource refresh token.” This refresh token can be used to refresh an access token to each of your application’s resources and is cached as the refresh token for each source by the ADALiOS library. Thus, to access the newly created custom web API, you can simply call the aquireTokenWithResource: method with the API “Audience” URL as the resource.

NSString* path = [[NSBundle mainBundle] pathForResource:@"ADALInfo" ofType:@"plist"]; 

    NSDictionary* ADAL = [NSDictionary dictionaryWithContentsOfFile:path];
    ADAL = [NSDictionary dictionaryWithDictionary:[ADAL objectForKey:@"AAD Instance"]]; 

    ADAuthenticationContext* authContext;
    ADAuthenticationError *error;
    authContext = [ADAuthenticationContext authenticationContextWithAuthority:[ADAL objectForKey:@"Authority"] error:&error]; 

    NSURL *redirectUri = [NSURL URLWithString:[ADAL objectForKey:@"RedirectUri"]];
        [authContext acquireTokenWithResource:[ADAL objectForKey:@"ApiResource"]
                                 clientId:[ADAL objectForKey:@"ClientId"]
                              redirectUri:redirectUri
                           promptBehavior:AD_PROMPT_NEVER
                                   userId:@""
                     extraQueryParameters:@""
                          completionBlock:^(ADAuthenticationResult *result) {
                              if (AD_SUCCEEDED != result.status){
                                  // display error on the screen
                                  NSLog(@"%@",result.error.errorDetails);
                              }
                              else {
                                  NSLog(@"%@",result.accessToken);
                              }
                          }];

This code will authenticate the application to the web API without any user action, provided that there is already a multi-resource refresh token cached (which there should be if this authentication is called after the authentication to Office 365 and SharePoint Online). You can set the promptBehavior attribute to AD_PROMPT_NEVER so that the user will never be prompted even if the authentication fails because there is no refresh token if this authentication is happening at a point in the application where it does not make sense to allow the login screen to pop up. If this does happen, an appropriate error will be returned explaining that the authentication failed because there was no refresh token present and AD_PROMPT_NEVER was set. Again, this code assumes that the API “Audience” URL that was set in the web.config file is stored in the .plist file of the iOS application under the “ApiResource” key.

With the application authenticated to the web API, it can now access the API search results through the use of an NSURLConnection with the authorization header set to the access token. I added “SearchAPIUrl” as another key in the .plist file in the iOS application with its value set to the URL of my custom web API.

NSString* path = [[NSBundle mainBundle] pathForResource:@"ADALInfo" ofType:@"plist"]; 

    NSDictionary* ADAL = [NSDictionary dictionaryWithContentsOfFile:path];
    ADAL = [NSDictionary dictionaryWithDictionary:[ADAL objectForKey:@"AAD Instance"]]; 

    NSURL *apiUrl = [NSURL URLWithString:[[ADAL objectForKey:@"SearchAPIUrl"] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; 

    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:apiUrl]; 

    ADAuthenticationError* error; 

    self.context = [ADAuthenticationContext authenticationContextWithAuthority:[ADAL objectForKey:@"Authority"] error:&error]; 

    ADTokenCacheStoreKey* key = [ADTokenCacheStoreKey keyWithAuthority:[ADAL objectForKey:@"Authority"] resource:[ADAL objectForKey:@"ApiResource"] clientId:[ADAL objectForKey:@"ClientId"] error:&error];
    id<ADTokenCacheStoring> cache = self.context.tokenCacheStore;
    ADTokenCacheStoreItem* item = [cache getItemWithKey:key userId:nil]; 

    NSString *authHeader = [NSString stringWithFormat:@"Bearer %@", item.accessToken]; 

    [request setValue:authHeader forHTTPHeaderField:@"Authorization"]; 

    [request setValue:@"application/json" forHTTPHeaderField:@"accept"]; 

    NSOperationQueue *queue = [[NSOperationQueue alloc]init]; 

    [NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { 

        if (error == nil){ 

            NSError *jsonError; 

            NSObject *object = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError]; 

            NSMutableArray *refreshResults = [[[[[[[object valueForKey:@"d"] objectForKey:@"query"] objectForKey:@"PrimaryQueryResult"] objectForKey:@"RelevantResults"] objectForKey:@"Table"] objectForKey:@"Rows"] objectForKey:@"results"]; 

            //any logic with the results goes here 

            dispatch_async(dispatch_get_main_queue(), ^{
               //any updates to the UI here
            }); 

        } 

        else {
            //may want to handle the error here
            dispatch_async(dispatch_get_main_queue(), ^{
               //any updates to the UI here
            });
        } 

    }];

Recap

At this point, you have a project that builds correctly using the ADALiOS library, authenticates the user to both SharePoint Online and a custom ASP.NET Web API 2.0, and can take advantage of both the SharePoint REST API and SharePoint Search REST API. You can now build native applications that seamlessly connect with SharePoint data using an OAuth 2.0 access token and refresh token.

The post Building an iOS App to access Office 365 and SharePoint – Part 3 appeared first on ThreeWill.


Viewing all articles
Browse latest Browse all 20

Latest Images

Trending Articles





Latest Images