This post will show how to call an O365 Exchange Online API from a SharePoint provider-hosted app. The code for this post is available at https://github.com/kaevans/spapp-exchange.
Background
This post is part of a series on building a SharePoint app that communicate with services protected by Azure AD.
- Part 1 - An Architecture for SharePoint Apps That Call Other Services
- Part 2 - Using OpenID Connect with SharePoint Apps
- Part 3 – Call O365 Exchange Online API from a SharePoint App (this post)
In my post, An Architecture for SharePoint Apps That Call Other Services, I proposed an architecture to enable SharePoint provider-hosted apps to access additional services. I showed that the access token used for SharePoint apps is not reusable for other services and accessing additional services requires a second access token. In my post, Using OpenID Connect with SharePoint Apps, I showed how to authenticate a user against the same Azure AD tenant that O365 uses, providing a seamless sign-on experience for the user. This post shows the next step, accessing a service that is protected by Azure AD. For this post, we will show to call the O365 Exchange Online API.
The key to this is that we have at least two access tokens: one issued by Azure ACS used with my SharePoint app, and one issued by Azure AD for the O365 Exchange Online API.
In reality, we will use 4 different access tokens in this post.
This post will be similar to the walkthrough Integrate Office 365 APIs into .NET Visual Studio projects, with a difference that we are using a SharePoint provider-hosted app that requires the SystemWebCookieManager implementation.
Authenticate with OpenID Connect
The first step is to create a new SharePoint provider-hosted app and authenticate the user with OpenID Connect. I showed the steps for this in the post Using OpenID Connect with SharePoint Apps. The high-level steps are:
- Create the new provider-hosted app
- Add NuGet packages
- Add OWIN startup class
- Implement the SystemWebCookieManager class
- Add the OWIN middleware
- Register the application with Azure AD
- Update web.config
Again, the steps are written (with screen shots) at Using OpenID Connect with SharePoint Apps. Once you are done, your web.config will look something like:
Your project structure will look like:
Reference O365 Exchange API
Now that you’ve added OpenID Connect authentication to your SharePoint app, the next step is to reference the O365 Exchange Online API. The tooling in Visual Studio makes this pretty easy. Just right-click the web project and choose Add Connected Service.
The next screen prompts you to sign in. Sign into the same O365 tenant that you are using for development of your provider-hosted app.
You are then prompted if you want to add the following redirect URLs. Click Yes.
Click App Properties.
Since you’ve already registered the application with Azure AD, the tooling picks it up (based on the client ID) and shows the properties. You do not need the HTTP endpoint for the app, only the HTTPS endpoint. You can safely delete the HTTP endpoint, leaving only the HTTPS endpoint.
Note that you could also make this application available to multiple organizations instead of a single organization. This would enable a scenario where the application is going to be published to the Store and your application is servicing thousands of tenants.
Click Apply. Next, select the Mail service, then click Permissions.
Our application is going to read the currently logged on user’s email. Check Read users’ mail.
In case you are interested what just happened, some NuGet packages were added to your web application.
Additionally, if you go to the application in Azure Active Directory, you can see that the reply URL was configured according to the values we set in the App Properties window, but more importantly the app was granted permission to the Office 365 Exchange Online API.
Another valuable thing to point out is that a new page should be visible in your browser, Integrate Office 365 APIs into .NET Visual Studio projects. We will use portions of that post for our solution.
Add a Token Cache
As explained in my previous posting, Call Multiple Services With One Login Prompt Using ADAL, Azure AD implements a multiple-resource refresh token that allows you to call multiple services without prompting the user for each service. When using the Azure AD Authentication Library (ADAL) with a client application, it is able to cache the token locally. When using with a web application, you should implement the cache yourself. There is an open-source implementation of an ADALTokenCache and its accompanying ApplicationDbContext to use with Entity Framework available on GitHub.
Add the Entity Framework NuGet package and the ADAL NuGet package.
Install-Package EntityFramework Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory
Add a connection string for a database that will store the tokens.
- <connectionStrings>
- <addname="DefaultConnection"
- connectionString="Data Source=(LocalDB)\v11.0;AttachDbFilename=|DataDirectory|\ADALTokenCacheDb.mdf;Integrated Security=True"
- providerName="System.Data.SqlClient" />
- </connectionStrings>
Copy the code for the ADALTokenCache and ApplicationDbContext to your Models directory.
Authenticate with OpenID Connect and Obtain an Access Token
When we authenticate with OpenID Connect, we receive a code. From that code, we can obtain an access token to the Azure AD Graph API and store the token in our cache. To call the Azure AD Graph API, we have to obtain a client secret for our Azure AD application. Go to the management portal and add a new key for the application.
After you hit Save, the key will be displayed. Copy that value into an appSetting in web.config named “ida:AppKey”. Also add the appSetting for GraphResourceID with the value “https://graph.windows.net”.
- <appSettings>
- <addkey="webpages:Version"value="3.0.0.0" />
- <addkey="webpages:Enabled"value="false" />
- <addkey="ClientValidationEnabled"value="true" />
- <addkey="UnobtrusiveJavaScriptEnabled"value="true" />
- <!-- SharePoint OAuth -->
- <addkey="ClientId"value="" />
- <addkey="ClientSecret"value="mPnwb/0rRRILiDLMDJtCGdywy/qMYRXneJ9AEurIeBA=" />
- <!-- Azure AD OAuth -->
- <addkey="ida:ClientID"value="77d50962-3a4d-461e-976b-cacad345a11c" />
- <addkey="ida:AppKey"value="YOUR_APPKEY_HERE (example mPnwb/0rRRILiDLMDJtCGdywy/qMYRXneJ9AEurIeBA=)"/>
- <addkey="ida:AADInstance"value="https://login.windows.net/{0}" />
- <addkey="ida:Tenant"value="kirke3.onmicrosoft.com" />
- <addkey="ida:GraphResourceID"value="https://graph.windows.net"/>
- </appSettings>
Update the ConfigureAuth method in the Startup.Auth.cs file.
- using ExchangeDemoWeb.Models;
- using ExchangeDemoWeb.Utils;
- using Microsoft.IdentityModel.Clients.ActiveDirectory;
- using Microsoft.Owin.Security;
- using Microsoft.Owin.Security.Cookies;
- using Microsoft.Owin.Security.OpenIdConnect;
- using Owin;
- using System;
- using System.Configuration;
- using System.Globalization;
- using System.IdentityModel.Claims;
- using System.Threading.Tasks;
- using System.Web;
- namespace ExchangeDemoWeb
- {
- publicpartialclassStartup
- {
- publicvoid ConfigureAuth(IAppBuilder app)
- {
- app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
- app.UseCookieAuthentication(newCookieAuthenticationOptions
- {
- //Implement our own cookie manager to work around the infinite
- //redirect loop issue
- CookieManager = newSystemWebCookieManager()
- });
- string clientID = ConfigurationManager.AppSettings["ida:ClientID"];
- string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
- string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
- string clientSecret = ConfigurationManager.AppSettings["ida:AppKey"];
- string graphResourceID = ConfigurationManager.AppSettings["ida:GraphResourceID"];
- string authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
- app.UseOpenIdConnectAuthentication(
- newOpenIdConnectAuthenticationOptions
- {
- ClientId = clientID,
- Authority = authority,
- Notifications = newOpenIdConnectAuthenticationNotifications()
- {
- // when an auth code is received...
- AuthorizationCodeReceived = (context) =>
- {
- // get the OpenID Connect code passed from Azure AD on successful auth
- string code = context.Code;
- // create the app credentials & get reference to the user
- ClientCredential creds = newClientCredential(clientID, clientSecret);
- string signInUserId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
- // use the OpenID Connect code to obtain access token & refresh token...
- // save those in a persistent store...
- AuthenticationContext authContext = newAuthenticationContext(authority, newADALTokenCache(signInUserId));
- // obtain access token for the AzureAD graph
- Uri redirectUri = newUri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
- AuthenticationResult authResult = authContext.AcquireTokenByAuthorizationCode(code, redirectUri, creds, graphResourceID);
- // successful auth
- returnTask.FromResult(0);
- },
- AuthenticationFailed = (context) =>
- {
- context.HandleResponse();
- returnTask.FromResult(0);
- }
- }
- });
- }
- }
- }
Finally, I add an Authorize attribute to the About method that Visual Studio generated in the HomeController.
You are at a point that you can hit F5 and test the application so far. If everything goes as expected, you should be able to set a breakpoint in the Startup.Auth.cs file and see the access token is returned, and you can go into the SQL Database and validate you see an entry for the user.
Finally…FINALLY… Call Exchange API
There has been quite a bit of digital ink spilled up to this point, but what we have is all the plumbing that makes this solution possible in addition to a firmer explanation of all the moving pieces. Rather than just dump a solution on you, you hopefully understand why all the parts are needed.
That said… we can finally call the Exchange API.
Add a new class to the Models folder named “MyMessage”.
- namespace ExchangeDemoWeb.Models
- {
- publicclassMyMessage
- {
- publicstring Subject { get; set; }
- publicstring From { get; set; }
- }
- }
Right-click the Controllers folder and choose Add / Controller. Choose MVC 5 Controller – Empty.
Name it MailController.
Update the code for the MailController. Notice that the Index action is now asynchronous, and we’ve applied the [Authorize] attribute to the class, ensuring the user is logged on prior to accessing the action. We are using the Discovery service to discover the user’s mailbox information without hard-coding a reference to the organization in any way. This allows us to build this solution in a multi-tenant fashion.
- using ExchangeDemoWeb.Models;
- using Microsoft.IdentityModel.Clients.ActiveDirectory;
- using Microsoft.Office365.Discovery;
- using Microsoft.Office365.OutlookServices;
- using System;
- using System.Collections.Generic;
- using System.Configuration;
- using System.Globalization;
- using System.Security.Claims;
- using System.Threading.Tasks;
- using System.Web.Mvc;
- namespace ExchangeDemoWeb.Controllers
- {
- [Authorize]
- publicclassMailController : Controller
- {
- // GET: Mail
- publicasyncTask<ActionResult> Index()
- {
- string clientID = ConfigurationManager.AppSettings["ida:ClientID"];
- string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
- string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
- string clientSecret = ConfigurationManager.AppSettings["ida:AppKey"];
- string graphResourceID = ConfigurationManager.AppSettings["ida:GraphResourceID"];
- string discoveryResourceID = "https://api.office.com/discovery/";
- string discoveryServiceEndpointUri = "https://api.office.com/discovery/v1.0/me/";
- string authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
- List<MyMessage> myMessages = newList<MyMessage>();
- var signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
- var userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
- //Create an authentication context from cache
- AuthenticationContext authContext = newAuthenticationContext(authority, newADALTokenCache(signInUserId));
- try
- {
- DiscoveryClient discClient = newDiscoveryClient(newUri(discoveryServiceEndpointUri),
- async () =>
- {
- //Get an access token to the discovery service
- var authResult = await authContext.AcquireTokenSilentAsync(discoveryResourceID, newClientCredential(clientID, clientSecret), newUserIdentifier(userObjectId, UserIdentifierType.UniqueId));
- return authResult.AccessToken;
- });
- var dcr = await discClient.DiscoverCapabilityAsync("Mail");
- OutlookServicesClient exClient = newOutlookServicesClient(dcr.ServiceEndpointUri,
- async () =>
- {
- //Get an access token to the Messages
- var authResult = await authContext.AcquireTokenSilentAsync(dcr.ServiceResourceId, newClientCredential(clientID, clientSecret), newUserIdentifier(userObjectId, UserIdentifierType.UniqueId));
- return authResult.AccessToken;
- });
- var messagesResult = await exClient.Me.Messages.ExecuteAsync();
- do
- {
- var messages = messagesResult.CurrentPage;
- foreach (var message in messages)
- {
- myMessages.Add(newMyMessage
- {
- Subject = message.Subject,
- From = message.Sender.EmailAddress.Address
- });
- }
- messagesResult = await messagesResult.GetNextPageAsync();
- } while (messagesResult != null);
- }
- catch (AdalException exception)
- {
- //handle token acquisition failure
- if (exception.ErrorCode == AdalError.FailedToAcquireTokenSilently)
- {
- authContext.TokenCache.Clear();
- //handle token acquisition failure
- }
- }
- return View(myMessages);
- }
- }
- }
Now we need a way to view the data. Right-click the Mail folder under the Views folder and choose “Add/View”. The new view is named “Index”, the template is “List”, and the model class is our “MyMessage” class.
We also need to add a link to our Mail controller in the navigation for the app. Go to the Views/Shared/_Layout.cshtml file and update the nav bar.
- <divclass="navbar-collapse collapse">
- <ulclass="nav navbar-nav">
- <li>@Html.ActionLink("Home", "Index", "Home")</li>
- <li>@Html.ActionLink("About", "About", "Home")</li>
- <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
- <li>@Html.ActionLink("My Messages", "Index", "Mail")</li>
- </ul>
- </div>
Testing… Testing… Is This Thing On?
The big payoff… hit F5. You are prompted to sign into O365 because this is a SharePoint provider hosted app.
Trust the SharePoint app.
We are now looking at our SharePoint provider-hosted app, the same one you’ve been using for awhile now.
Click the “My Messages” link in the navbar. Notice there is a redirect, but the user is not asked for additional credentials. And voila! We have successfully called the Exchange mail API on behalf of the current user.
I can verify that the emails shown in the screen match the emails in my inbox.
And just for completeness… we can copy the URL to the “My Messages” link, including all the SPHostUrl stuff in the querystring, and open an in-private browsing session. Paste the URL. This time you are prompted to sign into Azure AD, not O365.
The messages are properly displayed.
And I can click the “Home” link, which goes to the typical SharePoint provider-hosted app stuff that I am accustomed to.
Summary
This post showed how to create a SharePoint provider-hosted app that authenticates against Azure AD using OpenID Connect and then accesses the currently logged in user’s mailbox to display messages. The key to this was using the same Azure AD directory that our Exchange API uses. This provides a seamless sign-on experience for the user.
The code for this post is available at https://github.com/kaevans/spapp-exchange.
For More Information
An Architecture for SharePoint Apps That Call Other Services
Using OpenID Connect with SharePoint Apps
Integrate Office 365 APIs into .NET Visual Studio projects
https://github.com/kaevans/spapp-exchange