I've recently started playing with Azure Active Directory to authenticate users against my website built on AngularJS.
Using blogs and sample code on GitHub, I've gotten it working with single-tenant using a combination of ADAL.js and Katana's Bearer Token AD integration.
However, I'm now running into some issues with supporting multiple tenants.
I've got a page set up that displays the user as ADAL sees them (found through the root scope's userInfo
), as well as makes a call to my server that gets picked up by OWIN, and serializes context.Authentication.User
.
Client-side, everything seems to be working properly. I can log in with any of my tenants, and it gives me the object I'd expect (with isAuthenticated: true
, username
populated, and all sorts of properties on profile
describing the user, login, and tenant).
This is accomplished client-side by leaving off the tenant
argument to my adalAuthenticationServiceProvider.init
call, as described in the documentation.
Server-side, however, the UseWindowsAzureActiveDirectoryBearerAuthentication
method doesn't like having no value for Tenant
(in that it throws an exception). I've tried a few values for this, including the tenant with which my app was originally registered and, my logical favorite, "common," but no matter what I put in there (unless it's the tenant I'm trying to log in with, and if my ADAL is set up with that tenant), it seems to just skip over this.
For what it's worth, an actual API call is failing on the [Authorize]
filter and returning a 401, which tells me this isn't an issue with my OWIN interceptor.
How can I tell UseWindowsAzureActiveDirectoryBearerAuthentication
to support multi-tenant authentication?
When you are developing a multi tenant application, you can no longer rely 100% on the default authentication logic. The default authentication logic assumes that you declare the azure AD tenant form where you want to receive tokens, and will enforce than only tokens form that tenant are accepted. This is done by examining the metadata document associated to every tenant, which contains (among other things) the identifier of the tenant itself - that identifier must be present in the token you receive, in the iss claim: any other value means that the token comes form another tenant, hence it must be rejected.
By definition, multitenant applications must accept tokens from multiple tenants. This is done by using a parametric endpoint (the common endpoint, see this post) which allows you to "late bind" which tenant will be used to issue a token. However the common endpoint will serve a generic metadata document, which cannot contain a specific iss value: instead, it contains a placeholder that at runtime will always be substituted with the issuer identifier of the tenant you actually got the token from.
This means that in multitenant apps you have to take over the tenant validation logic. If you are just debugging you can turn it off, as you appear to have done - that will prevent the default issuer validation logic from kicking in and refusing the incoming token because its iss value does not correspond to the placeholder found for common. In more realistic cases, however, you will write your own logic in the TokenValidationParameters.IssuerValidator
delegate. For example, you might want to compare the iss value in the incoming token against a list of tenants that bought a monthly subscription to your service. HTH
I figured this out while writing the question. I think. But I spent all day on this finding almost no documentation on the matter, so I figured I'd post it anyway.
My solution (found through yet another blog post) was to include ValidateIssuer = false
as a parameter. This makes sense, since we no longer want to validate that the tenant giving us a token is the one that we've listed.
Here's my code that solved the problem.
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
ValidateIssuer = false // This line made it work
},
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,
Tenant = "common" // I don't know whether this has any impact,
// but it's a required parameter regardless.
});
I'd love if someone else wanted to correct me if this has any unforeseen circumstances--it's a tad daunting flipping a "validate" switch to off when you're working on authentication. But I think this all makes enough sense.
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