Codeplex Project for Dynamic Controller/Action Authorization in ASP.NET MVC

For those of you who used the code in my post here: http://www.ryanmwright.com/2010/04/25/dynamic-controlleraction-authorization-in-asp-net-mvc/

I have created a project on Codeplex for it (http://mvcauthorization.codeplex.com/). Note that this version has some major breaking changes from the code I posted in my previous post but has some major improvements, the two biggest being:

  • Provider based model so that a database provider is as simple as overriding two methods on AuthorizationProvider
  • A global action filter for authorization (requires DependencyResolver)

Check it out and let me know what you think. I will be adding a post to show how to integrate it with the MvcSitemap provider soon.

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;
}

Always keep your (jQuery) Promises

jQuery Deferreds are a relatively new feature and can be invaluable when coordinating multiple events and handlers. They provide you with an elegant way to register one or more callbacks into callback queues, invoke them, and signal success/failure. Another advantage they have over traditional callbacks is that you can bind a new callback at any point in your code, even after the event has occurred. If the event has already occurred (i.e. the deferred has been resolved) your callback will happen immediately. Never heard of them before? Well, if you have written code similar to this:

$.ajax(...).done(function() { })

then you have already used them! The above is valid because the jqXHR object that gets returned implements the Promise interface as of jQuery 1.5.

Here’s a high level summary of how Deferreds work:

  1. Call $.Deferred with a callback taking the newly created deferred handler as the first parameter. Once the your operation has completed you call resolve() on the deferred handler.
  2. The deferred handler should return a promise by calling promise(). Think of a promise as a lightweight version of deferred. In essence it provides an immutable version of the deferred object, meaning it can query the state of the deferred handler, but it can’t modify it (for example, it can’t resolve it).
  3. Pass the promise around to any interested consumers who would like to be notified of the event’s completion (success or failure)

Remember, you can call promise.done(…) at any point in your application, even after the deferred handler has been resolved, in which case the callback will be invoked immediately. Another interesting feature is that you can also endow any object with an existing deferred’s promise by calling deferred.promise(objectInstance). Indeed, if you look at the jQuery framework’s code this is how they attach the promise methods to the jqXHR object.

As you might guess this can become pretty powerful as it allows us to express event chains in almost any way imaginable.

As a simple example, imagine you would like to submit two asynchronous requests and perform a fade out animation at the same time. Once all three have finished a fade in animation should be run. Deferreds along with jQuery’s $.when() provide an elegant mechanism to achieve this. Take the code below:

var container = $("#mycontainer");
$.when(
	function () {
		return $.Deferred(function (dfd) {
			container.fadeOut('slow', dfd.resolve);
		}).promise();
	}(),
	$.ajax({
		url: 'Path/To/My/Url/1',
		type: 'POST',
		dataType: 'json'
	}),
	$.ajax({
		url: 'Path/To/My/Url/2',
		type: 'POST',
		dataType: 'json'
	})
).done(function (x, data) {
	container.html('Your request has been processed!');
	container.fadeIn('slow');
});

As you can see we pass three promises to $.when, one for the fade out animation and two for the ajax operations.

  • The first parameter is a self executing anonymous function that creates the deferred handler and returns the promise. In $.Deferred’s callback the dfd.resolve function is passed in to fadeOut()’s callback parameter, which means that once the animation completes the deferred will be resolved.
  • With regards to the other two parameters we pass to $.when, since the result of $.ajax is a jqXHR object that implements Promise we just pass the value that’s returned as is.

Once all three have completed we replace the content of the container with a friendly message and fade it in. This is obviously a very bare-bones example, and we’ve only scratched the surface with what can be achieved with deferreds. One thing you’ll notice is that I haven’t included any error handling for the $.ajax calls. I’ll leave that as homework.

That’s it! Pretty neat, huh?