3
votes

I have an application that I'd like to expose to as many users as possible. To accomplish this I'm following the directions as explained here to connect my app to Azure Active Directory and a variation of these instructions to connect AAD to Azure ACS 2.0.

Azure ACS 2.0 will handle all the federated domains, and Microsoft Accounts (formerly LiveID or Passport). It will also handle Facebook, Twitter, and other OAuth services.

Azure Active directory will handle Office 365, and anyone who is syncing their corporate Active Directory to the cloud.

My home realm discovery page will issue a GET at the following URL to determine if the LiveID or AzureAD domain should be used.

https://login.microsoftonline.com/[email protected] 

or http://odc.officeapps.live.com/odc/emailhrd/getidp?hm=0&emailAddress=USER%COMPANY.com

If the user doesn't exist, I'll use Azure ACS with a federation to that company. Lacking that, the user won't be able to log in.

Now that I explained my configuration, I intend to have Windows Identity Foundation (WIF) to allow authentications from both ACS 2.0 and ADFS.

Question

  • How do I get WIF 4.5, and specifically the ValidatingIssuerNameRegistry to properly handle multiple trusts to multiple IDPs?

Below is code that comes with VS2013 when federating an application with Azure Active Directory. It responds to all federation requests and does other things that I don't understand. Any links or information on this class would be helpful

  public class DatabaseIssuerNameRegistry : ValidatingIssuerNameRegistry
  { 
    public static bool ContainsTenant(string tenantId)
    {
        using (TenantDbContext context = new TenantDbContext())
        {
            return context.Tenants
                .Where(tenant => tenant.Id == tenantId)
                .Any();
        }
    }

    public static bool ContainsKey(string thumbprint)
    {
        using (TenantDbContext context = new TenantDbContext())
        {
            return context.IssuingAuthorityKeys
                .Where(key => key.Id == thumbprint)
                .Any();
        }
    }

    public static void RefreshKeys(string metadataLocation)
    {
        IssuingAuthority issuingAuthority = ValidatingIssuerNameRegistry.GetIssuingAuthority(metadataLocation);

        bool newKeys = false;
        foreach (string thumbprint in issuingAuthority.Thumbprints)
        {
            if (!ContainsKey(thumbprint))
            {
                newKeys = true;
                break;
            }
        }

        if (newKeys)
        {
            using (TenantDbContext context = new TenantDbContext())
            {
                context.IssuingAuthorityKeys.RemoveRange(context.IssuingAuthorityKeys);
                foreach (string thumbprint in issuingAuthority.Thumbprints)
                {
                    context.IssuingAuthorityKeys.Add(new IssuingAuthorityKey { Id = thumbprint });
                }
                context.SaveChanges();
            }
        }
    }

    public static bool TryAddTenant(string tenantId, string signupToken)
    {
        if (!ContainsTenant(tenantId))
        {
            using (TenantDbContext context = new TenantDbContext())
            {
                SignupToken existingToken = context.SignupTokens.Where(token => token.Id == signupToken).FirstOrDefault();
                if (existingToken != null)
                {
                    context.SignupTokens.Remove(existingToken);
                    context.Tenants.Add(new Tenant { Id = tenantId });
                    context.SaveChanges();
                    return true;
                }
            }
        }

        return false;
    }

    public static void AddSignupToken(string signupToken, DateTimeOffset expirationTime)
    {
        using (TenantDbContext context = new TenantDbContext())
        {
            context.SignupTokens.Add(new SignupToken
            {
                Id = signupToken,
                ExpirationDate = expirationTime
            });
            context.SaveChanges();
        }
    }

    public static void CleanUpExpiredSignupTokens()
    {
        DateTimeOffset now = DateTimeOffset.UtcNow;
        using (TenantDbContext context = new TenantDbContext())
        {
            IQueryable<SignupToken> tokensToRemove = context.SignupTokens.Where(token => token.ExpirationDate <= now);
            if (tokensToRemove.Any())
            {
                context.SignupTokens.RemoveRange(tokensToRemove);
                context.SaveChanges();
            }
        }
    }

    protected override bool IsThumbprintValid(string thumbprint, string issuer)
    {
        string issuerID = issuer.TrimEnd('/').Split('/').Last();

        return ContainsTenant(issuerID) &&
            ContainsKey(thumbprint);
    }
}
1
Your "as explained here" link is missing. I think that's kind of important. :-) - Jaxidian
@Jaxidian - Fixed it! - halfbit
A clarifying question: You seem to indicate that hitting Active Directory will not be behind ACS. Is there a reason for this or am I misunderstanding your plans? If it were me, I'd user ACS to broker everything, including AD, so your app only has one system to talk to. (Ping me on Twitter if you want me to respond more quickly: @Jaxidian ) - Jaxidian
@Jaxidian Good question. Does an Office365 / Azure AD SSO sign in flow change if ACS is used VS regular O365/AAD sign in? I have the multi-tenant domains "eventvwr.com" and "perfmon.com" and I want MSFT geeks to sign into those domains as easily as possible. - halfbit
It works like this: 1) Browser requests page from app. 2) App fails the user as unidentified. 3) Passive Federation redirects user to ACS. 4) You should have a live token, if logged in, with ACS that will allow you to get a token specific for your app; otherwise the user logs in via ACS's brokering system to third-party STS/equiv to provide ACS a trusted token to then allow user to get a token for your app. 5) Via Passive Federation, browser is redirected to your app. 6) Repeat at step 1 but, since you are authenticated, skip all other steps & use the app! - Does that answer your question? - Jaxidian

1 Answers

1
votes

Vittorio Bertocci does a good job of explaining the DatabaseIssuerNameRegistry in this post.

VS2013 RTM, Organizational Accounts and Publishing to Windows Azure Web Sites!

The bottom line is that DatabaseIssuerNameRegistry is just an Entity Framework based ValidatingIssuerNameRegistry that looks up the issuer name from a database using the the thumbprint of the token and verifies that it matches the configured value for the Issuer name, as opposed to using the web.config.It is more flexible and handles updating thumbprints if/when the authority changes them.