Saturday, 27 October 2012

Loading a Certificate from the Certificate Store via a Custom Configuration Section

I have recently been doing a fair amount of work with Windows Identity Foundation (WIF). In doing so I have had to load up certificates so in order to make the application flexible enough to deploy to different environments, use different certificates and follow certain standards I wanted to load the certificates from the Windows Certificate Store.

I knew that some of the other frameworks in the core libraries, such as WCF, load certificates out of the certificate stores via configuration so I wanted to emulate how they did that. After looking through some of the classes in the libraries and reflecting over the code and borrowing code generated when creating a WIF STS reference website I came up with the following that uses the CertificateReferenceElement.

interface IMySecurityConfiguration
{
    X509Certificate2 RequiredCertificate { get; } 
    X509Certificate2 OptionalCertificate { get; }
}

class MySecurityConfigurationSection : ConfigurationSection, IMySecurityConfiguration
{
    private const string ELEMENT_OPTIONALCERTIFICATE = "optionalCertificate";
    private const string ELEMENT_REQUIREDCERTIFICATE = "requiredCertificate";

    private ConfigurationPropertyCollection properties;

    protected override ConfigurationPropertyCollection Properties
    {
        get
        {
            return properties ?? (properties = new ConfigurationPropertyCollection
                                {
                                    new ConfigurationProperty(ELEMENT_REQUIREDCERTIFICATE,
                                                  typeof (CertificateReferenceElement), null,
                                                  ConfigurationPropertyOptions.IsRequired),
                                    new ConfigurationProperty(ELEMENT_OPTIONALCERTIFICATE,
                                                  typeof (CertificateReferenceElement), null,
                                                  ConfigurationPropertyOptions.None)
                                });
        }
    }

    private CertificateReferenceElement RequiredCertificateReference
    {
        get { return (CertificateReferenceElement) this[ELEMENT_REQUIREDCERTIFICATE]; }
    }

    private CertificateReferenceElement OptionalCertificateReference
    {
        get { return (CertificateReferenceElement) this[ELEMENT_OPTIONALCERTIFICATE]; }
    }

    public X509Certificate2 RequiredCertificate
    {
        get { return RequiredCertificateReference.LocateCertificate(); }
    }

    public X509Certificate2 OptionalCertificate
    {
        get
        {
            return OptionalCertificateReference.ElementInformation.IsPresent ?
                   OptionalCertificateReference.LocateCertificate() : null;
        }
    }
}

static class CertificateConfigurationExtensions
{
    public static X509Certificate2 LocateCertificate(this CertificateReferenceElement element)
    {
        return CertificateUtil.GetCertificate(element.StoreName, element.StoreLocation, element.X509FindType, element.FindValue, true);
    }
}

static class CertificateUtil
{
    public static X509Certificate2 GetCertificate(
        StoreName storeName, StoreLocation storeLocation, X509FindType findType, object findValue, bool throwIfMultipleOrNoMatch)
    {
        var certificateStore = new X509Store(storeName, storeLocation);
        X509Certificate2Collection certificates = null;
        try
        {
            certificateStore.Open(OpenFlags.ReadOnly);
            certificates = certificateStore.Certificates.Find(findType, findValue, false);
            if (certificates.Count == 1)
            {
                return new X509Certificate2(certificates[0]);
            }
            if (throwIfMultipleOrNoMatch)
            {
                if (certificates.Count == 0)
                {
                    throw new InvalidOperationException(
                        string.Format(
                            "Cannot find certificate: StoreName = '{0}', StoreLocation = '{1}', FindType = '{2}', FindValue - '{3}'",
                            storeName, storeLocation, findType, findValue));
                }
                else
                {
                    throw new InvalidOperationException(
                        string.Format(
                            "Found multiple certificates for:  StoreName = '{0}', StoreLocation = '{1}', FindType = '{2}', FindValue - '{3}'",
                            storeName, storeLocation, findType, findValue));
                }
            }
            else return null;
        }
        finally
        {
            if (certificates != null)
            {
                foreach (X509Certificate2 certificate in certificates)
                {
                    certificate.Reset();
                }
            }
            certificateStore.Close();
        }
    }
}

Using the configuration section

Then you would load up the config as follows:

<mysecurityconfiguration>
    <requiredcertificate findvalue="CN=foo" />
</mysecurityconfiguration>

For a full list of the properties have a look at the certificateReference element.

A note about loading a Private Key from the Certificate Store

In order to load a certificate with a private key you need to give the user the application is going to run as permission to load the private key. For more details see this blog post How to give IIS access to private keys.

Monday, 15 October 2012

ASP.NET Desktop Membership Manager Application

Last month I started up an open source project on CodePlex with a couple of developer friends. The application is for helping developers and system administrators to release ASP.Net web projects to a given environment where they have no ability to manage the ASP.Net Membership accounts. Either because the web application does not have user management yet or there is no need for membership management in the application.

The project is still in Beta but the most functional version so far has been released as a ClickOnce application and allows you to administer users and roles. It needs work on providing full support for all the membership options such as forgotten password questions and support for more complex profile properties but it has already been useful to me for creating roles and users even in its limited capacity.

There are other projects out there that try to address the pain of ASP.Net membership but they are either incomplete or web based solutions. Additionally all the desktop solutions require your to copy your web applications Membership, Role and Profile configuration into the applications configuration file before you start it up. This application addresses that by allowing you to load the configuration up at runtime.

If this is something that you would find useful I would love to get some feedback either here or on the project website, mostly I would be interested in how it works (or doesn’t work) with your configuration and what additional features would be useful.

ASP.Net Membership Manager on CodePlex

Thursday, 11 October 2012

Migrating Authentication out of your ASP.Net applications into a Single Sign-on Secure Token Service

It happens every time we create a new ASP.Net application we talk about doing something better than using ASP.Net membership but it never happens and before you know it your live using the SqlMembershipProvider database. Now you are trapped because removing it would be a big change and the business requires you to focus on more important features.

BUT that is not true, this post will go through the steps required to separate your authentication from your application whilst maintaining your original membership database giving you a Single Sign-On (SSO) Security Token Service (STS). The additional benefits of this mean that you can use the same STS for all your applications and focus your efforts on writing websites, not having to re write authentication and user management code.

I plan to in a later post show how you can use Azures Access Control Service (ACS) to separate your applications from a singe STS and allow you the flexibility of introducing additional STSs without having to change your applications.

Installing Windows Identity Foundation (WIF)

The first step is to pull out the authentication process from your application and move it to it’s own web application. There are a number of ways to do this but the easiest way is to install the Windows Identity Foundation (WIF) Framework and SDK which will plug-in to Visual Studio. This will provide some UI tools which will make things easier as well as providing a number of assembly libraries which will be used to create and consume the security token. See the download links at the end of the article.

Creating your ASP.Net STS

To start off we need to create a website that will be our STS. The STS takes over the role of authentication so that your website can concentrate on doing what it should do. In this example I am going to create a MVC4 website but if you are not bothered you can get Visual Studio to create you a dummy asp.net project, which we will do later anyway, and just use that.

Create a new ASP.Net MVC4 Internet Web Site project

Create a new Asp.Net MVC4 Internet Website project and add it to your solution, if you select blank website it will not create the AccountController for you and you will have to do this by hand. My examples below assume you are using the Razor view engine but feel free to use the view engine of your choice and adapt the code appropriately.

Change the login post action in the AccountController to do the following:
if (Membership.Provider.ValidateUser(model.UserName, model.Password))
{
    FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
       return Redirect(returnUrl);
}
Create a Custom Security Token service for your domain

The easiest way to do this is to get Visual Studio to generate the boiler plate code for you and then change it as you see fit. Most of my changes involved separating out the components so that I could do DI via Windsor.

  1. To generate the code first make sure you comment out the  “system.serviceModel” configuration section from your web config otherwise Visual Studio will think it is a WCF project.
  2. Right click on your website and select “Add STS reference…” as mentioned previously. Enter the Uri to the root of your website including the port number.
  3. Click “Next” and then click “Yes” if prompted.
  4. Select “Create a new STS project in the current solution”.
  5. Click “Next” and then “Finish”
  6. Go to the newly created project and copy everything out of the projects App_Code folder into your STS Website in an appropriate place.
  7. Delete the generated STS Website
  8. Edit the copied CustomSecurityTokenService and change the GetOutputClaimsIdentity method to the following which will add your users roles as claims to the generated token.
protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope )
{
    var outputIdentity = new ClaimsIdentity();
    var user = Membership.Provider.GetUser(principal.Identity.Name, true);
    outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Name, user.Username ) );

    foreach (var role in user.Roles)
    {
        outputIdentity.Claims.Add( new Claim( Microsoft.IdentityModel.Claims.ClaimTypes.Role, role ) );    
    }
    return outputIdentity;
}
Wire up the CustomSecurityTokenService in the Web.config

Add the following to your STS Website’s web.Config

<appSettings>
    <add key="IssuerName" value="PassiveSigninSTS"/>
    <add key="SigningCertificateName" value="CN=STSTestCert"/>
    <add key="EncryptingCertificateName" value=""/>
</appSettings>

<system.web>
    <compilation debug="true" targetFramework="4.0">
        <assemblies>
            <add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        </assemblies>
    </compilation>
</system.web>
Use the CustomSecurityTokenService to generate a Token that can be posted back to the Relying website

Create a Token action on the AccountController with a TokenModel and view that uses the CustomSecurityTokenService that presents a Token to a view which is then posted back to the calling website via Javascript.

public class TokenModel
{
    public string Wa { get; set; }
    public SignInResponseMessage Response { get; set; }
}

public ActionResult Token(TokenModel tokenModel)
{
    if (tokenModel.Wa == WSFederationConstants.Actions.SignIn)
    {
        var securityTokenService = new CustomSecurityTokenService(new CustomSecurityTokenServiceConfiguration());

        var requestMessage = (SignInRequestMessage)WSFederationMessage.CreateFromUri( Request.Url );

        SignInResponseMessage responseMessage = FederatedPassiveSecurityTokenServiceOperations
            .ProcessSignInRequest( requestMessage, User, securityTokenService )

        tokenModel.Response = responseMessage;

        return View(tokenModel);
    }
    if (tokenModel.Wa == WSFederationConstants.Actions.SignOut)
    {
        SignOutRequestMessage signoutRequest = WSFederationMessage.CreateFromUri( Request.Url );

        try
        {
            FormsAuthentication.SignOut();
        }
        finally
        {
            SessionAuthenticationModule authenticationModule = FederatedAuthentication.SessionAuthenticationModule;
            if (authenticationModule != null)
            {
                authenticationModule.DeleteSessionTokenCookie();
            }
        }

        if (!string.IsNullOrWhiteSpace(signoutRequest.Reply))
        {
            return Redirect(signoutRequest.Reply);
        }
    }
    return null;
}
@model <Your namespace>.TokenModel
@{
    ViewBag.Title = "Token";
}
@using (Html.BeginForm(null, null, FormMethod.Post, new { @action = Model.Response.BaseUri }))
{
    <p>Token.</p>
    <input type="hidden" name="wa" value="@Model.Response.Action" />
    <input type="hidden" name="wresult" value="@Model.Response.Result" />
    <input type="hidden" name="wctx" value="@Model.Response.Context" />
    
    <noscript>
        <p>JavaScript is disabled please click Submit to continue.</p>
        <input type="submit" value="Submit" />
    </noscript>
}
<script language="javascript">window.setTimeout('document.forms[0].submit()', 0);</script>

Linking the STS to your existing membership database

Replace the Membership and Role provider configuration in the STS we.config which would have been auto generated with the configuration from your websites web.config, remember to copy over the  membership database connection string.

Get your website to use the STS as an authentication service

We should now be at a point where we can now authenticate against the STS.

  1. First make your website and the STS use static ports, this will make things easier when working in a team environment.
  2. Add a reference to the Microsoft.Identity.Model
  3. Edit your websites config as per the following, you may find that most of this all done for from when we added the STS service reference but you will need to update the address to the STS service. There may also need to be some small differences depending on how complicated your web config is.
<configSections>
    <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</configSections>

<system.web>
    <httpRuntime requestValidationMode="2.0" />
    <authorization>
        <deny users="?" />
    </authorization>
    <authentication mode="None" />
    <compilation debug="true" targetFramework="4.0">
        <assemblies>
            <add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        </assemblies>
    </compilation>
       <!-- The httpModules will need to be removed when deploying to IIS7-->
    <httpModules>
        <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
        <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </httpModules>
</system.web>

<system.webServer>
    <modules>
        <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
        <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
    </modules>
</system.webServer>

<microsoft.identityModel>
    <service>
        <audienceUris>
            <add value="http://<website address and port number>/" />
        </audienceUris>
        <federatedAuthentication>
            <wsFederation passiveRedirectEnabled="true" issuer="http://<STS address and port number>/Account/Token" realm="http://<website address and port number>/" requireHttps="false" />
            <cookieHandler requireSsl="false" />
        </federatedAuthentication>
        <applicationService>
            <claimTypeRequired>
                           <!-- If you do not need roles or a name in the claim then remove the following -->
                <claimType type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
                <claimType type="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" />
            </claimTypeRequired>
        </applicationService>
        <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
            <trustedIssuers>
                           <!--
                                You will need to get the thumb print of the Certificate that was created and put in your user store
     when Visual Studio generated the STS earlier on
                                Be careful when copying the thumbprint as you may end up with an invisible funny invalid character
     at the front of the thumbprint that you will have to remove.
                           -->
                <add thumbprint="9A7465C17A42099FFB0C061F9D9A7BCBCD440908" name="http://<STS address and port number>/" />
            </trustedIssuers>
        </issuerNameRegistry>
    </service>
</microsoft.identityModel>
Change your websites logout method to do the following
FederatedAuthentication.SessionAuthenticationModule.SignOut();

WSFederationAuthenticationModule authModule = FederatedAuthentication.WSFederationAuthenticationModule;
string signoutUrl = (WSFederationAuthenticationModule
 .GetFederationPassiveSignOutUrl(authModule.Issuer, authModule.Realm, null));
return Redirect(signoutUrl);

Fire up both websites and try navigating to your website. If everything is configured correctly you should be redirected to your STS where you can authenticate using the existing membership database and you should then be redirected back to your website with a claim containing your username and roles.

Additional tasks

If we have got here then hopefully everything above worked, here are some additional task that I had to do which are specific to my domain but they are worth considering.

General clean up: The above code works, great, but it is not the prettiest and was simplified for brevity. Consider moving things around to make everything more testable and readable. I created wrappers for certain things so that I could use dependency injection.

Styling and user management: All the above does is move over the authentication to the STS, there is no user management. There is no reason why it cannot stay in the main website, this is of course a decision you will need to make.

Consider how you will deploy: You will need to consider how you are going to deploy this out to other development machines and environments. How are you going to manage certificates and such?

Development and testing: In order to facility testing and prevent the need to have both websites running in a development environment I created a Test STS that allowed me to login as a verity of different users with different claims which I then hosted on an internal server that the whole team could access. In stead of having to enter a username and password all you did was change the name of the user if you wanted and tick the roles you wanted to be in the claim.

Abstracting from a single STS using Azure’s ACS

In order to further separate  your website you can also consider using an Access Control Service (ACS) which Microsoft hosts on Azure. The service allows you to sign in from a verity of different Security Token Services and maps the claims from each of these STS Domains to claims your own domain can recognize. You could of course role your own but either of these two topics is a post in its own right.

Further Reading

MSDN: A Guide to Claims-Based Identity and Access Control (2nd Edition)

Windows Identity Foundation

Windows Identity Foundation SDK

Federated Identity with Multiple Partners


Federated Identity with Multiple Partners and Windows Azure Access Control Service

Microsoft.IdentityModel Namespace