OWIN Authentication Pipeline To Use Katana Middleware Correctly?

25,036

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.

Correct Code

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:

  1. First, tell the Owin security pipeline that by default we want to authenticate requests with the default CookeAuthentication AthenticationType value. (That's just a constant string on that CookieAuthenticationDefaults class, and it's the default value used by the CookieAuthenticationOptions.AuthenticationType property.)
  2. Next, register a cookie authentication middleware instance with all default options, so it corresponds to the AuthenticationType key that we set as the default in step 1.
  3. Next, register a WS-Federation authentication middleware instance with options that we define in the Web.config file, and with a custom AuthenticationType value so we can refer to it later.
  4. After all the authentication middleware registrations are done, we tell the pipeline to authenticate all requests (via our custom helper method that calls the Microsoft.Owin.Security methods for issuing challenges to any unauthenticated request)
  5. Finally, if the user has been authenticated, show the welcome page!

Wrong Code

So there are a couple ways you can go wrong here.

Not providing a default authentication type

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.

Using the wrong default authentication type

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

Possibilities

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

Share:
25,036
Tom Tregenna
Author by

Tom Tregenna

Public sector code monkey, and general problem solver.

Updated on August 10, 2021

Comments

  • Tom Tregenna
    Tom Tregenna almost 3 years

    I'm looking to use WsFederation Authentication against an internal ADFS 2 service and to use the OWIN authentication pipeline .

    What is considered to be the order in which middleware should be hooked up and which modules are required in various scenarios with minimal code?


    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.

    The following (with a custom SAML Token handler to read the token string into a valid XML document), works, 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
                    }
                }
            }
        }
    );
    
  • Tom Tregenna
    Tom Tregenna over 9 years
    Many thanks Lars, I finally managed to get the time to look at this again (sorry for the delay in accepting!), both yourself and Tratcher have been a great help with this, I've summed up what I think I know in my answer here - stackoverflow.com/questions/23262471/…
  • Lars Kemmann
    Lars Kemmann over 9 years
    Thanks Tom. Much appreciated. :) And your summary is spot-on. When you say "my understanding is that the Federation middleware utilises the Cookie middleware to manage its authentication-related cookies" - I might word it slightly differently. The Federation middleware doesn't even know anything about cookies, it's just dumb and checks for a token in every request that it's asked to authenticate. Once the request is authenticated by the WS-Fed middleware, the Cookie middleware knows to issue a cookie (I'm not sure how, it's some capability of the pipeline). So it's just an inverted dependency.
  • Miguel
    Miguel about 5 years
    On step 3, it does not work without the Wreply property set. I'm setting it to the same value as Wtrealm and then it works.
  • Miguel
    Miguel about 5 years
    --- Let me expand on my previous comment. I found out that Wreply is needed when you have multiple EndPoints registered for your Relaying Party Trust in AD FS. Under those circumstances, if Wreply is not set AD FS will reply using the default EndPoint which could be different to the Wtrealm and cause issues.
  • ΩmegaMan
    ΩmegaMan almost 3 years
    I implemented your AuthenticateAllRequests and it does the challenge and the server never returns control, it sits on a profile page showing my login info. I have a question on it here Authentication Does Not Do A Return Trip on Challenge, Just Shows Current User Profile Page