The API Economy: Consuming Our Web API from a Single Page App

This post shows how to consume a Web API secured with Azure Active Directory using ADAL.js.

Background

This post is part of a series on building a SharePoint app that communicate with services protected by Azure AD.

As originally proposed in the post An Architecture for SharePoint Apps That Call Other Services, changing our architecture to use a Web API affords new possibilities for our application to enable secure communication from multiple types of clients.  One client for the Web API was our SharePoint app.  Another client can now be a Single Page App written using Angular.  Realizing there’s a ton of geekery that I’ve written on this, I put a big red arrow in the architecture diagram to show “You Are Here”.

image

 

To achieve this awesomeness, we will leverage the ADAL.js library.  In fact, you’ll see we are now at the point that we don’t have to really write much code at all to make this work.

The code for this post is available at https://github.com/kaevans/spapp-webapi-exchange, including the SPA and Web API projects.

Create the Web Application

I could do this with any platform since I am just using HTML and JavaScript files.  I thought about doing this in Eclipse using a Java application, but decided to just stick with Visual Studio.  In Visual Studio, create a new ASP.NET Web Application project.

image

On the next page, choose Empty.  This will generate a project with no content.

image

Change the project’s “SSL Enabled” property to True.  Remember we only exchange OAuth tokens over SSL, let’s be safe out there people.

image

Register the SPA App with Azure AD

Go to the Azure Management Portal (https://manage.windowsazure.com), go to the Applications tab for your Azure AD tenant, and choose Add to add a new application.

image

When prompted, choose “Add an application my organization is developing”.

image

Provide a name for the new application, and set the type to “Web application and/or Web API”.

image

On the properties page, provide the sign-in URL for the application, which is the SSL URL that you obtained when you created the web application.

image

The pieces of information you primarily need are (1) the client ID and (2) the APP ID URI.  The reply URL (3) and the sign-in URL (not shown) need to be updated once you deploy your application.

image

Scroll to the bottom of the page in the permissions section and click Add application.

image

In the next screen, choose your custom Web API.  Note: See the previous post A Sample SharePoint App That Calls A Custom Web API for details on how the Web API is built and how it is registered with Azure AD.

image

Once you’ve added the application, choose the permission and click Save.

image

Applications in Azure AD do not use the OAuth2 implicit flow by default, you have to add that capability by editing the manifest.  Download the manifest.

image

Change the oauth2AllowImplicitFlow value to true.

Manifest

  1. "oauth2AllowImplicitFlow": true,

You can see where to make the change in this screen shot.

image

Save the file locally, then upload it to the management portal by choosing the “Upload manifest” button for the application.

image

Create the SPA

I am going to use the SinglePageApp-WebAPI-AngularJS-DotNet sample from the Azure AD team as a starting point.  If you want more details on creating single page apps with Angular.js, go through that sample as there is some good information to be discovered there.  First, I add an HTML page “index.html” and add the following markup.

index.html

  1. <!DOCTYPE html>
  2. <html xmlns="https://www.w3.org/1999/xhtml">
  3. <head>
  4.     <title>Mail SPA: a SPA sample demonstrating Azure AD and ADAL JS</title>
  5.     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
  6.     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
  7. </head>
  8. <body ng-app="mailApp" ng-controller="homeCtrl" role="document">
  9.  
  10.  
  11.     <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
  12.         <div class="container">
  13.             <div class="navbar-header">
  14.                 <button type="button" class="navbar-toggle collapsed"
  15.                         data-toggle="collapse"
  16.                         data-target=".navbar-collapse">
  17.  
  18.                     <span class="icon-bar"></span>
  19.                     <span class="icon-bar"></span>
  20.                 </button>
  21.                 <a class="navbar-brand" href="#/Home">ADAL JS Sample</a>
  22.             </div>
  23.             <div class="navbar-collapse collapse">
  24.                 <ul class="nav navbar-nav">
  25.                     <li ng-class="{ active: isActive('/Home') }"><a href="#/Home">Home</a></li>
  26.                     <li ng-class="{ active: isActive('/Mail') }"><a href="#/Mail">My Messages</a></li>
  27.                 </ul>
  28.             </div>
  29.         </div>
  30.     </div>
  31.     <br />
  32.     <div class="container" role="main">
  33.         <div class="row">
  34.             <div class="col-xs-10 col-xs-offset-1" style="background-color:azure">
  35.                 <div class="page-header">
  36.                     <h1>Exchange Demo SPA</h1>
  37.                 </div>
  38.                 <p>This sample demonstrates how to take advantage of ADAL JS for adding Azure AD authentication to your AngularJS apps.</p>
  39.             </div>
  40.         </div>
  41.         <div class="row">
  42.             <div class="col-xs-10 col-xs-offset-1">
  43.                 <div ng-view class="panel-body">
  44.  
  45.                 </div>
  46.             </div>
  47.         </div>
  48.         <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  49.         <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.25/angular.min.js"></script>
  50.         <script src="https://code.angularjs.org/1.2.25/angular-route.js"></script>
  51.         <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
  52.         <script src="App/Scripts/adal.js"></script>
  53.         <script src="App/Scripts/adal-angular.js"></script>
  54.         <script src="App/Scripts/app.js"></script>
  55.  
  56.         <script src="App/Scripts/homeCtrl.js"></script>
  57.         <script src="App/Scripts/mailCtrl.js"></script>
  58.         <script src="App/Scripts/mailSvc.js"></script>
  59. </body>
  60. </html>

Notice at the bottom of the page, we are referencing scripts in the App/Scripts folder.  I add a folder, “App”, and two subfolders “Scripts” and “Views”.

image

I copied adal-angular.js and adal.js from the SinglePageApp-WebAPI-AngularJS-DotNet sample into the Scripts folder.  I am not providing a code listing for those assets here. 

Let’s add two HTML pages to the Views folder.  The first, Home.html, is just used to let me know that the Angular routes are working. 

Home.html

  1. <div>
  2.     home sweet home
  3. </div>

The next HTML page will show our email messages.

Mail.html

  1. <div ng-init="populate()">
  2.     <p class="error">{{error}}</p>
  3.     <p>{{loadingMessage}}</p>
  4.             <div class="panel">
  5.         <table class="table table-striped">
  6.             <tbody>
  7.                 <tr data-ng-repeat="item in mailList">
  8.                     <td>
  9.                                                                         <p>{{item.From}}</p>
  10.                     </td>
  11.                     <td>
  12.                                                                         <p>{{item.Subject}}</p>
  13.                     </td>
  14.                 </tr>
  15.             </tbody>
  16.         </table>
  17.     </div>
  18. </div>

I add a JavaScript file called homeCtrl.js that will be the controller when on the home page.

homeCtrl.js

  1. 'use strict';
  2. angular.module('mailApp')
  3. .controller('homeCtrl', ['$scope', 'adalAuthenticationService','$location', function ($scope, adalService, $location) {
  4.     $scope.isActive = function (viewLocation) {        
  5.         return viewLocation === $location.path();
  6.     };
  7. }]);

Next I add an indexCtrl.js to handle any callbacks to the index page.

indexCtrl.js

  1. 'use strict';
  2. angular.module('mailApp')
  3. .controller('indexCtrl', ['$scope', 'adalAuthenticationService', function ($scope, adalService)
  4. {
  5.  
  6. }]);

Next we need a controller to display our email messages from our custom Web API. 

mailCtrl.js

  1. 'use strict';
  2. angular.module('mailApp')
  3. .controller('mailCtrl', ['$scope', '$location', 'mailSvc', 'adalAuthenticationService', function ($scope, $location, mailSvc, adalService)
  4. {
  5.     $scope.error = "";
  6.     $scope.loadingMessage = "Loading...";
  7.     $scope.mailList = null;
  8.  
  9.     $scope.populate = function ()
  10.     {
  11.         mailSvc.getItems().success(function (results)
  12.         {
  13.             $scope.mailList = results;
  14.             $scope.loadingMessage = "";
  15.         }).error(function (err)
  16.         {
  17.             $scope.error = err;
  18.             $scope.loadingMessage = "";
  19.         })
  20.     };
  21.  
  22. }]);

And finally the service that will call our custom API. 

mailSvc.js

  1. 'use strict';
  2. angular.module('mailApp')
  3. .factory('mailSvc', ['$http', function ($http)
  4. {
  5.  
  6.     //var apiEndpoint = "Enter the root location of your Mail API here, e.g. https://contosotogo.azurewebsites.net/";
  7.     var apiEndpoint = "https://localhost:44350/";
  8.  
  9.     $http.defaults.useXDomain = true;
  10.     delete $http.defaults.headers.common['X-Requested-With'];
  11.  
  12.     return {
  13.         getItems: function ()
  14.         {
  15.             return $http.get(apiEndpoint + 'api/mail');
  16.         }
  17.     };
  18. }]);

OK, that’s all our controllers and our one service.  Now we need to add the app.js that initializes everything.  Using the adal.js library and adal-angular.js implementation, we can simply provide the tenant and the clientId for our application.  The endpoints object lets us specify an external endpoints that will be called.  In this case, the Web API runs on a different port than our SPA application.  Additionally, we provide the APP ID URI for the Web API application registration in Azure AD (see the previous post A Sample SharePoint App That Calls A Custom Web API for details on how the Web API is built and how it is registered with Azure AD).

The App.js file uses the “requireADLogin” property on the Angular route provider to ensure that we are first authenticated before accessing the Mail view.

app.js

  1. 'use strict';
  2. angular.module('mailApp', ['ngRoute','AdalAngular'])
  3. .config(['$routeProvider', '$httpProvider', 'adalAuthenticationServiceProvider', function ($routeProvider, $httpProvider, adalProvider) {
  4.  
  5.     $routeProvider.when("/Home", {
  6.         controller: "homeCtrl",
  7.         templateUrl: "/App/Views/Home.html",    
  8.     }).when("/Mail", {
  9.         controller: "mailCtrl",
  10.         templateUrl: "/App/Views/Mail.html",
  11.         requireADLogin: true,    
  12.     }).otherwise({ redirectTo: "/Home" });
  13.  
  14.     var endpoints = {
  15.         
  16.         "https://localhost:44350/":
  17.         "https://kirke3.onmicrosoft.com/ExchangeDemoAPI",
  18.     };
  19.  
  20.     adalProvider.init(
  21.         {
  22.             tenant: 'kirke3.onmicrosoft.com',
  23.             clientId: 'c2b3f3bc-1b6a-4c17-8b89-b5e474dd3949',
  24.             extraQueryParameter: 'nux=1',
  25.             endpoints: endpoints,
  26.             cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.  
  27.             // Also, token acquisition for the To Go API will fail in IE when running on localhost, due to IE security restrictions.
  28.         },
  29.         $httpProvider
  30.         );
  31.    
  32. }]);

That’s all there is to our SPA application.  A few HTML files and some JavaScript and we are done.

Testing

To test the application, I changed the application startup projects to start just my Web API project and the SPA project.

image

I run the application, and it opens to the Home controller view.

image

Click the “My Messages” link in the navigation bar, and you are prompted to log in.  This is because the route provider has the property requireADLogin set to true.

image

Log in, and then we can see our email message.  This was a SPA application calling a custom Web API using OAuth, which then in turn calls the O365 Exchange Online API on behalf of the current user.

image

Summary

In case you didn’t see how incredibly cool that was, let me point it out as clearly as I can.  We just created an application that is purely HTML and JavaScript that enables the user to log on using Azure Active Directory, securely calls a custom Web API, and that Web API calls the O365 Exchange Online API on behalf of the current user to retrieve email messages. 

For More Information

Enabling Cross-Origin Requests in ASP.NET Web API 2

ADAL.js library

SinglePageApp-WebAPI-AngularJS-DotNet

OAuth Fiddler Extension – extension for Fiddler that enables you to inspect OAuth tokens.

https://github.com/kaevans/spapp-webapi-exchange – source code for this post, including the SPA and Web API projects