Category Archives: Windows Identity Foundation

Active Federation with Windows Identity Foundation

Sometimes you may need to perform active federation with Windows Identity Foundation (for example, authenticate against an STS such as ADFS from a process such as a windows service or console application). After hours and hours of scouring the sparse documentation I constructed the solution which is listed below. Note that this implementation uses UsernameMixed (because in this scenario I have the login credentials on hand) but can be modified if need be.

The end result is you can call Login(userName, password) and when the function call completes you will have an IClaimsPrincipal set that contains all your identity information. Note that this is dependent on some configuration entries, namely:

  • CertificateIssuerName – The name of the certificate issuer
  • STSThumbprint – The thumbprint of the STS
  • STSEndpoint – The remote endpoint of the STS
  • STSRelyingPartyEndpoint – The address of the RP endpoint registered with the STS
  • DisableRemoteSTSCertificateValidation – Boolean to enable/disable certificate validation (in case you want to disable it in a local/DEV environment)

The process is threefold:

  • Create an X509CertificateValidator to validate the certificate
  • Create a IssuerNameRegistry to validate the thumbprint
  • Utilize some code to actually make the call, decrypt the SAML token, and construct an IClaimsPrincipal

The Certificate Validator:

    public class CustomCertificateValidator : X509CertificateValidator
    {
        public CustomCertificateValidator()
        {
        }

        public override void Validate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate)
        {
            string allowedIssuerKey = "CertificateIssuerName";

            if (string.IsNullOrEmpty(ConfigurationManager.AppSettings[allowedIssuerKey]))
            {
                throw new ApplicationException(string.Format("Issuer config key {0} does not exist in the configuration", allowedIssuerKey));
            }

            if (!certificate.IssuerName.Name.Equals(ConfigurationManager.AppSettings[allowedIssuerKey]))
            {
                throw new ApplicationException(string.Format("Certificate comes from issuer {0} which is not allowed.", certificate.IssuerName.Name));
            }
        }
    }

The IssuerNameRegistry:

    public class CustomTrustedIssuerNameRegistry : IssuerNameRegistry
    {
        /// <summary>
        /// 
        /// </summary>
        public CustomTrustedIssuerNameRegistry()
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="securityToken"></param>
        /// <returns></returns>
        public override string GetIssuerName(SecurityToken securityToken)
        {
            X509SecurityToken x509Token = securityToken as X509SecurityToken;
            if (x509Token != null)
            {
                string thumbprintConfigKey = "STSThumbprint";

                if (string.IsNullOrEmpty(ConfigurationManager.AppSettings[thumbprintConfigKey]))
                {
                    throw new InvalidDataException(string.Format("Thumbprint config key {0} does not exist in the configuration", thumbprintConfigKey));
                }
                
                if (String.Equals(x509Token.Certificate.Thumbprint, ConfigurationManager.AppSettings[thumbprintConfigKey]))
                {
                    return x509Token.Certificate.SubjectName.Name;
                }
            }

            throw new SecurityTokenException("Untrusted issuer.");
        }

        public override string GetWindowsIssuerName()
        {
            return base.GetWindowsIssuerName();
        }
    }

And the main code:


/// <summary>
/// Login method to be used by processes that are not run through the web and can use a membership
/// provider. This login method will attach an IClaimsPrincipal object to the current thread
/// and all new threads that will be created as part of this app domain.
/// </summary>
/// <param name="userName">User name.</param>
/// <param name="password">Password.</param>
/// <returns>Whether the login was successful.</returns>
public static bool Login(string userName, string password)
{
    // Get the previous principal (in case validation fails and we need to restore)
    IPrincipal previousPrincipal = Thread.CurrentPrincipal;
 
    try
    {
        if (System.Threading.Thread.CurrentPrincipal.Identity.IsAuthenticated)
        {
            // Logout if necessary
            Logout();
        }
 
        string endpointKey = "STSEndpoint";
        string rpKey = "STSRelyingPartyEndpoint";
        string disableCertValidationKey = "DisableRemoteSTSCertificateValidation";
 
        // The STS endpoint
        var adfsEndpoint = ConfigurationManager.AppSettings[endpointKey];
 
        // The RP endpoint
        var relyingPartyEndpoint = ConfigurationManager.AppSettings[rpKey];
 
        if (string.IsNullOrEmpty(adfsEndpoint))
        {
            throw new InvalidDataException(string.Format("Endpoint '{0}' is not defined in the configuration and cannot be resolved in Login()", endpointKey));
        }
 
        if (string.IsNullOrEmpty(rpKey))
        {
            throw new InvalidDataException(string.Format("Relying Party '{0}' is not defined in the configuration and cannot be resolved in Login()", relyingPartyEndpoint));
        }
 
        // Append Login with credentials we have here.
        adfsEndpoint = string.Concat(adfsEndpoint, "services/Trust/13/UsernameMixed/");
 
        if (!string.IsNullOrEmpty(disableCertValidationKey))
        {
            string disableRemoteSTSCertificateValidation = ConfigurationManager.AppSettings[disableCertValidationKey];
 
            bool disableSTSCertificateValidation;
 
            if (Boolean.TryParse(disableRemoteSTSCertificateValidation, out disableSTSCertificateValidation))
            {
                if (disableSTSCertificateValidation)
                {
                    // Look up the endpoint for this application
                    ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(
                        delegate
                        {
                            return true;
                        });
                }
            }
        }
 
        var factory = new WSTrustChannelFactory(
                new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
                new EndpointAddress(adfsEndpoint));
 
        // Set the credentials
        factory.Credentials.UserName.UserName = userName;
        factory.Credentials.UserName.Password = password;
 
        // Set the issuer registry to validate the thumbprint against
        FederatedAuthentication.ServiceConfiguration.IssuerNameRegistry = new CustomTrustedIssuerNameRegistry();
        FederatedAuthentication.ServiceConfiguration.CertificateValidator = new CustomCertificateValidator();
 
        var handler = FederatedAuthentication.ServiceConfiguration.SecurityTokenHandlers.Where(e => e.TokenType == typeof(Saml2SecurityToken)).FirstOrDefault();
        System.Collections.ObjectModel.Collection<Uri> uris = handler.Configuration.AudienceRestriction.AllowedAudienceUris;
 
        if (uris != null && !uris.Any(u => u.OriginalString == relyingPartyEndpoint))
        {
            uris.Add(new Uri(relyingPartyEndpoint));
        }
 
        var channel = factory.CreateChannel();
 
        var rst = new RequestSecurityToken
        {
            RequestType = RequestTypes.Issue,
            AppliesTo = new EndpointAddress(relyingPartyEndpoint),
            KeyType = KeyTypes.Bearer
        };
 
        RequestSecurityTokenResponse rstr = null;
 
        GenericXmlSecurityToken token = channel.Issue(rst, out rstr) as GenericXmlSecurityToken;
 
        DecryptAndAttachTokenToPrincipal(token);
    }
    catch (Exception ex)
    {
        if (ex is MessageSecurityException)
        {
            // User validation fails.
            // Consume the MessageSecurityException, restore the previous principal, and return false.
            Thread.CurrentPrincipal = previousPrincipal;
            return false;
        }
        else
        {
            // Bubble up any other exceptions
            throw ex;
        }
    }
 
    return true;
}
 
/// <summary>
/// Decrypts the GenericXmlSecurityToken passed in and attaches the claims identity to the
/// thread's principal as a ClaimsPrincipal
/// </summary>
/// <param name="genericXmlSecurityToken"></param>
public static void DecryptAndAttachTokenToPrincipal(GenericXmlSecurityToken genericXmlSecurityToken)
{
    var token = FederatedAuthentication.ServiceConfiguration.SecurityTokenHandlers.ReadToken(new XmlTextReader(new StringReader(genericXmlSecurityToken.TokenXml.OuterXml))); 
    var identity = FederatedAuthentication.ServiceConfiguration.SecurityTokenHandlers.ValidateToken(token).First();
 
    // Get the IClaimsPrincipal and attach it to the current thread
    IClaimsPrincipal claimsPrincipal = ClaimsPrincipal.CreateFromIdentity(identity);
    Thread.CurrentPrincipal = claimsPrincipal;
}