Wow, that title is a mouthful. Just reading that makes you think that this is some weird edge case that you’d never have to deal with but this is actually quite a common scenario. Let me give you the scenario we were trying to get to work:
- The user of the rich client application (I’ll use a UWP application but could be an iOS, Android, WPF, or in fact an external web site) needs to retrieve some information from their Office365 account. The required information isn’t currently available via the Graph API, so it requires the use of the Exchange Web Service (EWS) library, which is really only usable in applications written using the full .NET Framework (ie no PCL support). This means any calls to Exchange have to be proxied via a service (ie Web API) interface. Routing external calls via a service proxy is often a good practice as it makes the client application less fragile, making it possible to change the interactions with the external service without having to push out new versions of the application.
- Access to the WebAPI is protected using Azure AD. The credentials presented to the Web API will need to include appropriate permissions to connect through to Exchange using the EWS library.
- The user will need to sign into the rich client application. Their credentials will be used to access the Web API, and subsequently Exchange.
- Any user, from any tenant, should be able to sign into the rich client application and for information to be retrieved from their Exchange (ie Office 365) instance. In other words both the rich client and Web API need to be multi-tenanted.
Single Tenanted
I’ll start by walking through creating a single tenanted version of what we’re after. For this, I’m going to borrow the basic getting started instructions from Danny Strockis’ post (https://azure.microsoft.com/en-us/documentation/samples/active-directory-dotnet-webapi-onbehalfof/).
Creating the Applications
Let’s start by creating a new solution, Exchange Contacts, with a Universal Windows Platform (UWP) application, call it ExchangeContactsClient.
Next we’ll create a new ASP.NET Web Application project, call it ExchangeContactsWeb
For the moment I’m not going to setup either Authentication or hosting in Azure
Next I need to add the Active Directory Authentication Library (ADAL) NuGet package to both the WebAPI and UWP projects
To make it easy to debug and step through the interaction between the UWP application and the WebAPI I recommend setting Visual Studio up to start both projects when you press F5 to run. To do this, right-click the solution and select Set Startup Projects
I’d also suggest at this point running the solution to firstly make sure both applications are able to be run (ie that they were created correctly by Visual Studio) – I had to upgrade the Microsoft.NETCore.UniversalWindowsPlatform NuGet package in order for the UWP project to run. You also need to retrieve the localhost URL of the WebAPI that will be used later.
Local WebAPI URL: http://localhost:8859/
Configuring Azure AD
Before we continue developing the applications we’ll step over to Azure and configure the necessary applications in Azure Active Directory (Azure AD) – https://portal.azure.com. When you land on the Azure portal, make sure you’re working with the correct subscription – this is shown in the top right corner, under your profile information. In this case I’m going to be working with our Office 365 developer subscription (available for MSDN subscribers) called BTR Office Dev. This is associated with the btro365dev.onmicrosoft.com Azure AD tenant.
From the left side of the portal, select the Azure Active Directory node
Followed by selecting App registrations – this will list any existing Azure AD applications your organisation has.
You can think of an Azure AD application as a connector that allows your application to connect to Azure AD, so in order for both the UWP and WebAPI projects to connect to Azure AD I’ll need two new Azure AD applications
Register the WebAPI application
Click the Add button at the top of the list of the Azure AD applications
Give the application a friendly name (ExchangeContactsWeb), make sure the Application Type is set to Web app/API and specify the Sign-on URL. The Sign-on URL should be the Local WebAPI URL recorded earlier, ie https://localhost:8859/
After creating the Azure AD application for the Web API application, immediately select it from the list of Azure AD applications and take note of the Application Id.
Web API Client Id: 88e4051b-9f88-4ccb-9d0b-e7fca46c4430
App Key: T35aTVE9vrA5TEGCg9Jyw8wOzO47/IZpcN3NPeMDF/A=
Register the UWP application
Click the Add button at the top of the list of Azure AD applications
Give the application a friendly name (ExchangeContactsClient), make sure the Application Type is set to Native, and specify the Redirect URI (eg https://ContactsClient). The URL you specify for the Redirect URL doesn’t matter, so long as it’s a valid URL and that you specify the same Redirect URI within the UWP application when attempting to authenticate.
After creating the Azure AD application for the UWP application, immediately select it from the list of Azure AD applications and take note of the Application Id.
UWP Client Id: 1b64d2a6-32f4-4dba-9e3f-73def5520baa
Connecting the Azure AD applications
At this point you have two Azure AD application: one that the UWP application will use to connect to Azure AD, the other that the Web API will connect with. In order for a user to sign into the UWP application and request access to the Web API, these two Azure AD applications need to be connected. This is done by selecting the native application (ie ExhcnageContactsClient) from the list of Azure AD applications, selecting All Settings, followed by Required permissions, followed by the Add button. Type the start of the name of the WebAPI Azure AD application eg “exchange” to bring up the list of applications that can be added
As you can see from the images, this brings up No Results. This is because there appears to be an issue with the way that Azure AD applications are registered when created through the new Azure portal. Note that this is true at the time of writing this post, but may well be fixed in the future as the Azure AD management experience comes out of preview. The following step is required in order to fix this issue: Click on the Overview node, then at the top of the overview pane, click the link to the Classic portal. Alternatively you can just go to https://manage.windowsazure.com
Once in the classic portal, from the list of icons on the left side, select Active Directory (diamond icon), then select the WebAPI Azure AD application, followed by the Configure tab. Once on the configure tab, remove the trailing slash from the Sign-on URL (eg https://localhost:8859) and click Save. Then add the slash back to the Sign-on URL (eg https://localhost:8859/).
Doing this seems to re-register (correctly) the Web API Azure AD application. Whilst in the Classic portal, scroll down and locate the section entitled “single sign-on” and retrieve the App ID URI – this is the resource identifier for the Web API
Web API Resource Id: https://btro365dev.onmicrosoft.com/a52151ed-8e86-4827-b657-1cd20061b86c
In the Azure portal (https://portal.azure.com) navigate back to the UWP Azure AD application (ie ExchangeContactsClient), All settings, Required permissions, Add and then again enter the start of the name of the WebAPI Azure AD application (eg “exchange”). This should now display the Web API Azure AD application (ie ExchangeContactsWeb).
Select the Azure AD application (ie ExchangeContactsWeb) and click Select to move onto selecting permissions
Check the box alongside “Access ExchangeContactsWeb” and click Select, followed by Done to complete the process of setting up permissions.
Building the Web Application
Add a new controller,ContactsController, into the Web API project.
In order to take advantage of Azure AD in order to authorize access to the Web API I need to add a reference to Microsoft.Owin.Security.ActiveDirectory.
At this point, it’s also worth making sure that other NuGet packages are updated – when preparing this sample the Newtonsoft.Json package had to be updated, along with making sure the binding redirect in web.config is updated
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" culture="neutral" publicKeyToken="30ad4fe6b2a6aeed" />
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
</dependentAssembly>
The other NuGet reference that the ContactsController will need is Microsoft.Graph so that information can be retrieved on behalf of the user.
Next I’ll add a Get method to the new ContactsController. I’ll also apply the Authorize attribute to the ContactsController which will ensure any request has a bearer token. I’ll come back to the Get method later and add implementation details to retrieve Contacts
[Authorize]
public class ContactsController : ApiController
{
public async Task<IEnumerable<string>> Get()
{
return new[]{"To be determined"};
}
}
In order to use Azure AD to authenticate requests it’s also necessary to modify the Startup.Auth.cs
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = ConfigurationManager.AppSettings["ida:Audience"],
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters
{
SaveSigninToken = true
}
});
}
}
The Startup relies on two app settings that need to be added to the web.config
<add key="ida:Tenant" value="btro365dev.onmicrosoft.com" />
<add key="ida:Audience" value="https://btro365dev.onmicrosoft.com/a52151ed-8e86-4827-b657-1cd20061b86c" />
The Tenant value is the name of the tenant where the Azure AD applications are defined. You’ll notice that the Audience value is the Web API Resource Id that was recorded earlier.
Building the UWP Application
I’ll start by adding a simple button to the MainPage of the application by modifying the file MainPage.xaml to include the following:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<Button Click="AuthenticateClick">Authenticate</Button>
</StackPanel>
</Grid>
In the code behind file, MainPage.xaml.cs, I’ll add the following:
private const string Authority = "https://login.microsoftonline.com/btro365dev.onmicrosoft.com/";
private const string WebAPIResourceId = "https://btro365dev.onmicrosoft.com/a52151ed-8e86-4827-b657-1cd20061b86c";
private const string UWPClientId = "1b64d2a6-32f4-4dba-9e3f-73def5520baa";
private static Uri RedirectUri { get; } = new Uri("https://ContactsClient");
private const string BaseServiceUrl = "http://localhost:8859";
private async void AuthenticateClick(object sender, RoutedEventArgs e)
{
var authContext = new AuthenticationContext(Authority);
try
{
var result = await authContext.AcquireTokenAsync(WebAPIResourceId, UWPClientId, RedirectUri,
new PlatformParameters(PromptBehavior.Always, false));
using (var httpClient = new HttpClient())
{
// Once the token has been returned by ADAL, add it to the http authorization header, before making the call to access the To Do list service.
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer",result.AccessToken);
// Retrieve the list of contacts
var response = await httpClient.GetAsync(BaseServiceUrl + "/api/contacts");
if (response.IsSuccessStatusCode)
{
// Read the response and databind to the GridView to display To Do items.
var contacts = await response.Content.ReadAsStringAsync();
Debug.WriteLine(contacts);
}
}
}
catch (AdalException)
{
// Handle both user cancelling sign in and/or authentication failure
}
}
This code defines a number of constants as follows:
Authority: This is the url where the user is navigated to in order to be authenticated against Azure AD – the BTR Office Dev tenant is clearly visible at the end of the Authority
WebAPIResourceId: This identified the resource that the initial authentication attempt is request access to
UWPClientId: In order to access Azure AD the UWP application uses the Azure AD native application setup earlier
RedirectUri: As the authentication is done via a web page, the RedirectUri defines where the browser will be navigated to at the end of the authentication process. In this case it’s a fake Uri, which has to correspond with the RedirectUri specified in the Azure AD native application.
BaseServiceUrl: This is the base url of the Web APIs that the application will be retrieving data from.
The code uses ADAL to acquire an access token, which can then be presented when invoking a Get operation on the ContactsController.
When you run the solution, you should see a web browser appear, as well as the ExchangeContacts application. Clicking the Authenticate button launches a familiar dialog that prompts for email address and password. At this point the user has to be a member of the btro365dev.onmicrosoft.com tenant.
After signing in, the user will also be prompted to grant permission for the application to access information.
Side Note on the Access Token
After the authentication process is completed, it’s possible to extract the access token and then use a website like jwt.io, to unpackage the access token. For example, the following is taken from the results.AccessToken object after the user signs into the application.
eyJ0eXAiOiJKV1QiLCJub25jZSI6IkFRQUJBQUFBQUFEUk5ZUlEzZGhSU3JtLTRLLWFkcENKaEFrMWJhQjJnRHpaN1J3OUh4WkI4dEdJUVFGdzI4WWVDd2dlRFYxRUZhRjJvMFd5MzFORG0tS0dWZTc1Rm9JOGdvSlBJa1hLSkVydmQzV19MajQwMWlBQSIsImFsZyI6IlJTMjU2IiwieDV0IjoiSTZvQnc0VnpCSE9xbGVHclYyQUpkQTVFbVhjIiwia2lkIjoiSTZvQnc0VnpCSE9xbGVHclYyQUpkQTVFbVhjIn0.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC85MjY2ZmE2Yy0yYTU4LTRmMjQtYTI0Yi0yYTEzMTk4NTliM2UvIiwiaWF0IjoxNDc2ODg2MzAxLCJuYmYiOjE0NzY4ODYzMDEsImV4cCI6MTQ3Njg5MDQ5NywiYWNyIjoiMSIsImFtciI6WyJwd2QiXSwiYXBwX2Rpc3BsYXluYW1lIjoiRXhjaGFuZ2VDb250YWN0c1dlYiIsImFwcGlkIjoiODhlNDA1MWItOWY4OC00Y2NiLTlkMGItZTdmY2E0NmM0NDMwIiwiYXBwaWRhY3IiOiIxIiwiZV9leHAiOjExMDk1LCJmYW1pbHlfbmFtZSI6IlJhbmRvbHBoIiwiZ2l2ZW5fbmFtZSI6Ik5pY2siLCJpcGFkZHIiOiI2MC4yNDEuMTg1LjUwIiwibmFtZSI6Ik5pY2sgKE9mZmljZSAzNjUgRGV2KSIsIm9pZCI6IjdkY2NhYWQxLTZiZDgtNDZjMi1hZGM3LWQ1ODY2MjllMTAxMiIsInB1aWQiOiIxMDAzQkZGRDhFRDRBREUxIiwic2NwIjoiVXNlci5SZWFkIiwic3ViIjoiaW4wQklSdFNOblhHckhTNWlncGdCb2JvdU9Nc0x3V2NkRFhNVXdPbjNaZyIsInRpZCI6IjkyNjZmYTZjLTJhNTgtNGYyNC1hMjRiLTJhMTMxOTg1OWIzZSIsInVuaXF1ZV9uYW1lIjoibmlja0BidHJvMzY1ZGV2Lm9ubWljcm9zb2Z0LmNvbSIsInVwbiI6Im5pY2tAYnRybzM2NWRldi5vbm1pY3Jvc29mdC5jb20iLCJ2ZXIiOiIxLjAifQ.CfeVUjnJt8dGdKGFEK39piuhB4imbWJh06uqoonfU-gQmHB4VWgY0i43b-YyXZRP9z4SD7dXGiS1IzDUX4GT3VvlsCXq48RkkbSKVzZLVZk_VojiPDn3jSczw70VKFZrXqPCfRcvhbwnFBGTlY_06vtDT3jE-TACLc8jR3D_mTSL9d8akWsxUhVIw6vKaRuyTpBtnO7KO8KS87oLf0wJlKaSfWTO5Nykp5AUhQHfAOij33XOejy5dNIWkml00kO1agBP4WMCHm3EklLlOrnpW8K71KHweqflVK8Zzy8H6XORt3UZIFiCUUF1ORPxQgHpSNJ8Sqw7LlKGPxC6NHd9wA
Entering this into the site http://jwt.io this token can be extracted to show relevant information about the user and their permissions.
{
"aud": "https://btro365dev.onmicrosoft.com/a52151ed-8e86-4827-b657-1cd20061b86c",
"iss": "https://sts.windows.net/9266fa6c-2a58-4f24-a24b-2a1319859b3e/",
"iat": 1476887372,
"nbf": 1476887372,
"exp": 1476891272,
"acr": "1",
"amr": [
"pwd"
],
"appid": "1b64d2a6-32f4-4dba-9e3f-73def5520baa",
"appidacr": "0",
"e_exp": 10800,
"family_name": "Randolph",
"given_name": "Nick",
"ipaddr": "60.241.185.50",
"name": "Nick (Office 365 Dev)",
"oid": "7dccaad1-6bd8-46c2-adc7-d586629e1012",
"scp": "user_impersonation",
"sub": "Qu6BNhi6dbma-rLraV_3B06be5DsdohiMlt8hJsHpVM",
"tid": "9266fa6c-2a58-4f24-a24b-2a1319859b3e",
"unique_name": "nick@btro365dev.onmicrosoft.com",
"upn": "nick@btro365dev.onmicrosoft.com",
"ver": "1.0"
}
As you can see, the information in this json the user is nick@btro365dev.onmicrosoft.com. What’s not immediately obvious is that this token has been issued by the BTR Office Dev tenant, which is the tenant that the Azure AD applications are defined – to be expected in a single tenanted scenario. In the Classic portal you can see the tenant Id when you select the Azure AD instance – it’s the guid that appears in the address bar. This is much less obvious in the new portal. This guid matches the guid in the iss field in the access token (ie "iss": https://sts.windows.net/9266fa6c-2a58-4f24-a24b-2a1319859b3e/)
Accessing Graph API
In the previous section we can see that the audience of the access token is https://btro365dev.onmicrosoft.com/a52151ed-8e86-4827-b657-1cd20061b86c, which essentially is the identifier of the Web API in Azure, meaning that this access token can be presented when making requests to the Web API. However, if we then want to call on another service, in this case the Microsoft Graph API, we have to exchange this access token for one that is valid for the Graph API. The following code retrieves an access token for connecting to the Graph API and then uses it to retrieve the display name of the current user (Currently we haven’t assigned enough permissions to the Azure AD apps in order to query the Contacts for the user but by default they have permission to query information about themselves)
[Authorize]
public class ContactsController : ApiController
{
private static string Authority { get; } = ConfigurationManager.AppSettings["ida:Authority"];
private static string WebAPIClientId { get; } = ConfigurationManager.AppSettings["ida:WebAPIClientID"];
private static string WebAPIAppKey { get; } = ConfigurationManager.AppSettings["ida:AppKey"];
private static string GraphResourceId { get; } = ConfigurationManager.AppSettings["ida:GraphResourceId"];
public async Task<IEnumerable<string>> Get()
{
var accessToken = await RetrieveAccessToken();
return await RetrieveContacts(accessToken);
}
private static async Task<string> RetrieveAccessToken()
{
var clientCred = new ClientCredential(WebAPIClientId, WebAPIAppKey);
var bootstrapContext =
ClaimsPrincipal.Current.Identities.First().BootstrapContext as
System.IdentityModel.Tokens.BootstrapContext;
var userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null
? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value
: ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
var userAssertion = new UserAssertion(bootstrapContext?.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer",
userName);
var authContext = new AuthenticationContext(Authority);
var result = await authContext.AcquireTokenAsync(GraphResourceId, clientCred, userAssertion);
var accessToken = result.AccessToken;
return accessToken;
}
public static async Task<IEnumerable<string>> RetrieveContacts(string accessToken)
{
var graphClient = new GraphServiceClient(
"https://graph.microsoft.com/v1.0",
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
}));
var data = graphClient.Me.Request();
var getresult = await data.GetAsync();
Debug.WriteLine(getresult != null);
return new[] { getresult.DisplayName };
}
}
This code relies on some additional web.config app settings
<appSettings>
<add key="ida:Tenant" value="btro365dev.onmicrosoft.com" />
<add key="ida:Audience" value="https://btro365dev.onmicrosoft.com/a52151ed-8e86-4827-b657-1cd20061b86c" />
<add key="ida:WebAPIClientID" value="88e4051b-9f88-4ccb-9d0b-e7fca46c4430" />
<add key="ida:AppKey" value="T35aTVE9vrA5TEGCg9Jyw8wOzO47/IZpcN3NPeMDF/A=" />
<add key="ida:Authority" value="https://login.microsoftonline.com/btro365dev.onmicrosoft.com" />
<add key="ida:GraphResourceId" value="https://graph.microsoft.com" />
</appSettings>
Multi-Tenanted
So far we have an application that a user can sign into and invoke a protected service in order to retrieve information from the Graph API. Unfortunately this only works for users that are part of the BTR Office Dev tenant. For example if I sign in with the user tester@builttoroam.com, I see the following error: AADSTS50020: User account 'tester@builttoroam.com' from identity provider 'https://sts.windows.net/9236ecf1-cfae-41f7-829c-af7e3d09a7e6/' does not exist in tenant 'BTR Office Dev' and cannot access the application '1b64d2a6-32f4-4dba-9e3f-73def5520baa' in that tenant. The account needs to be added as an external user in the tenant first. Sign out and sign in again with a different Azure Active Directory user account.
Essentially this is saying that we’ve attempted to sign into a single-tenanted application. More specifically we’re attempting to sign into an application via an authority that doesn’t know or understand about the user we’re attempting to sign in with. The first change we’ll make is to the sign in authority in the UWP application.
private const string Authority = "https://login.microsoftonline.com/common/";
Now when we sign in, we see a different error: AADSTS70001: Application with identifier '1b64d2a6-32f4-4dba-9e3f-73def5520baa' was not found in the directory builttoroam.com
In this case, the error is saying that the UWP client application (ie the application with Azure Ad Client Id '1b64d2a6-32f4-4dba-9e3f-73def5520baa') doesn’t exist within the Built to Roam directory. This is correct, since it was created in the Btr Office Dev tenant. To address this issue, we need to indicate that this application should be enabled for other tenants (ie multi-tenanted). Open the Azure Ad application for the UWP application in the Azure portal and click the Manifest button. Adjust the “availableToOtherTenants” attribute to “true”
Now when you run the UWP application and attempt to sign in with a foreign user (eg tester@builttoroam.com) you see a different error, this time it appears in code, rather than the Azure authentication UI: Additional information: AADSTS50001: The application named https://btro365dev.onmicrosoft.com/a52151ed-8e86-4827-b657-1cd20061b86c was not found in the tenant named builttoroam.com. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.
This can be a little confusing as it’s not immediately obvious where the identifier “https://btro365dev.onmicrosoft.com/a52151ed-8e86-4827-b657-1cd20061b86c “ comes from – the guid doesn’t match the application Id for any of the Azure Ad applications. It is in fact the Web API Resource Id, and it’s essentially saying that the Azure Ad application for the Web API doesn’t exist in the Built to Roam tenant. Again, this is true, since it was created in the Btr Office Dev tenant. Again, we need to adjust the Azure Ad application for the Web API to allow use for other tenants, by modifying the availableToOtherTenants attribute in the manifest.
Again, if you run the UWP application and sign in using a foreign user, you’ll see another, different, exception: Additional information: AADSTS65005: Resource '88e4051b-9f88-4ccb-9d0b-e7fca46c4430' does not exist or one of its queried reference-property objects are not present.
This error is much harder to diagnose as the error basically just says that either the Web API doesn’t exist (the resource it references is the Application Id of the Azure Ad application for the Web API) or that it isn’t configured correctly, but it doesn’t say what configuration isn’t correct. This is actually a very poorly documented configuration change required to get multi-tenancy to work. You have to let the Web API know about the UWP application by updating the manifest of the Web API Azure Ad application to include the application id of the UWP Azure Ad application in it’s list of known client applications. Set the knownClientApplications attribute to include the application id of the UWP Azure AD application (ie 1b64d2a6-32f4-4dba-9e3f-73def5520baa)
Running the UWP application and signing in using a foreign user now yields a permissions prompt, granting the UWP application permissions to access the user’s profile:
After giving approval, the UWP application will receive an access token eg eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ikk2b0J3NFZ6QkhPcWxlR3JWMkFKZEE1RW1YYyIsImtpZCI6Ikk2b0J3NFZ6QkhPcWxlR3JWMkFKZEE1RW1YYyJ9.eyJhdWQiOiJodHRwczovL2J0cm8zNjVkZXYub25taWNyb3NvZnQuY29tL2E1MjE1MWVkLThlODYtNDgyNy1iNjU3LTFjZDIwMDYxYjg2YyIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzkyMzZlY2YxLWNmYWUtNDFmNy04MjljLWFmN2UzZDA5YTdlNi8iLCJpYXQiOjE0NzY5MjAwNTYsIm5iZiI6MTQ3NjkyMDA1NiwiZXhwIjoxNDc2OTIzOTU2LCJhY3IiOiIxIiwiYW1yIjpbInB3ZCJdLCJhcHBpZCI6IjFiNjRkMmE2LTMyZjQtNGRiYS05ZTNmLTczZGVmNTUyMGJhYSIsImFwcGlkYWNyIjoiMCIsImVfZXhwIjoxMDgwMCwiZmFtaWx5X25hbWUiOiJVc2VyIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJpcGFkZHIiOiIxMTAuMTc0LjQ1LjIxOSIsIm5hbWUiOiJUZXN0IFVzZXIiLCJvaWQiOiJhNGE4OTA2Zi1hY2NmLTQyNDQtYmMyNC1kZmE5ODg3YTFiYTEiLCJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLCJzdWIiOiJEMHVZTnF5QnJabkdidXRmbUhTaGg5WlR5UjhFcW51YW1ULURYeFlocmdnIiwidGlkIjoiOTIzNmVjZjEtY2ZhZS00MWY3LTgyOWMtYWY3ZTNkMDlhN2U2IiwidW5pcXVlX25hbWUiOiJ0ZXN0ZXJAYnVpbHR0b3JvYW0uY29tIiwidXBuIjoidGVzdGVyQGJ1aWx0dG9yb2FtLmNvbSIsInZlciI6IjEuMCJ9.YjLGoycr8pRdMaoJRD6FqhVzrV-TDsw3RJdzQ_IIwULAsyUVtjgQtq0wlsYo6O19JLxq99LhZMt4VwgCpaJzwoLQcL1TOOBR9LiBsEs9NdF2LJjMKLtC2_9tnaCjZA1o0ZqThWlwYlFfFRlRXn9M4Dx1CM-pRen3t4qnIP4jfkSFhVEKKxLnm_frUebpYDn2b3OyvlisP3V1uAhIHLUrjTr5vB4RZPkjfaWDxMFr2J4zzalnlTBgeB8FvbPxPfqr47AssfQ81gczKlFOI1lpnc2ddLndzNAoSG3TmrkesBYO_mtXHNjkQiersbbs-q3Mt1F4kY7AOMg64_9R72_wpg
If you expand this token, you’ll see that whilst the Audience hasn’t changed (since we’re still requesting access to the same resource), the issuing authority has (eg "iss": https://sts.windows.net/9236ecf1-cfae-41f7-829c-af7e3d09a7e6/). The issuing authority is now the Built to Roam tenant.
Even though the token now looks correct, when you attempt to call the Web API you’ll get a 401 Unauthorized exception, with almost no further information in the exception in the UWP application, nor will there be any exception thrown within the Web API. Luckily there is a configuration setting on the Web API that allows you to see more information about authentication failures. Add the following within the <configuration> tags of the Web.config file:
<system.diagnostics>
<switches>
<add name="Microsoft.Owin" value="Verbose" />
</switches>
</system.diagnostics>
Now you’ll see something similar to the following in the Output window when debugging:
Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationMiddleware Error: 0 : Authentication failed
System.IdentityModel.Tokens.SecurityTokenInvalidIssuerException: IDX10205: Issuer validation failed. Issuer: 'https://sts.windows.net/9236ecf1-cfae-41f7-829c-af7e3d09a7e6/'. Did not match: validationParameters.ValidIssuer: 'null' or validationParameters.ValidIssuers: 'https://sts.windows.net/9266fa6c-2a58-4f24-a24b-2a1319859b3e/'.
at System.IdentityModel.Tokens.Validators.ValidateIssuer(String issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.JwtSecurityTokenHandler.ValidateIssuer(String issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.JwtSecurityTokenHandler.ValidateToken(String securityToken, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
at Microsoft.Owin.Security.Jwt.JwtFormat.Unprotect(String protectedText)
Again, this error states what is failing but doesn’t really give much of an indication of what to do to fix it. This error essentially is saying that the authentication, despite configuring Azure Ad to be multi-tenanted, is still setup to only validate the Btr Office Dev users (ie against the https://sts.windows.net/9266fa6c-2a58-4f24-a24b-2a1319859b3e/ issuer). You just need to modify the Startup class of the WebAPI to disable issue validation:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = ConfigurationManager.AppSettings["ida:Audience"],
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters
{
SaveSigninToken = true,
ValidateIssuer = false
}
});
}
}
Running again…. and you’d hope we’re close by now… you guessed it…. a different exception. This time it occurs when attempting to retrieve information from the Graph API: Operation not supported as user tester@builttoroam.com not found in tenant 9266fa6c-2a58-4f24-a24b-2a1319859b3e
Looking at this, it’s clear that the wrong tenant is being used. This is because when we’re requesting the access token for the Graph API resource, we’re again doing this against the Btr Office Dev tenant (to be honest, I’m not sure why that works at all but it does seem to issue an access token, that is probably useless for accessing any external resource not defined within the Btr Office Dev tenant). The fix is to change the Authority used by the Web API that’s in the web.config file:
<add key="ida:Authority" value="https://login.microsoftonline.com/common" />
Now, hey presto, it works… the user (in fact any Azure Ad user from any tenant) can sign into the UWP application and retrieve their display name via a call to the Web API.
Retrieving Contacts from Exchange
The last step is for us to extend this to connect through to Exchange (ie Office 365) in order to retrieve a list of contacts. Retrieving Contacts can now be done via the Graph API but there are still some things, such as querying for rooms, that can only be done via the Exchange Web Services (EWS). So, for this exercise we’re going to use the EWS to connect to Office 365 in order to retrieve a list of contacts.
Add the Microsoft.Exchange.WebServices NuGet package to the WebAPI project.
I’ll update the service code to retrieve an access token for the Office 365 instead of the Graph API and then retrieve contacts using EWS
private static string Office365ResourceId { get; } = ConfigurationManager.AppSettings["Office365ServerUrl"];
private static string EWSUrl { get; } = ConfigurationManager.AppSettings["EWSUrl"];
public async Task<IEnumerable<string>> Get()
{
var accessToken = await RetrieveOffice365AccessToken();
return RetrieveContacts(accessToken);
}
private static async Task<string> RetrieveOffice365AccessToken()
{
var clientCred = new ClientCredential(WebAPIClientId, WebAPIAppKey);
var bootstrapContext =
ClaimsPrincipal.Current.Identities.First().BootstrapContext as
System.IdentityModel.Tokens.BootstrapContext;
var userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null
? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value
: ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
var userAssertion = new UserAssertion(bootstrapContext?.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer",
userName);
var authContext = new AuthenticationContext(Authority);
var result = await authContext.AcquireTokenAsync(Office365ResourceId, clientCred, userAssertion);
var accessToken = result.AccessToken;
return accessToken;
}
public static IEnumerable<string> RetrieveContacts(string accessToken)
{
var exchangeService = new ExchangeService
{
Url = new Uri(EWSUrl),
TraceEnabled = true,
TraceFlags = TraceFlags.All,
Credentials = new OAuthCredentials(accessToken)
};
var contactsfolder = ContactsFolder.Bind(exchangeService,
WellKnownFolderName.Contacts,
new PropertySet(BasePropertySet.IdOnly, FolderSchema.TotalCount));
var numItems = contactsfolder.TotalCount < 50 ? contactsfolder.TotalCount : 50;
var view = new ItemView(numItems);
// To keep the response smaller, request only the display name.
view.PropertySet = new PropertySet(BasePropertySet.IdOnly, ContactSchema.DisplayName);
// Request the items in the Contacts folder that have the properties that you selected.
var contactItems = exchangeService.FindItems(WellKnownFolderName.Contacts, view);
return contactItems.Items.Select(x => (x as Contact)?.DisplayName).ToArray();
}
This code relies on two additional app settings in the web.config file
<add key="Office365ServerUrl" value="https://outlook.office365.com" />
<add key="EWSUrl" value="https://outlook.office365.com/ews/exchange.asmx" />
If you attempt to run this you’ll run into exceptions where the Azure AD applications don’t have permissions on the Office 365 Exchange Online resource. To resolve this open both Azure Ad applications (ie for the Web API and the UWP application) and add the Office 365 Exchange Online (Microsoft.Exchange) resource
Make sure you assign the “Access mailboxes as the signed-in user via Exchange Web Service” option which is a delegated permission that doesn’t require administrator approval.
When you run the application and sign in, the first time for each user, you’ll see a permissions request, giving access to the Exchange mailbox for the user.
Once approval has been granted, the UWP application will be able to connect to the Web API, and retrieve information from Office 365 via the Exchange Web Services.