Mapping a client certificate to an AD domain account using clientCertificateMappingAuthentication
In a web services environment, there may be a need to map a client certificate to a windows domain account. When calling the service, the caller should possess a certificate that is mapped to a domain account.
There are two ways to accomplish this:
- First, the caller certificate is mapped by IIS directly to a user account that is configured through iisClientCertificateMappingAuthentication.
- The article will focus on the second way in which the caller certificate is redirected by IIS to Active Directory to be mapped to the appropriate windows account.
Upon a successful mapping, the user account should belong to a particular Role in order to access the service method called. The Role is checked based on the returned windows account.
In order to achieve the above requirement, we will break the initial problem into two sub problems.
1- Configure the clientCertificateMappingAuthentication
2- Use PrincipalPermission or Roles.IsUserInRole to validate the user windows identity against a particular Role.
A- Prerequisite:
Environment:
===========
- One Domain Controller Server running on Windows Server 2012 VM (dcwin2012.napedc1.net)
- One IIS server hosting the WCF service running on Windows Server 2012 R2 VM (win2012r2-1.napedc1.net)
- A client MVC application running on a Windows 8.1 VM (win8vm.napedc1.net)
Users:
=====
We created two users:
- Evrard Kone (napedc1\ekone)
- Arnaud Konan (napedc1\akonan)
Certificates:
==========
We will use MakeCert tool to create the needed certificates. To learn more about this tool, refer the following articles:
https://msdn.microsoft.com/en-us/library/bfsktky3(v=vs.110).aspx
https://msdn.microsoft.com/en-us/library/ms733813(v=vs.110).aspx
Root CA:
makecert -n "CN=RootWCFCa" -r -sv RootWCFCa.pvk RootWCFCa.cer
SSL Cert
makecert -sk WcfServiceCert -iv RootWCFCa.pvk -n "CN= win2012r2-1.napedc1.net" -ic RootWCFCa.cer -sr LocalMachine -ss My –sky exchange -pe
Client Cert
makecert -sk WcfClientCert -iv RootWCFCa.pvk -n "CN= WcfPrincipalesig" -ic RootWCFCa.cer -sr CurrentUser -ss My –sky signature –pe
B- Map client cert to user napedc1\ekone:
The article below shows how to map the user account to a certificate:
https://technet.microsoft.com/en-us/library/cc736781(v=ws.10).aspx
When the CA of the certificate mapped to the domain user is a stand-alone CA, it should be added to the NTAuthCertificates store.
Otherwise, you may get the following exception:
“A certification chain processed correctly, but one of the CA certificates is not trusted by the policy provider”
To avoid the above exception, the CA certificate should be added to NTAuthCertificates as follows:
certutil -enterprise -addstore NTAuth RootWCFCa.cer
More information may be found in kb295663
C- Configuring clientCertificateMappingAuthentication in IIS:
1- Install clientCertificateMappingAuthentication from the Add Role and Features:
2- Add the service certificate to the 443 port:
3- Enable SSL in IIS
4- To configure clientCertificateMappingAuthentication, first highlight your application and double click on Configuration Editor
5- Select system.webServer/security/authentication/clientCertificateMappingAuthentication from the Section dropdown list of the Configuration Editor
6- Set the Enable property of clientCertificateMappingAuthentication to True
7- Finally Enable Anonymous Authentication on the Application. When the call reaches IIS, it passes the certificate to AD in order to map it to the right user.
The authentication falls back to Anonymous if the AD certificate mapping fails.
If Anonymous Auth is not enabled, the incoming request call will be denied at IIS level and will never reach the WCF pipeline.
WCF service configuration:
======================
****Update the Web.Config
1 <system.serviceModel>
2 <diagnostics>
3 <messageLogging logMalformedMessages="true" logMessagesAtTransportLevel="true" />
4 </diagnostics>
5 <bindings>
6 <wsHttpBinding>
7 <binding name="ws">
8 <security mode="Transport">
9 <transport clientCredentialType="Certificate" />
10 </security>
11 </binding>
12 </wsHttpBinding>
13 </bindings>
14 <services>
15 <service name="WcfPrincipalService.Service1" behaviorConfiguration="mybhv">
16 <endpoint address="" binding="wsHttpBinding" contract="WcfPrincipalService.IService1" bindingConfiguration="ws" />
17 </service>
18 </services>
19 <behaviors>
20 <serviceBehaviors>
21 <behavior name="mybhv">
22 <!-- To avoid disclosing metadata information, set the values below to false before deployment -->
23 <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
24 <serviceDebug includeExceptionDetailInFaults="true" />
25 <serviceCredentials>
26 <clientCertificate>
27 <authentication revocationMode="NoCheck" certificateValidationMode="None"
28 mapClientCertificateToWindowsAccount="true" />
29 </clientCertificate>
30 </serviceCredentials>
31 </behavior>
32 </serviceBehaviors>
33 </behaviors>
34
35 <protocolMapping>
36 <add binding="wsHttpBinding" scheme="https" />
37 </protocolMapping>
38 <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
39 </system.serviceModel>
****Implementation using Roles.IsUserInRole:
Add the following entry to the service web.config:
<system.web>
------
<roleManager enabled="true" defaultProvider="AspNetWindowsTokenRoleProvider" />
------
</system.web>
Update the calling method as follows:
1 public class Service1 : IService1
2 {
3 public string GetData(int value)
4 {
5 string winidentity = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Name;
6 string primidentity = OperationContext.Current.ServiceSecurityContext.PrimaryIdentity.Name;
7 string identitypool = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
8 var groups = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Groups;
9 StringBuilder builder = new StringBuilder();
10
11 if (String.IsNullOrEmpty(winidentity))
12 return "User is Anonymous";
13
14 if (!Roles.Provider.IsUserInRole(winidentity, "napedc1\\AwesomeG"))
15 return builder.Append("Sorry User " + winidentity + " is not part of the napedc1\\AwesomeG Role").ToString();
16
17 var grp = (from sid in groups select sid.Translate(typeof(NTAccount)).Value).ToList();
18
19 builder.AppendLine("User " + winidentity + " Belongs to the following groups: ");
20 foreach (var gname in grp)
21 {
22 builder.AppendLine(gname.ToString());
23 }
24
25 return String.Format("Primary Identity {0},\nWindows Identity {1}, \nIdentity Pool {3} and \n {2}",
26 primidentity, winidentity, builder.ToString(), identitypool);
27
28 }
29
30 }
****Implementation using PrincipalPermission:
1 public class Service1 : IService1
2 {
3 [PrincipalPermission(SecurityAction.Demand, Role = "napedc1\\AwesomeG")]
4 public string GetData(int value)
5 {
6 string winidentity = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Name;
7 string primidentity = OperationContext.Current.ServiceSecurityContext.PrimaryIdentity.Name;
8 string identitypool = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
9 var groups = OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Groups;
10 StringBuilder builder = new StringBuilder();
11
12 if (String.IsNullOrEmpty(winidentity))
13 return "User is Anonymous";
14
15 var grp = (from sid in groups select sid.Translate(typeof(NTAccount)).Value).ToList();
16
17 builder.AppendLine("User " + winidentity + " Belongs to the following groups: ");
18 foreach (var gname in grp)
19 {
20 builder.AppendLine(gname.ToString());
21 }
22
23 return String.Format("Primary Identity {0},\nWindows Identity {1}, \nIdentity Pool {3} and \n {2}",
24 primidentity, winidentity, builder.ToString(), identitypool);
25
26 }
27
28 }
When the caller’s certificate is successfully mapped to a domain account in AD, a Windows Identity of the caller is returned back. This Identity is queried against “napedc1\\AwesomeG” to ensure that the caller is authorized to access GetData function.
In case of mapping failure, the user will not be authorized to access GetData Method.
Read more about authorization here.
WCF Client configuration:
======================
****Configuration:
1 <system.serviceModel>
2 <bindings>
3 <wsHttpBinding>
4 <binding name="WSHttpBinding_IService1">
5 <security mode="Transport">
6 <transport clientCredentialType="Certificate" />
7 </security>
8 </binding>
9 </wsHttpBinding>
10 </bindings>
11 <client>
12 <endpoint address="https://win2012r2-1.napedc1.net/Test/WcfPrincipalService/Service1.svc"
13 binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1"
14 contract="ServiceWcfRef.IService1" name="WSHttpBinding_IService1" behaviorConfiguration="bhv" />
15 </client>
16 <behaviors>
17 <endpointBehaviors>
18 <behavior name="bhv">
19 <clientCredentials>
20 <clientCertificate findValue="32004b933af9459edf74da818b0e00dcfb174aaf" storeName="My" x509FindType="FindByThumbprint" storeLocation="LocalMachine"/>
21 <serviceCertificate>
22 <authentication certificateValidationMode="None"/>
23 <defaultCertificate findValue="ae46ea8e429928240b30a6afaed5fd63763b1170" storeName="My" storeLocation="LocalMachine" x509FindType="FindByThumbprint" />
24 </serviceCertificate>
25 </clientCredentials>
26 </behavior>
27 </endpointBehaviors>
28 </behaviors>
29 </system.serviceModel>