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!