Effectively handle Azure Active Directory or other STS keys rolling and avoid error ID4175

ID4175: The issuer of the security token was not recognized by the IssuerNameRegistry

Boom... In the beginning of February 2015, Azure Active Directory (Azure AD) switched the primary key they used to sign security tokens. If you weren't ready for this, this meant a new rushed deployment to patch your configuration with the new values.

Why is it happening?

Every security token service (STS) will occasionally change their certificates. For security reason, they retire aging certificates for new ones. Rolling the keys limits the amount of time that bad intentioned peoples have for trying their luck against a system. It can also be because the certificates are expiring.

How to handle this properly

Having the keys hardcoded in your web.config is not a viable option. Simply because for every roll of keys it forces you to redeploy your application. 

In Visual Studio 2013 or later when you create a new web project with federation in the cloud, it will generate a custom ValidatingIssuerNameRegistry named DatabaseIssuerNameRegistry.

Here is what is generated:

    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;
            bool refreshTenant = false;
            foreach (string thumbprint in issuingAuthority.Thumbprints)
            {
                if (!ContainsKey(thumbprint))
                {
                    newKeys = true;
                    refreshTenant = true;
                    break;
                }
            }

            foreach (string issuer in issuingAuthority.Issuers)
            {
                if (!ContainsTenant(GetIssuerId(issuer)))
                {
                    refreshTenant = true;
                    break;
                }
            }

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

                    if (refreshTenant)
                    {
                        foreach (string issuer in issuingAuthority.Issuers)
                        {
                            string issuerId = GetIssuerId(issuer);
                            if (!ContainsTenant(issuerId))
                            {
                                context.Tenants.Add(new Tenant { Id = issuerId });
                            }
                        }
                    }
                    context.SaveChanges();
                }
            }
        }

        private static string GetIssuerId(string issuer)
        {
            return issuer.TrimEnd('/').Split('/').Last();
        }

        protected override bool IsThumbprintValid(string thumbprint, string issuer)
        {
            return ContainsTenant(GetIssuerId(issuer))
                && ContainsKey(thumbprint);
        }
    }

The magic is invoked in this method of the IdentityConfig.cs

        public static void RefreshValidationSettings()
        {
            string metadataLocation = ConfigurationManager.AppSettings["ida:FederationMetadataLocation"];
            DatabaseIssuerNameRegistry.RefreshKeys(metadataLocation);
        }

In short, the RefreshKeys method will download the metadata document. If it detects a changes with the configuration that is stored in his local database, it will persists the new certificates thumbprints with other informations to the local database.

This ensure that your STS can roll his keys whenever it needs and that your application be able to take the changes adequately.

See Also

MSDN: Important Information About Signing Key Rollover in Azure AD

Now let those keys roll!

I am the proud father of two little gems. A beer & wine enthusiasm. For everything else, I work and play with Azure at day, and I am an Azure MVP & Advisor at night.
Montreal, Canada