I've recently started looking at the new ASP.Net Identity framework and the Katana middleware, there's a surprising amount of code and documentation out there, but I'm seeing what appears to be a lot of conflicting information, which I guess is a result of the increasing frequency of code updates.
I'm looking to use WsFederation Authentication against an internal ADFS 2 service, but the way the OWIN authentication pipeline works has me a little confused and I'm hoping someone can offer some definitive information.
Specifically, I'm interested in the order in which middleware should be hooked up and which modules are required in various scenarios, I'd like to get rid of anything that doesn't need to be there and at the same time ensure that the process is as secure as possible.
For example, it would appear that UseWsFederationAuthentication
should be used in conjunction with UseCookieAuthentication
, but I'm not sure what the correct AuthenticationType
would be (this post suggests that it's just an identifier string, but is it's value significant?) or even if we still need to use SetDefaultSignInAsAuthenticationType
.
I also noticed this thread on the Katana Project discussions board, where Tratcher mentions a common mistake, but isn't very specific as to which part of the code is in error.
Personally, I'm now using the following (with a custom SAML Token handler to read the token string into a valid XML document), it works for me, but is it optimal?
var appURI = ConfigurationManager.AppSettings["app:URI"];
var fedPassiveTokenEndpoint = ConfigurationManager.AppSettings["wsFederation:PassiveTokenEndpoint"];
var fedIssuerURI = ConfigurationManager.AppSettings["wsFederation:IssuerURI"];
var fedCertificateThumbprint = ConfigurationManager.AppSettings["wsFederation:CertificateThumbprint"];
var audienceRestriction = new AudienceRestriction(AudienceUriMode.Always);
audienceRestriction.AllowedAudienceUris.Add(new Uri(appURI));
var issuerRegistry = new ConfigurationBasedIssuerNameRegistry();
issuerRegistry.AddTrustedIssuer(fedCertificateThumbprint, fedIssuerURI);
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType // "Federation"
}
);
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = appURI,
SignOutWreply = appURI,
Configuration = new WsFederationConfiguration
{
TokenEndpoint = fedPassiveTokenEndpoint
},
TokenValidationParameters = new TokenValidationParameters
{
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType
},
SecurityTokenHandlers = new SecurityTokenHandlerCollection
{
new SamlSecurityTokenHandlerEx
{
CertificateValidator = X509CertificateValidator.None,
Configuration = new SecurityTokenHandlerConfiguration
{
AudienceRestriction = audienceRestriction,
IssuerNameRegistry = issuerRegistry
}
}
}
}
);
Many thanks for anything you can offer to help clear up this confusion for me.
As @Tratcher said, the AuthenticationType
parameter is used by Microsoft.Owin.Security
as a key to do lookups of authentication middleware instances.
The code below will use the following simple helper method to require that all requests are authenticated. In practice you're more likely to use an [Authorize]
attribute on sensitive controllers, but I wanted an example that doesn't rely on any frameworks:
private static void AuthenticateAllRequests(IAppBuilder app, params string[] authenticationTypes)
{
app.Use((context, continuation) =>
{
if (context.Authentication.User != null &&
context.Authentication.User.Identity != null &&
context.Authentication.User.Identity.IsAuthenticated)
{
return continuation();
}
else
{
context.Authentication.Challenge(authenticationTypes);
return Task.Delay(0);
}
});
}
The context.Authentication.Challenge(authenticationTypes)
call will issue an authentication challenge from each of the provided authentication types. We're just going to provide the one, our WS-Federation authentication type.
So first, here's an example of the "optimal" Owin Startup configuration for a site that's simply using WS-Federation, as you are:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions
{
AuthenticationType = "WS-Fed Auth (Primary)",
Wtrealm = ConfigurationManager.AppSettings["app:URI"],
MetadataAddress = ConfigurationManager.AppSettings["wsFederation:MetadataEndpoint"]
});
AuthenticateAllRequests(app, "WS-Fed Auth (Primary)");
app.UseWelcomePage();
}
Note the use of the "WS-Fed Auth (Primary)"
AuthenticationType
to uniquely identify the WS-Federation middleware instance we've configured. This means that you could, for example, use a "WS-Fed Auth (Secondary)"
with a separate WS-Federation server as a fallback, if you had that requirement.
This configuration will do the following:
CookieAuthenticationDefaults
class, and it's the default value used by the CookieAuthenticationOptions.AuthenticationType
property.)AuthenticationType
key that we set as the default in step 1.Microsoft.Owin.Security
methods for issuing challenges to any unauthenticated request)So there are a couple ways you can go wrong here.
To experiment, I tried doing this, and you'll see right away what the problem is:
public void Configuration(IAppBuilder app)
{
var x = app.GetDefaultSignInAsAuthenticationType();
app.SetDefaultSignInAsAuthenticationType(x);
}
That first call will give you the exception you mentioned in your first comment:
"A default value for SignInAsAuthenticationType was not found in IAppBuilder Properties. This can happen if your authentication middleware are added in the wrong order, or if one is missing."
Right - because by default the Microsoft.Owin.Security
pipeline doesn't assume anything about the middleware you're going to use (i.e., Microsoft.Owin.Security.Cookies
isn't even known to be present), so it doesn't know what should be the default.
This cost me a lot of time today because I didn't really know what I was doing:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType("WS-Fed AAD Auth");
// ... remainder of configuration
}
So, that's going to keep trying to authenticate the caller with WS-Federation on every call. It's not that that's super-expensive, it's that the WS-Federation middleware will actually issue a challenge on every request. So you can't ever get in, and you see a whole lot of login URLs fly past you. :P
So what's great about having all this flexibility in the pipeline is that you can do some really cool things. For instance, I have a domain with two different web apps inside of it, running under different subpaths like: example.com/foo
and example.com/bar
. You can use Owin's mapping functionality (as in app.Map(...)
) to set up a totally different authentication pipeline for each of those apps. In my case, one is using WS-Federation, while the other is using client certificates. Trying to do that in the monolithic System.Web
framework would be horrible. :P