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.
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(" Not before: " + Request.ClientCertificate.ValidFrom + "</BR>");
Response.Write(" 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!
Comments
Anonymous
April 10, 2009
PingBack from http://asp-net-hosting.simplynetdev.com/automate-client-certificate-one-to-one-mapping-in-iis-60-using-c/Anonymous
June 19, 2009
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?Anonymous
June 22, 2009
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.Anonymous
July 09, 2009
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.Anonymous
July 09, 2009
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.Anonymous
July 09, 2009
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.Anonymous
July 09, 2009
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?Anonymous
July 10, 2009
Yes. It's exactly what I need. Thanks.Anonymous
July 15, 2009
The comment has been removedAnonymous
July 16, 2009
Thanks so much. You are the savior!Anonymous
October 21, 2009
I implemented the same thing on our site. It's interesting to see another person's take on it. Thanks for posting the code.Anonymous
November 11, 2009
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.Anonymous
November 11, 2009
Mike, are you looking for some scripting language like VBScript etc or will a C# code be okay?Anonymous
November 11, 2009
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?Anonymous
November 11, 2009
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.Anonymous
November 12, 2009
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?Anonymous
November 12, 2009
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.Anonymous
November 12, 2009
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.Anonymous
November 19, 2009
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.Anonymous
November 19, 2009
Bill, do you see any other error message like some error code etc. may be in the browser or the event logs.Anonymous
November 19, 2009
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. BillAnonymous
December 14, 2009
Hi Saurabh, I tried to mapp client cert manually, to the user account. when I use one to one mapping, and tried to add the cert, it says, the cert file is not valid. But the certificate, when I try to open in MMC, it is OK. need help..Anonymous
December 15, 2009
Jay, how did you manually map the client cert? Check if this helps: http://blogs.msdn.com/saurabh_singh/archive/2007/04/14/how-to-setup-iis-and-ad-for-client-certificate-setup-and-authentication.aspxAnonymous
May 13, 2010
I've set up your example and it runs without generating any errors. However, I don't see that anything happens after I click the button to map. What should I see in AD if a certificate has been mapped successfully? Is there a specific account property or setting that I can query to see if the account has been mapped? Thank you for any information you provide.Anonymous
May 13, 2010
Russell, this post talks about 1-to-1 mapping within IIS. the mapping entry does not reflect in AD. As shown in the screenshot above this setting is part of IIS mmc which adds few entries in the metabase.xml file. So i don;t see a point of querying any property within AD. when you click on the button do you see any of the above entries getting added in the IIS mmc under Account mappings window? If no, then code is not working correctly in your case because of some cofiguration/settings etc...else if you see the above entry in IIS mmc it means this code worked well and has done its piece of adding an entry for 1-to-1 mapping. Let me know how it goes.Anonymous
August 02, 2010
Hi Saurabh, i really like your code to do client mapping. My requirement is to automate teh whole process of client request and issuign certificate and installing that on client. Do you know the way to automate Client Certificate Request using C# or VB.net. I came accross a code which uses CERTEnroll but am not able to use it as i am developing the application for creating client request in Windows XP which has XEnroll dll. let me know your thoughts. thanks NamitaAnonymous
August 02, 2010
Hello Namita, sorry for the late response. Not that I have seen a code which fully automates it, I may have to look elsewhere to see if i can write one myself or suggest you one. I am currently bogged down with work so may not be feasible soon.Anonymous
August 10, 2011
Hello, I hope this post finds you. I am new to C# and I need help getting this to work. When I click the button, I get this exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Runtime.InteropServices.COMException (0x800700B7): Cannot create a file when that file already exists. (Exception from HRESULT: 0x800700B7) --- End of inner exception stack trace --- at System.DirectoryServices.DirectoryEntry.Invoke(String methodName, Object[] args) at _Default.Button1_Click(Object sender, EventArgs e)An error occurred while setting up the mapping. If I delete the website in IIS 7.0 and recreate it, then clicking the button adds the mapping but the password is blank. I don't know why this is happening because I have to enter a password to authenticate. If I delete the mapping in IIS and run this app again, then I get the exception above.Anonymous
August 10, 2011
Need to revisit the code, but this is tested for IIS 6.0. It seems you are using IIS 7.0. Lot of APIs and confgiuration layout has changed in between IIS 6 and 7.Anonymous
August 10, 2011
Need to revisit the code, but this is tested for IIS 6.0. It seems you are using IIS 7.0. Lot of APIs and confgiuration layout has changed in between IIS 6 and 7.Anonymous
August 11, 2011
Thanks for the reply. At first, I had this code working on IIS 7 on a port 8888 Sharepoint site. After I deployed the same code on a port 443 Sharepoint site, the mapping stopped working. Since then, I have reinstalled IIS and Sharepoint but it hasn't helped. Which file or SQL table is the username/password/cert values stored in? I really need this client cert mapping capability. Any help is appreciated.Anonymous
August 28, 2012
How do you send a web request to get information from the website using the one to one certificate mapping?