To preface this, I'm new to Azure programming and Azure AD authentication and I've been following tutorials I've found at various sites (including MS) to get me this far. I'm using Xcode v7.2, ADAL for iOS v1.2.4, Visual Studio 2015 Update 1, and the Azure App Service Tools v2.8.1.
I have an existing native iOS app that I need to be able to authenticate multiple Azure Active Directory instance users through. These users are internal and external (customers who sign up for our services). To that end, I've experimentally implemented the following high level architecture:
Native Client App (iOS / obj-c) -> ADAL iOS library -> (Azure AD authentication) -> Azure Mobile App (service layer)
The iOS app utilizes the ADAL iOS library to acquire an access token which it uses to call authorized Web API services in the Azure Mobile App project.
I'm able to authenticate users from two tenants (an internal Azure AD and an external Azure AD), but only users in the same tenant as the service (internal) are able to call the authenticated APIs. The test user account I used from the external tenant is set up as a Global Admin and I am presented with the appropriate consent view in the native app when authenticating. I can then click through the consent and I receive an access token. When using that token to call a test API however, I get a 401 back. The verbose logs for the Azure Mobile App on the server show the following messages (all URLs below are https, I just don't have the rep to post them as such):
2016-01-12T13:00:55 PID[7972] Verbose Received request: GET MyAzureMobileApp.azurewebsites.net/api/values
2016-01-12T13:00:55 PID[7972] Verbose Downloading OpenID configuration from sts.windows.net/<internal AD GUID>/.well-known/openid-configuration
2016-01-12T13:00:55 PID[7972] Verbose Downloading OpenID issuer keys from login.windows.net/common/discovery/keys
2016-01-12T13:00:56 PID[7972] Warning JWT validation failed: IDX10205: Issuer validation failed. Issuer: 'sts.windows.net/<external AD GUID>/'. Did not match: validationParameters.ValidIssuer: 'sts.windows.net/<internal ad guid>/' or validationParameters.ValidIssuers: 'null'..
2016-01-12T13:00:56 PID[7972] Information Sending response: 401.71 Unauthorized
I've read in several posts that you can disable the token issuer validation in your service by setting the ValidateIssuer parameter in TokenValidationParameters to false. I've tried to do this, but it doesn't seem to have any effect. Here is the code from my Azure Mobile App project:
The startup code:
// Startup.cs
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(MyAzureMobileApp.Startup))]
namespace MyAzureMobileApp
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureMobileApp(app);
ConfigureAuth(app);
}
}
}
The code for the MobileApp -- this should be stock, as generated by the Azure Mobile App project template:
// Startup.MobileApp.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data.Entity;
using System.Web.Http;
using Microsoft.Azure.Mobile.Server;
using Microsoft.Azure.Mobile.Server.Authentication;
using Microsoft.Azure.Mobile.Server.Config;
using MyAzureMobileApp.DataObjects;
using MyAzureMobileApp.Models;
using Owin;
namespace MyAzureMobileApp
{
public partial class Startup
{
public static void ConfigureMobileApp(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
new MobileAppConfiguration()
.UseDefaultConfiguration()
.ApplyTo(config);
// Use Entity Framework Code First to create database tables based on your DbContext
Database.SetInitializer(new MobileServiceInitializer());
MobileAppSettingsDictionary settings = config.GetMobileAppSettingsProvider().GetMobileAppSettings();
if (string.IsNullOrEmpty(settings.HostName))
{
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
// This middleware is intended to be used locally for debugging. By default, HostName will
// only have a value when running in an App Service application.
SigningKey = ConfigurationManager.AppSettings["SigningKey"],
ValidAudiences = new[] { ConfigurationManager.AppSettings["ValidAudience"] },
ValidIssuers = new[] { ConfigurationManager.AppSettings["ValidIssuer"] },
TokenHandler = config.GetAppServiceTokenHandler()
});
}
app.UseWebApi(config);
}
}
public class MobileServiceInitializer : CreateDatabaseIfNotExists<MobileServiceContext>
{
protected override void Seed(MobileServiceContext context)
{
List<TodoItem> todoItems = new List<TodoItem>
{
new TodoItem { Id = Guid.NewGuid().ToString(), Text = "First item", Complete = false },
new TodoItem { Id = Guid.NewGuid().ToString(), Text = "Second item", Complete = false }
};
foreach (TodoItem todoItem in todoItems)
{
context.Set<TodoItem>().Add(todoItem);
}
base.Seed(context);
}
}
}
The authentication startup code:
// Startup.Auth.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IdentityModel.Tokens;
using System.Linq;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.ActiveDirectory;
using Owin;
namespace MyAzureMobileApp
{
public partial class Startup
{
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
ValidateIssuer = false
}
});
}
}
}
The service implementation:
using System.Web.Http;
using Microsoft.Azure.Mobile.Server.Config;
namespace MyAzureMobileApp.Controllers
{
// Use the MobileAppController attribute for each ApiController you want to use
// from your mobile clients
[MobileAppController]
// Use the MobileAppController attribute for each ApiController you want to use
// from your mobile clients
[Authorize]
public class ValuesController : ApiController
{
// GET api/values
public string Get()
{
return "GET returned: Hello World!";
}
// POST api/values
public string Post()
{
return "POST returned: Hello World!";
}
}
}
And my appSettings section in web.config:
<appSettings>
<add key="PreserveLoginUrl" value="true" />
<!-- Use these settings for local development. After publishing to your
Mobile App, these settings will be overridden by the values specified
in the portal. -->
<add key="MS_SigningKey" value="Overridden by portal settings" />
<add key="EMA_RuntimeUrl" value="Overridden by portal settings" />
<!-- When using this setting, be sure to add matching Notification Hubs connection
string in the connectionStrings section with the name "MS_NotificationHubConnectionString". -->
<add key="MS_NotificationHubName" value="Overridden by portal settings" />
<add key="ida:ClientId" value="-- MyAzureMobileApp App ID from Azure AD --" />
<add key="ida:Tenant" value="InternalTestAD.onmicrosoft.com" />
<add key="ida:Audience" value="https://InternalTestAD.onmicrosoft.com/MyAzureMobileApp" />
<add key="ida:Password" value="-- password value removed --" />
</appSettings>
I don't see a place to specify valid token issuers except as a property of the TokenValidationParameters collection in WindowsAzureActiveDirectoryBearerAuthenticationOptions.
According to my understanding of the code, I should have issuer validation disabled, but I have tried adding the external Azure AD STS URL here. Unfortunately, it doesn't seem to have any effect.
Does anybody know if this code is getting ignored or overridden for some reason? Is there some other setting I've missed to either disable issuer validation altogether, or specify a list of valid issuers?
I can certainly provide more information as requested, I'm just not sure what else might be relevant.
Thanks!
I believe I have found the cause of my validation logic being ignored. In the setup of my web api site in Azure App Services, I specified the primary tenant issuer URL by populating the Issuer URL textbox in the "Authentication/Authorization" > "Azure Active Directory Settings" blade. It turns out that when you're going to have more than one issuer (as in my multi-tenant scenario) you should leave this field blank.
It makes perfect sense that the JWT will validate against the issuer you provide in that textbox. What is not so intuitive to me is that you should leave it blank when you have more then one issuer. Maybe MS could add that in to the information bubble above it? Either that or provide some mechanism for allowing multiple issuer URLs.
Hopefully this saves someone else some time with this issue.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With