WS-Federation with multiple federation locations

May 8, 2014 at 1:20 AM
I came across the WS-Federation middleware today and it works great if I have only a single MetadataAddress that I want to be able to log-in as. My requirement is that I have to be able to service multiple business tenants and each one has their own federated authentication store. Is there a way to determine the federated options based on the request (i.e. looking in the database based on an email address or something like that) and leveraging that? If not, is this something that is planned or is there a work-around that you could suggest?
Coordinator
May 8, 2014 at 3:31 AM
How many did you have in mind? If it's just a few then you could have multiple instances of the middleware.
May 8, 2014 at 7:51 AM
I'd like to know this too, as we are facing the same issue. Our app needs to allow login from multiple tenants. The number of tenants isn't fixed, and will grow as more customers sign up and use our application.
May 8, 2014 at 1:02 PM
It will start with at least a dozen or two. Enough that I don't want to add middleware for each and as mley said, it is not fixed and will continue to grow as new businesses signup. Ideally, I would get a parameter from the request context (or authentication properties) and use that to determine the options.
Coordinator
May 8, 2014 at 4:20 PM
You might be able to manually configure the Options.TokenValidationParameters for this. However, generating the correct challenge redirects will probably require implementing Options.Notifications.RedirectToIdentityProvider. I'll ping the protocol owner and see if they have any more suggestions.
May 8, 2014 at 4:57 PM
I was able to configure the RedirectToIdentityProvider notification so that I can dynamically set the IssuerAddress and Wtrealm, which will allow me to set those properties based on something in the context. Now I need to figure out how to configure the TokenValidationParameters correctly. Currently, I am setting AudienceValidator and IssuerValidator to simply return true but I am still getting the following exception: "The key needed to verify the signature could not be resolved from the following security key identifier 'SecurityKeyIdentifier'". I am not sure if I need to customize something with the SecurityTokenHandlers as well or if I am missing something with the TokenValidationParameters? Maybe need to implement IssuerSigningKeyRetrieval function?
May 8, 2014 at 5:17 PM
There were two pieces that I was missing. First, I needed to implement IssuerSigningKeyRetriever to dynamically determine the signing key. Currently, I have that certificate hard-coded in the function and I need to figure out how to make sense of the token to determine the correct tenant certificate to use but that is on me. Second, I had to disable audience validation. It appears that AudienceValidator class does not make use of TokenValidationParameters.AudienceValidator. It only looks for ValidAudience and ValidAudiences, which seems like a bug to me. Not exactly sure how severe it is to disable audience validation in a scenario like this. However, now I am at least able to federate authentication dynamically.
Coordinator
May 8, 2014 at 5:24 PM
Can you share some sample code of what you ended up doing?
May 8, 2014 at 5:43 PM
Actually little code, which is a huge testament to the library. I started with the individual accounts template. In the ChallengeResult, I determine the tenant based on some request parameters and add a ".tenant" property to the AuthenticationProperties.Dictionary. Then I use the following code in the startup routine. Again, a lot of it is still hard-coded at this point but the basic idea is there.
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions {
    SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
    TokenValidationParameters = new TokenValidationParameters {
        ValidateAudience = false,
        IssuerValidator = (issuer, token) => true,
        IssuerSigningKeyRetriever = token => {
            //TODO: determine certificate based on token
            var base64 = "<base 64 signing certificate>";
            var cert = new X509Certificate2(Convert.FromBase64String(base64));
            return new SecurityKey[] {
                new X509SecurityKey(cert)
            };
        }
    },
    Notifications = new WsFederationAuthenticationNotifications {
        RedirectToIdentityProvider = notification => {
            var challenge = notification.OwinContext.Authentication.AuthenticationResponseChallenge;
            string tenantId, issuerAddress, wtrealm;
            if (challenge == null || challenge.Properties == null || !challenge.Properties.Dictionary.TryGetValue(".tenant", out tenantId)) {
                notification.Cancel();
            } else if (!TryGetTenantWsFederation(tenantId, out issuerAddress, out wtrealm)) {
                notification.Cancel();
            } else {
                notification.ProtocolMessage.IssuerAddress = issuerAddress;
                notification.ProtocolMessage.Wtrealm = wtrealm;
            }
            return Task.FromResult(0);
        }
    }
});
I would certainly welcome any feedback of anything that I could do better or more securely but this seems to work in my demo application.