Quantcast
Channel: piotrwalat.net
Viewing all articles
Browse latest Browse all 15

Client certificate authentication in ASP.NET Web API and Windows Store apps

$
0
0

SSL over HTTPS provides a mechanism for mutual server-client authentication. This can be used as an alternative to more commonly used username/password based approach. In this post I am going to show how to set up client certificate authentication in ASP.NET Web API application and how to use delegating handlers to provide custom logic that handles certificates and allows to introduce arbitrary authentication mechanism (eg. role based authentication). I will also show how to import client certificates into XAML Windows Store app and how to use it to authenticate to a HTTP service.

You can skip next step if you already have certificates and do not need to create self-signed surrogates.

Generating certificates

In order to issue client certificate we will need to create a Certificate Authority (CA) using a self-signed certificate. This will be also used to create server certificate that is imported into IIS (makecert should be available in VS command prompt).

makecert -r -pe -n "CN=Awesome CA" -ss CA -a sha1 -sky signature -cy authority -sv AwesomeCA.pvk AwesomeCA.cer  
makecert -pe -n "CN=127.0.0.1" -a sha1 -sky exchange -eku 1.3.6.1.5.5.7.3.1 -ic AwesomeCA.cer -iv AwesomeCA.pvk -sp "Microsoft RSA SChannel Cryptographic Provider"  -sy 12 -sv LocalServer.pvk LocalServer.cer  
pvk2pfx -pvk LocalServer.pvk -spc LocalServer.cer -pfx LocalServer.pfx

After running these commands and entering password for several times, you should end up with a couple of certificate files. Now you will need to tell your computer to trust the newly created CA (it is generally a good idea to remove that trust after you are finished with testing). To do that start mmc.exe as an Administrator Add/Remove Snap-In (Ctrl+M), and when prompted with certificate store option choose Computer account. Under Certificates (Local Computer) choose AllTasks and Import AwesomeCA.cer. It should become visible in the list along other Trusted Root CAs.

Setting up IIS

The next step consists of setting up SSL in IIS. I am using Windows 8 that runs IIS 8, but instructions for 7/7.5 should be very similar/the same.

Open IIS Manager and go to Server Certificates panel. Then click Import.. and ... import your LocalServer.pfx.

Then under Default Web Site go to Bindings and make sure that the https binding is properly set up (if it doesn't exist, create it) and that newly created certificate is mapped to that binding.

Now create a basic ASP.NET Web API application - the template provides ValuesController by default. Make sure that the user that you are running Visual Studio as has sufficient permissions to create new virtual directories in IIS.

Go to Project properties and make sure you use IIS as server (not IIS Express), also use https:// instead of http:// for Project Url option, create virtual directory if necessary. After you do this go back to IIS Manager and under SSL Settings for a newly create virtual directory check 'Require SSL' and 'Require client certificates'. I am using 127.0.0.1 as host address here, but in real life you probably would use a domain name and not an IP address. Remember that the certificate has been issued for that particular name (for example localhost and not 127.0.0.1).

Now when you go to the newly created ASP.NET Web API service using your web browser you will see 403 error, this is because the application requires client to present a SSL certificate and the browser does not have one.

 

Let's create client cert signed by our CAs:

makecert -pe -n "CN=piotr@piotrwalat.net" -a sha1 -sky exchange -eku 1.3.6.1.5.5.7.3.2 -ic AwesomeCA.cer -iv AwesomeCA.pvk -sv Client.pvk Client.cer  
pvk2pfx -pvk Client.pvk -spc Client.cer -pfx Client.pfx -po PASSWORD

You should end up with Client.pfx certificate that can be imported to your users store (and later on to Windows 8 app certificate store). Import the file (make sure to mark is as exportable - we will need this later) and navigate to /api/values endpoint - this time you should see the data.

Client certificates in Windows 8

Now, lets move on and create a sample Windows 8 XAML app that will consume the service. Windows 8 apps run in sand-boxed environment - this also means that they get their own certificate stores. By enabling Shared User Certificates capability in Package.appxmanifest you allow app to reach for certificates outside of its store, which is useful for example when dealing with smartcards.

You can include certificates that should ship with your app in Certificates declaration inside of Package.appxmanifest. This is an xml file so you can either edit the source directly or use the designer that ships with VS 2012. Copy over CA .cer file to the project folder and add it to Certificates declaration, use "Root" as store name.

  <Capabilities>  
    <Capability Name="sharedUserCertificates" />
    <Capability Name="enterpriseAuthentication" />
    <Capability Name="privateNetworkClientServer" />
    <Capability Name="internetClient" />
  </Capabilities>
  <Extensions>
    <Extension Category="windows.certificates">
      <Certificates>
        <Certificate StoreName="Root" Content="AwesomeCA.cer" />
        <SelectionCriteria AutoSelect="true" />
      </Certificates>
    </Extension>
  </Extensions>

 

There is a subtle bug in the way that windows 8 xaml apps handle certificates that we need to apply workaround for (jpsanders mentions it here). Make sure to add the following certigicate policy OID to the cleint cert (i am using mmc.exe + cert snap-in to do that).

Also make sure that Client authentication is selected as one of certificate purposes. Export the certificate as .pfx and copy it to the project directory.

Here is the code that can be used to import the file to app certificate storage.

StorageFolder packageLocation = Windows.ApplicationModel.Package.Current.InstalledLocation;  
StorageFolder certificateFolder = await packageLocation.GetFolderAsync("Certificates");  
StorageFile certificate = await certificateFolder.GetFileAsync("Client.pfx");

IBuffer buffer = await Windows.Storage.FileIO.ReadBufferAsync(certificate);  
string encodedString = Windows.Security.Cryptography.CryptographicBuffer.EncodeToBase64String(buffer);

await CertificateEnrollmentManager.ImportPfxDataAsync(  
    encodedString,
    "PASSWORD",
    ExportOption.NotExportable,
    KeyProtectionLevel.NoConsent,
    InstallOptions.None,
    "Client certificate");

In order for HttpClient to be able to use the certificate, we need to create an instance of HttpClientHandler and tell it to pick the certificate automatically.

HttpClientHandler messageHandler = new HttpClientHandler();  
messageHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;  
HttpClient httpClient = new HttpClient(messageHandler);  
HttpResponseMessage result = await httpClient.GetAsync("https://127.0.0.1/Piotr.Win8CertAuth.Api/api/values");

Once you run this code you should be able to successfully connect to ASP.NET Web API service and retrieve the data.

 

Adding delegating handler

So far the mechanism wasn't really ASP.NET Web API specific and would have really worked in any ASP.NET application. It is also pretty basic, without any logic to really extend certificate validation or provide any kind of certificate-to-user mapping.

So how do we actually retrieve the certificate in ASP.NET Web API? Actually it turns out that's really easy as there is HttpRequestMessage.GetClientCertificate(); extension method that returns certificate object. Let's create a delegating handler that will intercept the request and inject certificate related logic into the pipeline.

public interface IValidateCertificates  
{
    bool IsValid(X509Certificate2 certificate);
    IPrincipal GetPrincipal(X509Certificate2 certificate2);
}

public class BasicCertificateValidator : IValidateCertificates  
{
    public bool IsValid(X509Certificate2 certificate)
    {
        return certificate.Issuer == "CN=Awesome CA"
               && certificate.GetCertHashString() == "B04AED3DA6CB4BD2F817EE2C726183C00035F4C6";
        //make a better check here (eg. against mapping, verify the chain etc)
    }

    public IPrincipal GetPrincipal(X509Certificate2 certificate2)
    {
        return new GenericPrincipal(
            new GenericIdentity(certificate2.Subject), new[] { "User" });
    }
}

public class CertificateAuthHandler : DelegatingHandler  
{
    public IValidateCertificates CertificateValidator { get; set; }

    public CertificateAuthHandler()
    {
        CertificateValidator = new BasicCertificateValidator();
    }

    protected override System.Threading.Tasks.Task<HttpResponseMessage>
        SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        X509Certificate2 certificate = request.GetClientCertificate();
        if (certificate == null || !CertificateValidator.IsValid(certificate))
        {
            return Task<HttpResponseMessage>.Factory.StartNew(
                () => request.CreateResponse(HttpStatusCode.Unauthorized));

        }
        Thread.CurrentPrincipal = CertificateValidator.GetPrincipal(certificate);
        return base.SendAsync(request, cancellationToken);
    }
}

Now, we can add the handler instance to global configuration object.

GlobalConfiguration.Configuration.MessageHandlers.Add( new CertificateAuthHandler());

From now on for every user that has a valid client certificate we will create IPrincipal object and assign it to current thread. This means that you can use Authorize attribute to provide more granular authorization to your services.

[Authorize(Roles = "User")]  
public IEnumerable<string> Get()  
{
    return new string[] { "value1", "value2" };
}

Hope you find this post helpful (source code coming soon).

 


Viewing all articles
Browse latest Browse all 15

Trending Articles