The purpose of this guide is to walk through the process of creating a simple ASP.NET MVC C# app that retrieves messages in Office 365 or Outlook.com. The source code in this repository is what you should end up with if you follow the steps outlined here.
This tutorial will use the Active Directory Authentication Library (Prerelease) to make OAuth2 calls and the Microsoft Office 365 Mail, Calendar, and Contacts Library for .NET to call the Mail API.
NOTE: The previous version of this tutorial used the Microsoft Office 365 API Tools to register the application in Azure AD. The registrations created with this tool are incompatible with Outlook.com, so this tutorial has been updated to use the Application Registration Portal instead. For the original version, see the v1 branch.
NOTE: If you are downloading this sample, you'll need to do a few things to get it to run.
- Open the dotnet-tutorial.sln file.
- Right-click References in Solution Explorer and choose Manage NuGet Packages.
- Click the Restore button in the Manage NuGet Packages dialog to download all of the required packages.
This guide assumes:
- That you already have Visual Studio 2013 installed and working on your development machine.
- That you have an Office 365 tenant, with access to an administrator account in that tenant, OR an Outlook.com developer preview account.
Let's dive right in! In Visual Studio, create a new Visual C# ASP.NET Web Application. Name the application "dotnet-tutorial".
Select the MVC
template. Click the Change Authentication
button and choose "No Authentication". Un-select the "Host in the cloud" checkbox. The dialog should look like the following.
Click OK to have Visual Studio create the project. Once that's done, run the project to make sure everything's working properly by pressing F5 or choosing Start Debugging from the Debug menu. You should see a browser open displaying the stock ASP.NET home page. Close your browser.
Now that we've confirmed that the app is working, we're ready to do some real work.
Our app will be very simple. When a user visits the site, they will see a button to log in and view their email. Clicking that button will take them to the Azure login page where they can login with their Office 365 or Outlook.com account and grant access to our app. Finally, they will be redirected back to our app, which will display a list of the most recent email in the user's inbox.
Let's begin by replacing the stock home page with a simpler one. Open the ./Views/Home/Index.cshtml
file. Replace the existing code with the following code.
@{
ViewBag.Title = "Home Page";
}
<div class="jumbotron">
<h1>ASP.NET MVC Tutorial</h1>
<p class="lead">This sample app uses the Mail API to read messages in your inbox.</p>
<p><a href="#" class="btn btn-primary btn-lg">Click here to login</a></p>
</div>
This is basically repurposing the jumbotron
element from the stock home page, and removing all of the other elements. The button doesn't do anything yet, but the home page should now look like the following.
Our goal in this section is to make the link on our home page initiate the OAuth2 Authorization Code Grant flow with Azure AD. To make things easier, we'll use the Microsoft.Experimental.IdentityModel.Clients.ActiveDirectory Prerelease NuGet package to handle our OAuth requests.
Before we proceed, we need to register our app to obtain a client ID and secret. Head over to https://apps.dev.microsoft.com to quickly get a client ID and secret. Using the sign in buttons, sign in with either your Microsoft account (Outlook.com), or your work or school account (Office 365).
Once you're signed in, click the Add an app button. Enter dotnet-tutorial
for the name and click Create application. After the app is created, locate the Application Secrets section, and click the Generate New Password button. Copy the password now and save it to a safe place. Once you've copied the password, click Ok.
Locate the Platforms section, and click Add Platform. Choose Web, then enter http://localhost:<PORT>/Home/Authorize
under Redirect URIs, where <PORT>
is the port number that the Visual Studio Development Server is using for your project. You can locate this by selecting the dotnet-tutorial
project in Solution Explorer, then checking the value for URL
in the Properties window.
Click Save to complete the registration. Copy the Application Id and save it along with the password you copied earlier. We'll need those values soon.
Here's what the details of your app registration should look like when you are done.
Open the Web.config
file and add the following keys inside the <appSettings>
element:
<add key="ida:ClientID" value="YOUR APP ID" />
<add key="ida:ClientSecret" value="YOUR APP PASSWORD" />
Replace the value of the ida:clientID
key with the application ID you generated above, and replace the value of the ida:ClientSecret
key with the password you generated above.
The next step is to install the ADAL and Outlook libraries from NuGet. On the Visual Studio Tools menu, choose NuGet Package Manager, then Package Manager Console. To install the ADAL library, enter the following command in the Package Manager Console:
PM> Install-Package Microsoft.Experimental.IdentityModel.Clients.ActiveDirectory -Version 4.0.208020147-alpha -Pre
Next install the Microsoft Office 365 Mail, Calendar and Contacts Library with the following command:
PM> Install-Package Microsoft.Office365.OutlookServices-V2.0
Now we're all set to do the sign in. Let's start by adding a SignIn
action to the HomeController
class. Open the .\Controllers\HomeController.cs
file. At the top of the file, add the following lines:
using System.Threading.Tasks;
using Microsoft.Experimental.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Office365.OutlookServices;
Add a private static string array to the HomeController
class to hold the scopes that the app will request.
// The required scopes for our app
private static string[] scopes = { "https://outlook.office.com/mail.read" };
Now add a new method called SignIn
to the HomeController
class.
public async Task<ActionResult> SignIn()
{
string authority = "https://login.microsoftonline.com/common";
string clientId = System.Configuration.ConfigurationManager.AppSettings["ida:ClientID"];
AuthenticationContext authContext = new AuthenticationContext(authority);
// The url in our app that Azure should redirect to after successful signin
Uri redirectUri = new Uri("#"); // TEMPORARY
// Generate the parameterized URL for Azure signin
Uri authUri = await authContext.GetAuthorizationRequestUrlAsync(scopes, null, clientId, redirectUri, UserIdentifier.AnyUser, null);
// Redirect the browser to the Azure signin page
return Redirect(authUri.ToString());
}
Notice that we set the redirectUri
variable to '#'
, which won't do a whole lot. We need to implement an action in our app that can receive a redirect back from Azure and use that URL as the value of redirectUri
.
Add another action to the HomeController
class called Authorize
. This action will serve as our redirect URL.
public ActionResult Authorize()
{
string authCode = Request.Params["code"];
return Content("Auth Code: " + authCode);
}
This doesn't do anything but display the authorization code returned by Azure, but it will let us test that we can successfully log in. Update the SignIn
action to use the URL for the Authorize
action for redirectUri
.
public ActionResult SignIn()
{
string authority = "https://login.microsoftonline.com/common";
string clientId = System.Configuration.ConfigurationManager.AppSettings["ida:ClientID"];
AuthenticationContext authContext = new AuthenticationContext(authority);
// The url in our app that Azure should redirect to after successful signin
Uri redirectUri = new Uri(Url.Action("Authorize", "Home", null, Request.Url.Scheme));
// Generate the parameterized URL for Azure signin
Uri authUri = await authContext.GetAuthorizationRequestUrlAsync(scopes, null, clientId, redirectUri, UserIdentifier.AnyUser, null);
// Redirect the browser to the Azure signin page
return Redirect(authUri.ToString());
}
Finally, let's update the home page so that the login button invokes the SignIn
action.
@{
ViewBag.Title = "Home Page";
}
<div class="jumbotron">
<h1>ASP.NET MVC Tutorial</h1>
<p class="lead">This sample app uses the Mail API to read messages in your inbox.</p>
<p><a href="@Url.Action("SignIn", "Home", null, Request.Url.Scheme)" class="btn btn-primary btn-lg">Click here to login</a></p>
</div>
Save your work and run the app. Click on the button to sign in. After signing in, you should be returned to your app, which displays an authorization code. Now let's do something with it.
Now let's update the Authorize
function to retrieve a token. Replace the current code with the following.
// Note the function signature is changed!
public async Task<ActionResult> Authorize()
{
// Get the 'code' parameter from the Azure redirect
string authCode = Request.Params["code"];
string authority = "https://login.microsoftonline.com/common";
string clientId = System.Configuration.ConfigurationManager.AppSettings["ida:ClientID"];
string clientSecret = System.Configuration.ConfigurationManager.AppSettings["ida:ClientSecret"];
AuthenticationContext authContext = new AuthenticationContext(authority);
// The same url we specified in the auth code request
Uri redirectUri = new Uri(Url.Action("Authorize", "Home", null, Request.Url.Scheme));
// Use client ID and secret to establish app identity
ClientCredential credential = new ClientCredential(clientId, clientSecret);
try
{
// Get the token
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
authCode, redirectUri, credential, scopes);
// Save the token in the session
Session["access_token"] = authResult.Token;
// Try to get user info
Session["user_email"] = GetUserEmail(authContext, clientId);
return Content("Access Token: " + authResult.Token);
}
catch (AdalException ex)
{
return Content(string.Format("ERROR retrieving token: {0}", ex.Message));
}
}
The values returned by AcquireTokenByAuthorizationCodeAsync
don't just include the access token. It also contains in ID token. We can use this token to find out a few pieces of information about the logged on user. In this case, we want to get the user's email address. You'll see why we want this soon.
NOTE: The prerelease version of ADAL v4 doesn't return the ID token directly, but it is accessible. The method included here is intended to work around this issue until ADAL is updated.
Let's start by implementing GetUserEmail
.
private string GetUserEmail(AuthenticationContext context, string clientId)
{
// ADAL caches the ID token in its token cache by the client ID
foreach (TokenCacheItem item in context.TokenCache.ReadItems())
{
if (item.Scope.Contains(clientId))
{
return GetEmailFromIdToken(item.Token);
}
}
return string.Empty;
}
This function loops through the ADAL token cache to find the ID token, then passes the token to a GetEmailFromIdToken
function. Let's implement that.
private string GetEmailFromIdToken(string token)
{
// JWT is made of three parts, separated by a '.'
// First part is the header
// Second part is the token
// Third part is the signature
string[] tokenParts = token.Split('.');
if (tokenParts.Length < 3)
{
// Invalid token, return empty
}
// Token content is in the second part, in urlsafe base64
string encodedToken = tokenParts[1];
// Convert from urlsafe and add padding if needed
int leftovers = encodedToken.Length % 4;
if (leftovers == 2)
{
encodedToken += "==";
}
else if (leftovers == 3)
{
encodedToken += "=";
}
encodedToken = encodedToken.Replace('-', '+').Replace('_', '/');
// Decode the string
var base64EncodedBytes = System.Convert.FromBase64String(encodedToken);
string decodedToken = System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
// Load the decoded JSON into a dynamic object
dynamic jwt = Newtonsoft.Json.JsonConvert.DeserializeObject(decodedToken);
// User's email is in the preferred_username field
return jwt.preferred_username;
}
Save your changes and restart the app. This time, after you sign in, you should see an access token displayed. Now that we can retrieve the access token, we're ready to call the Mail API.
Let's start by adding a new action to the HomeController
class. Open the .\Controllers\HomeController.cs
file. Add a new function to the HomeController
class called Inbox
.
public async Task<ActionResult> Inbox()
{
string token = (string)Session["access_token"];
string email = (string)Session["user_email"];
if (string.IsNullOrEmpty(token))
{
// If there's no token in the session, redirect to Home
return Redirect("/");
}
return Content(string.Format("Email: {0}, Token: {1}", email, token));
}
Now update the Authorize
function to redirect to the Inbox
action after successfully retrieving a token.
public async Task<ActionResult> Authorize()
{
// Get the 'code' parameter from the Azure redirect
string authCode = Request.Params["code"];
string authority = "https://login.microsoftonline.com/common";
string clientId = System.Configuration.ConfigurationManager.AppSettings["ida:ClientID"];
string clientSecret = System.Configuration.ConfigurationManager.AppSettings["ida:ClientSecret"];
AuthenticationContext authContext = new AuthenticationContext(authority);
// The same url we specified in the auth code request
Uri redirectUri = new Uri(Url.Action("Authorize", "Home", null, Request.Url.Scheme));
// Use client ID and secret to establish app identity
ClientCredential credential = new ClientCredential(clientId, clientSecret);
try
{
// Get the token
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
authCode, redirectUri, credential, scopes);
// Save the token in the session
Session["access_token"] = authResult.Token;
// Try to get user info
Session["user_email"] = GetUserEmail(authContext, clientId);
return Redirect(Url.Action("Inbox", "Home", null, Request.Url.Scheme));
}
catch (AdalException ex)
{
return Content(string.Format("ERROR retrieving token: {0}", ex.Message));
}
}
If you run the app now, you won't see much of a change. It still just displays the access token. The difference is we're displaying it from the Inbox action, which verifies that we succeeded in passing the token via the Session
, and we're also displaying the user's email. Let's put these values to use.
Update the Inbox
function with the following code.
public async Task<ActionResult> Inbox()
{
string token = (string)Session["access_token"];
string email = (string)Session["user_email"];
if (string.IsNullOrEmpty(token))
{
// If there's no token in the session, redirect to Home
return Redirect("/");
}
try
{
OutlookServicesClient client = new OutlookServicesClient(new Uri("https://outlook.office.com/api/v2.0"),
async () =>
{
// Since we have it locally from the Session, just return it here.
return token;
});
client.Context.SendingRequest2 += new EventHandler<Microsoft.OData.Client.SendingRequest2EventArgs> (
(sender, e) => InsertXAnchorMailboxHeader(sender, e, email));
var mailResults = await client.Me.Messages
.OrderByDescending(m => m.ReceivedDateTime)
.Take(10)
.Select(m => new { m.Subject, m.ReceivedDateTime, m.From })
.ExecuteAsync();
string content = "";
foreach (var msg in mailResults.CurrentPage)
{
content += string.Format("Subject: {0}<br/>", msg.Subject);
}
return Content(content);
}
catch (AdalException ex)
{
return Content(string.Format("ERROR retrieving messages: {0}", ex.Message));
}
}
To summarize the new code in the mail
function:
- It creates an
OutlookServicesClient
object. - It adds an event handler for the
SendingRequest2
event on theOutlookServicesClient
object. This is where our work to get the user's email pays off. The event handler (which we will implement shortly) will add anX-AnchorMailbox
HTTP header to the outgoing API requests. Setting this header to the user's mailbox allows the API endpoint to route API calls to the appropriate backend mailbox server more efficiently. - It issues a GET request to the URL for inbox messages, with the following characteristics:
- It uses the
OrderBy()
function with a value ofReceivedDateTime desc
to sort the results by ReceivedDateTime. - It uses the
Take()
function with a value of10
to limit the results to the first 10. - It uses the
Select()
function to limit the fields returned to only those that we need.
- It uses the
- It loops over the results and prints out the subject.
Now let's implement the InsertXAnchorMailboxHeader
function.
private void InsertXAnchorMailboxHeader(object sender, Microsoft.OData.Client.SendingRequest2EventArgs e, string email)
{
e.RequestMessage.SetHeader("X-AnchorMailbox", email);
}
If you restart the app now, you should get a very basic listing of email subjects. However, we can use the features of MVC to do better than that.
MVC can generate views based on a model. So let's start by creating a model that represents the properties of a message that we'd like to display. For our purposes, we'll choose Subject
, ReceivedDateTime
, and From
. In Visual Studio's Solution Explorer, right-click the ./Models
folder and choose Add, then Class. Name the class DisplayMessage
and click Add.
Open the ./Models/DisplayMessage.cs
file and replace the empty class definition with the following.
public class DisplayMessage
{
public string Subject { get; set; }
public DateTimeOffset ReceivedDateTime { get; set; }
public string From { get; set; }
public DisplayMessage(string subject, DateTimeOffset? dateTimeReceived,
Microsoft.Office365.OutlookServices.Recipient from)
{
this.Subject = subject;
this.ReceivedDateTime = (DateTimeOffset)dateTimeReceived;
this.From = from != null ? string.Format("{0} ({1})", from.EmailAddress.Name,
from.EmailAddress.Address) | "EMPTY";
}
}
All this class does is expose the three properties of the message we want to display.
Now that we have a model, let's create a view based on it. In Solution Explorer, right-click the ./Views/Home
folder and choose Add, then View. Enter Inbox
for the View name. Change the Template field to List
, and choose DisplayMessage (dotnet_tutorial.Models)
for the Model class. Leave everything else as default values and click Add.
Just one more thing to do. Let's update the Inbox
function to use our new model and view.
public async Task<ActionResult> Inbox()
{
string token = (string)Session["access_token"];
string email = (string)Session["user_email"];
if (string.IsNullOrEmpty(token))
{
// If there's no token in the session, redirect to Home
return Redirect("/");
}
try
{
OutlookServicesClient client = new OutlookServicesClient(new Uri("https://outlook.office.com/api/v2.0"),
async () =>
{
// Since we have it locally from the Session, just return it here.
return token;
});
client.Context.SendingRequest2 += new EventHandler<Microsoft.OData.Client.SendingRequest2EventArgs> (
(sender, e) => InsertXAnchorMailboxHeader(sender, e, email));
var mailResults = await client.Me.Messages
.OrderByDescending(m => m.ReceivedDateTime)
.Take(10)
.Select(m => new Models.DisplayMessage(m.Subject, m.ReceivedDateTime, m.From))
.ExecuteAsync();
return View(mailResults.CurrentPage);
}
catch (AdalException ex)
{
return Content(string.Format("ERROR retrieving messages: {0}", ex.Message));
}
}
The changes here are minimal. Instead of building a string with the results, we instead create a new DisplayMessage
object within the Select
function. This causes the mailResults.CurrentPage
collection to be a collection of DisplayMessage
objects, which is perfect for our view.
Save your changes and run the app. You should now get a list of messages that looks something like this.
Now that you've created a working sample, you may want to learn more about the capabilities of the Mail API. If your sample isn't working, and you want to compare, you can download the end result of this tutorial from GitHub. If you download the project from GitHub, be sure to re-run the Add Connected Service wizard to register the app before trying it.
Copyright (c) Microsoft. All rights reserved.
Connect with me on Twitter @JasonJohMSFT
Follow the Exchange Dev Blog