11 April 2009

Automate client certificate one-to-one mapping in IIS 6.0 using C#

In PSS, we occasionally get requests from our customers wherein they want to automatically add entries for client certificate mapping in IIS or Active Directory (AD). That is either a 1-to-1, Many-to-1 or AD mapping for the client certificate authentication for the web site. I recommend going with AD mapping because that eases the management but it finally depends upon one's need.

I am not sure but I feel there is a security breach plus annoyance when an administrator has to laboriously enter the mappings for all the accounts/certificates (I am being specific to 1-to-1/Many-to-1 here).

The concern I feel when dealing with the administrator doing it for 1-to-1 and Many-to-1 are:

a. If there are hundreds of users you need to do this manually for everyone of those accounts and it's a pain.

b. Yes, the above can be automated using a script but then the second concern that arises is that whoever is running the script has to know the passwords for all these accounts to be mapped. I think this doesn't sound good.

I have written a sample application using which users can enter the mappings themselves in the IIS's Client certificate setting, i.e. entries having the client certificate mapped to a windows account (either a local IIS or AD account) and the corresponding password.

So this is how it works:

  • User accesses this web page from their workstation which already has the client certificate installed.
  • They are authenticated over Basic with SSL.
  • Browser sends across the client certificate as part of the HTTP web request.
  • This application gathers the user account, password plus the client certificate from the incoming HTTP web request and does the mapping in IIS.

 

image

I am adding the code here in case someone may want to extract the section for automated scripting instead of using it as a web app.

This code is also attached to this post as well.

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Security.Cryptography.X509Certificates;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.DirectoryServices;
 
/* This sample application is to automate One-to-One Client certificate mapping in IIS 6.0.
 * User should be able to access this site from the browser and select the client certificate
 * in their machine which will be mapped to their account on the IIS server for 1-to-1 mapping. 
 * You need to deploy this application on the IIS server which is hosting the website(s) which 
 * needs client certificate mapping, preferably under its own virtual directory.
 *
 * Important:
 * - Have the authentication for this web application configured to use Basic along with SSL.
 * - Have the "Accept client certificates" or "Require client certificates" selected under 
 *   <Website> -> Properties -> Directory Security -> Secure communications -> Edit -> Client certificates
 * - Ensure the website that we want the mapping for is mentioned in the web.config file associated with
 *   this application under <appSettings>
 * - In the Web.config file we are impersonating an Administrator account for this application. 
 *   <identity impersonate="true" userName="Administrator" password="myadminpassword"/>
 *   This is done because non-admin users cannot modify the IIS metabase. If you do not want users to map
 *   entries themselves through web page you can change this to <identity impersonate="true" />.
 *   In such a case only admins can add the mappings for their user accounts. Non-admins won't be able to 
 *   add the client mapping entries.
 *   This is valid for both domain or local Windows NT accounts.
 * - This app is written using .Net 2.0, ASP.Net 2.0 and above in mind. You should be able to make it work
 *   with ASP.Net 1.1 as well.
 * - You may prefer to run this application under its own dedicated application pool to ensure stability and security.
 * 
 * DISCLAIMER: The code is not tested for production scenarios so use it at your own risk. 
 *             In case one wants to use batch scripting etc or some kind of console app instead 
 *             of web app you can extract the code section from this page which should work fine for the job.
 */
 
public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
      {
        Response.Write("<B>Client Certificate One-to-One Mapping Application:</B><BR><HR>");
        Response.Write("Serial number: " + Request.ClientCertificate.SerialNumber + "</BR><HR>");
        Response.Write("Issuer: " + Request.ClientCertificate.Issuer + "</BR><HR>");
        Response.Write("Subject Name: " + Request.ClientCertificate.Subject + "</BR><HR>");
        if (Request.ClientCertificate.IsPresent)
        {
            Response.Write("Validity<BR>");
            Response.Write("&nbsp;&nbsp;&nbsp;&nbsp;Not before: " + Request.ClientCertificate.ValidFrom + "</BR>");
            Response.Write("&nbsp;&nbsp;&nbsp;&nbsp;Not after: " + Request.ClientCertificate.ValidUntil + "</BR><HR>");
        }
        else
            Response.Write("<B>There is no client certificate sent along with the request!</B><HR>");
 
        Response.Write("Authenticated User: " + Request.ServerVariables["AUTH_USER"] + "</BR><HR>");
        Response.Write("Authentication Type: " + Request.ServerVariables["AUTH_TYPE"] + "</BR><HR>");
    }
    protected void Button1_Click(object sender, EventArgs e)
    {
        string user = Request.ServerVariables["AUTH_USER"];
        string password = Request.ServerVariables["AUTH_PASSWORD"];
        string clientCertMappingName = "Mapping for " + user;  // <--- Our One-to-One Mapping name for the entry
        HttpClientCertificate cert = Request.ClientCertificate;
        /*
          If you want to map a client certificate located on the disk instead of the one as part of the 
          HTTP Web request try the code below.
          
          X509Certificate certificate = X509Certificate2.CreateFromCertFile(@"c:\cert.cer");
          X509Certificate certificate = cert.Certificate;
          byte[] certHash = certificate.GetRawCertData();
        */
        byte[] certHash = Request.ClientCertificate.Certificate;
        try
        {
        //Get the name of the Web site for which mapping has to be done from the App settings in the web.config file.
        string friendlyWebsiteName = ConfigurationManager.AppSettings["websitename"].ToString();
 
        //Get the Site Identifier based on the friendly name of the Web Site.
        string siteId = getsiteid(friendlyWebsiteName);
 
        if (siteId != null)
            {
            string sitePath = "IIS://localhost/W3SVC/" + siteId + "/IIsCertMapper";
            using (DirectoryEntry de = new DirectoryEntry(sitePath))
            {
                de.Invoke("CreateMapping", new object[] { certHash, user, password, clientCertMappingName, true });
            }
            Response.Write("Account Mapped: <B>" + Request.ServerVariables["AUTH_USER"] + "</B></BR>");
            Response.Write("Mapping Name: <B>" + "Mapping for " + Request.ServerVariables["AUTH_USER"] + "</B></BR>");
            Response.Write("Web Site: <B>" + friendlyWebsiteName + "</B></BR>");
            }
        else
            {
            Response.Write("<B>Web Site does not have a valid Site ID. Ensure we have the correct site name in the config file for this app.</B>");
            }
        }
        
        catch (System.Runtime.InteropServices.COMException)
        {
            Response.Write("A COM exception occurred while setting up the mapping.");
        }
        catch (SystemException)
        {
            Response.Write("An error occurred while setting up the mapping.");
        }
        catch (Exception)
        {
            Response.Write("An error occurred while setting up the mapping.");
        }
       
    }
 
    public string getsiteid(string websitename)
    {
        DirectoryEntry root = new DirectoryEntry("IIS://localhost/W3SVC");
        try
        {
            string siteid = null;
            foreach (DirectoryEntry de in root.Children)
            {
                if (de.SchemaClassName == "IIsWebServer")
                {
                    if (websitename.ToUpper() == de.Properties["ServerComment"].Value.ToString().ToUpper())
                        siteid = de.Name;
                }
            }
            if (siteid == null) return null;
            return siteid;
        }
        catch
        {
            return null;
        }
        finally
        {
            root.Close();
        }
    }
}

 

Ciao

Nice weekend!

Attachment(s):Code.zip
 

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

# Automate client certificate one-to-one mapping in IIS 6.0 using C# | ASP NET Hosting said:

PingBack from http://asp-net-hosting.simplynetdev.com/automate-client-certificate-one-to-one-mapping-in-iis-60-using-c/

10 April 09 at 8:57 PM
# Brad said:

How do you get the client certificate?  When I goto my dev site where your code is, it says no client certificate was sent.  So, how do I force my client certificate to be sent?

19 June 09 at 11:06 AM
# Saurabh Singh said:

Brad, this means for some reason your clients are unable to send client certificates to the server. Please check my other posts around issues that may casue client certs not to be sent. You can search by "client certificate" tag.

22 June 09 at 1:38 PM
# Tin said:

Hi Saurabh,

It's a very helpful article. Since new users don't have AD account, I can't use automapping. How do I import the public certifcate file (.cer) into specific folder in IIS? (the certificate file should be imported to IIS automatically when user requests a new account. then admin will create an AD account and map the cert in IIS maually) Thanks for your help.

09 July 09 at 10:30 AM
# Saurabh Singh said:

Tin,

I am not sure if I got the requirement correctly.

For mapping the client cert to a specific account you need to have that .cer file imported/copied locally to the IIS server. If you have access to the .cer file you can manually copy it to the IIS server and once copied you can do the mapping.

Let me know if this answers your questions or if you were looking for some automated way of doing all this.

09 July 09 at 12:50 PM
# Tin said:

Hi Saurabh,

Thanks for your quick response. I do not have access to user's .cer file. I have a (SharePoint) new user request form where I can check the validity of user's CAC. Is there a way to import/copy into a temp folder in IIS server by script (when user submit the request)?

Once the .cer file is in IIS, I can then create a new AD account and map the new account with the imported .cer file.

Thanks.

09 July 09 at 6:28 PM
# Saurabh Singh said:

Tin,

Just wnat to confirm your requirement:

-- User is sending HTTP request to the IIS web app which requires client certificate as part of the web request.

-- Client Certificate is being sent as part of the web request and during this process you want to save the .cer format of the client cert coming from the Web request on the IIS server as a .cer file.

Right?

09 July 09 at 7:06 PM
# Tin said:

Yes. It's exactly what I need. Thanks.

10 July 09 at 11:42 AM
# Saurabh Singh said:

Tin, let me know if this works out for you. It's just a sample code but you should get the gist. You can modify it accordingly.

We read the client cert from the incoming http web request and save the content in the .cer format, which you can later use for manually mapping to individual users.

========XXX========

protected void Create_Cert_Button(object sender, EventArgs e)

   {

       //Name of the Directory where the Certificate will be stored for users. The name will be based on the Login name.

       string rootPath = "C:\\";

       string directoryToPutCertIn = Request.ServerVariables["AUTH_USER"].Replace('\\', '-');

       //Get the Client certificate from the Web Request.

       byte[] certHash = Request.ClientCertificate.Certificate;

       //Convert the based64 encode byte array into string.

       string cert = Convert.ToBase64String(certHash);

       //Check for a directory per user. If not then create one.

       //here in this example certificate is stored in the form C:\domain-user\mycert.cer

       if(!Directory.Exists(rootPath + directoryToPutCertIn))

           Directory.CreateDirectory(rootPath + directoryToPutCertIn);

       StreamWriter sw = new StreamWriter(rootPath + directoryToPutCertIn + "\\mycert.cer",false, System.Text.Encoding.ASCII);

       //Begin writing the certificate into the stream.

       sw.WriteLine("-----BEGIN CERTIFICATE-----");

       sw.WriteLine(cert);

       sw.WriteLine("-----END CERTIFICATE-----");

       sw.Close();

   }

Hope this helps!

16 July 09 at 1:14 AM
# tin said:

Thanks so much. You are the savior!

16 July 09 at 4:55 PM
# Robert Eberhart said:

I implemented the same thing on our site.  It's interesting to see another person's take on it.  Thanks for posting the code.

21 October 09 at 11:25 AM
# Mike McGuffin said:

We basically do the same thing but I have one more little problem.  Is there a way to have a stand alone script to import a CER file into IIS and do a one-to-one mapping.  I have tried to get it to work using IISCertMapper but keep getting ASN.1 errors on the import.  I can open the CER file, read the Cert blob but the script reports an ASN1 error and dies.

11 November 09 at 11:18 AM
# Saurabh Singh said:

Mike, are you looking for some scripting language like VBScript etc or will a C# code be okay?

11 November 09 at 11:45 AM
# Mike McGuffin said:

I was looking at and using VBScript if possible but I have not had much luck with it so far.  Can C# be run independently without the use of something like CS-SCript?

11 November 09 at 12:07 PM
# Saurabh Singh said:

I did try a few things using VBScript and i fear it may not be as simple as it appears to be. You may have to go via pure native way using C/C++ and IIS Admin objects etc to achieve the same. I will see if i can find something on this. The issue here is that we are trying to read the cert blob from a file instead of a web request.

11 November 09 at 6:13 PM
# Mike McGuffin said:

Thanks for the input Saurabh.  I was wondering if I was doing something incorrectly but didn't think so.  I appreciate you looking at this as it has been very frustrating for me not being able to get the results I have been trying to achieve.  Do you think it is possible in C# at all?

12 November 09 at 8:02 AM
# Saurabh Singh said:

Mike, yes you should be able to achieve 1-to-1 mapping using C# code for cert from a file instead of a web request. In the above source code that I have shared i have commented out that section. I suggest you can write a windows/console app and give an option for filename on the disk and achieve the mapping for the website.

Here is the code part anyway from the above sample attached with this post.

//If you want to map a client certificate located on the disk instead of the one as part of the HTTP Web request try the code below.

       X509Certificate certificate = X509Certificate2.CreateFromCertFile(@"C:\cer.cer");

        byte[] certHash = certificate.GetRawCertData();

I am not good in VBScripting but I guess i will check with someone and see if it is possible that way too.

12 November 09 at 8:54 AM
# Mike McGuffin said:

Thanks again.  I had noticed that part of the C# code when I initially started this little project.  I am an opposite of you I suppose in that I know more VBScript than I know C#.  Currently learning VB 2008 so i will see what I can do with your code and try to make it work for me.

12 November 09 at 10:26 AM
# Bill Schuteker said:

I am a code n00b and I am trying to get this to work for our site. I have it running and get the page to display the cert data and user account info but when I click on the button to map it I am getting the ""A COM exception occurred while setting up the mapping" error. Can you please explain in a bit of detail how to track this down or what troubleshooting steps to take?

Thanks in advance.

19 November 09 at 8:23 AM
# Saurabh Singh said:

Bill, do you see any other error message like some error code etc. may be in the browser or the event logs.

19 November 09 at 4:46 PM
# bschuteker said:

Saurabh,

Thanks for the time and hard work. I didn't see any other errors and now it doesn't matter. Someone much smarter than me has re written the code in VB Script and it is working. We couldn't have done it without you.

Bill

20 November 09 at 5:53 AM

Leave a Comment

Comment Policy: No HTML allowed. URIs and line breaks are converted automatically. Your e–mail address will not show up on any public page.

(required) 
(optional)
(required) 

  
Enter Code Here: Required

About Saurabh Singh

I am a Support Escalation Engineer with Microsoft GTSC, India. I have been supporting IIS and ASP.Net. I am a Computer Engineer (B.E.), did my graduation from one of the premier Engineering institutes in India. I have been working in the IT field for over 6 years now.
Page view tracker