Call O365 Exchange Online API from a SharePoint App
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)
- Part 4 – A Sample SharePoint App That Calls a Custom Web API
- Part 5 - The API Economy: Consuming Our Web API from a Single Page App
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.
Token Cache Connetion String
- <connectionStrings>
- <add name="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 in Web.config
- <appSettings>
- <add key="webpages:Version" value="3.0.0.0" />
- <add key="webpages:Enabled" value="false" />
- <add key="ClientValidationEnabled" value="true" />
- <add key="UnobtrusiveJavaScriptEnabled" value="true" />
- <!-- SharePoint OAuth -->
- <add key="ClientId" value="" />
- <add key="ClientSecret" value="mPnwb/0rRRILiDLMDJtCGdywy/qMYRXneJ9AEurIeBA=" />
- <!-- Azure AD OAuth -->
- <add key="ida:ClientID" value="77d50962-3a4d-461e-976b-cacad345a11c" />
- <add key="ida:AppKey" value="YOUR_APPKEY_HERE (example mPnwb/0rRRILiDLMDJtCGdywy/qMYRXneJ9AEurIeBA=)"/>
- <add key="ida:AADInstance" value="https://login.windows.net/{0}" />
- <add key="ida:Tenant" value="kirke3.onmicrosoft.com" />
- <add key="ida:GraphResourceID" value="https://graph.windows.net"/>
- </appSettings>
Update the ConfigureAuth method in the Startup.Auth.cs file.
Startup.Auth.cs
- 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
- {
- public partial class Startup
- {
- public void ConfigureAuth(IAppBuilder app)
- {
- app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
- app.UseCookieAuthentication(new CookieAuthenticationOptions
- {
- //Implement our own cookie manager to work around the infinite
- //redirect loop issue
- CookieManager = new SystemWebCookieManager()
- });
- 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(
- new OpenIdConnectAuthenticationOptions
- {
- ClientId = clientID,
- Authority = authority,
- Notifications = new OpenIdConnectAuthenticationNotifications()
- {
- // 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 = new ClientCredential(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 = new AuthenticationContext(authority, new ADALTokenCache(signInUserId));
- // obtain access token for the AzureAD graph
- Uri redirectUri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
- AuthenticationResult authResult = authContext.AcquireTokenByAuthorizationCode(code, redirectUri, creds, graphResourceID);
- // successful auth
- return Task.FromResult(0);
- },
- AuthenticationFailed = (context) =>
- {
- context.HandleResponse();
- return Task.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”.
MyMessage.cs
- namespace ExchangeDemoWeb.Models
- {
- public class MyMessage
- {
- public string Subject { get; set; }
- public string 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.
MailController.cs
- 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]
- public class MailController : Controller
- {
- // GET: Mail
- public async Task<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 = new List<MyMessage>();
- var signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
- var userObjectId = ClaimsPrincipal.Current.FindFirst("https://schemas.microsoft.com/identity/claims/objectidentifier").Value;
- //Create an authentication context from cache
- AuthenticationContext authContext = new AuthenticationContext(authority, new ADALTokenCache(signInUserId));
- try
- {
- DiscoveryClient discClient = new DiscoveryClient(new Uri(discoveryServiceEndpointUri),
- async () =>
- {
- //Get an access token to the discovery service
- var authResult = await authContext.AcquireTokenSilentAsync(discoveryResourceID, new ClientCredential(clientID, clientSecret), new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
- return authResult.AccessToken;
- });
- var dcr = await discClient.DiscoverCapabilityAsync("Mail");
- OutlookServicesClient exClient = new OutlookServicesClient(dcr.ServiceEndpointUri,
- async () =>
- {
- //Get an access token to the Messages
- var authResult = await authContext.AcquireTokenSilentAsync(dcr.ServiceResourceId, new ClientCredential(clientID, clientSecret), new UserIdentifier(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(new MyMessage
- {
- 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.
Code Snippet
- <div class="navbar-collapse collapse">
- <ul class="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
Comments
Anonymous
March 24, 2015
Awesome work Kirk! Really useful and showing how the O365 API's and the SharePoint client side API's can be combined.Anonymous
March 30, 2015
Thanks Kirk for this wonderful post!!Anonymous
April 01, 2015
I have been looking at similar posts online, nothing comes close to this level of detail and clearness. Two thumbs up! :)Anonymous
April 06, 2015
Hi Kirk, Great article, thanks! I'm trying to get this to work when calling EWS Managed Api (using the oAuthCredentials class) I keep getting either 401s or 403s. So it appears the Access Token Claims don't really match for Managed API and the 365 API? Any ideas on this? MartinAnonymous
April 07, 2015
@Martin - stackoverflow.com/.../office-365-ews-authentication-using-oauthAnonymous
April 07, 2015
Hi Kirk, I did read that post beforehand. I am using the oAuthCredentials class, and I checked all the boxes in Delegated dropdown (in the azure app) I am an Administrator for my Tenant, so that means after it prompts me for access to the app, it should grant me access, right? After silently logging on and granting access to the app, it still gives me a 401 error. Any ideas? Should I add &prompt=admin_consent to the Logon url or something? In effect your saying that it should work. (thats also important to note :)Anonymous
April 07, 2015
I forget to say that the REST Api's are working as expected, so the access token is valid and in use for that.Anonymous
April 09, 2015
Looks like it is all documented here. msdn.microsoft.com/.../dn903761(v=exchg.150).aspxAnonymous
April 12, 2015
Okay thanks. I did read that as well beforehand. but it appears I've been ignoring your AdalTokenCache in my custom EWS call. That in combination with some use of the wrong AcquireToken() approach apparently resulted in the errors. Anyway, got it working now, thanks.Anonymous
April 29, 2015
Thanks Kirk! This is exactly what I have been looking for although my scenario is slightly different but I'm hoping the same pattern will work. I have built an office app (for outlook) deployed to O365 and in that app I'm trying to grab some list items from SharePoint or save items to SharePoint. Will the authentication work the same way as described here? Thanks! -PaulAnonymous
April 29, 2015
@Paul - I haven't tried, but if you get it to work please leave a comment here with a link to a blog posting for details!