For intranet applications where the users could be authenticated against Active Directory, using WindowsCredentials, setting up security for a WCF service might not be all that difficult. It might not be difficult even to set up a WCF Service hosted by IIS and make it use the ASP.NET Roles/Providers. But what I wanted was to come up with a series of steps that allows me to secure a WCF service for internet-like applications. While it appears that there would have been 1000s of implementations on the subject where the client application provides a UserName/Password login control and then on the authenticated users would be able to work with the service.
To go back a little, this is what I want
- I have a client application which has a Login Control and the user enters Username and password. Without proper username.password combination, the service communications going forward should not be allowed. Remember Forms Authentication in ASP.NET ?? Something similar to that.
- I do not want to tie my service strongly to the host – meaning I do not want to make use of the ASP.NET Membership Provider models, though it is relatively easy to do so. So I have a console host program that serves as the WCF Service.
- For every call to an OperationContract, I do not want to read the Message Headers or add extra parameters to see the username and password. I don’t want specific logic within each operation that handles this check.
- I want the operations to be limited to those users with some kind of “Roles”. Basically, i have a set of operations that only users of a Role “X” should be able to perform; whereas there are some other operations for users with other roles.
- I don’t want my communication channel to be open and would want to prevent the users to sniff the traffic to see what is going on.
To summarize these requirements,
- I want a secure communication between client and server.
- I want to restrict access to the service unless the client sends in valid username/password.
- I want to restrict access to operations based on the roles of the calling user.
- I don’t want to deal with Windows Authentication at this moment, since I have plans to host my service on the internet in which case WindowsIdentity is not really preferred.
In this post, I would like to show the way I achieved these goals. Note that I am not qualified enough to make strong statements or give strong explanation on how the security works. The intention of this post would be to help assist developers like me who has little knowledge of WCF Security but do understand how Security works in general. I recommend you read MSDN documentation for the classes and terms I throw here and there.
While, the source code is available for download here : http://drop.io/yskic3h , here in this post I simply mention the steps that I used to achieve each of the goals mentioned above.
1. Secure Communication Channel
Used wsHttpBinding as the binding of my choice. The wsHttpBinding by default employs Windows security. We would have to change that to make use of Message security using “UserName” as clientCredentialType. All this is configured as a binding configuration.
<wsHttpBinding>
<binding name="secureBinding">
<!-- the security would be applied at Message level -->
<security mode="Message">
<message clientCredentialType="UserName"/>
</security>
</binding>
</wsHttpBinding>
Now this bindingConfiguration has to be set on the endpoint as shown
<services>
<service name="WcfService.SecureService" behaviorConfiguration="secureBehavior">
<!--
notice the bindingConfiguration, we are applying secureBinding that
was defined in the bindings section.
-->
<endpoint address="secureService"
binding="wsHttpBinding"
bindingConfiguration="secureBinding"
contract="WcfService.ISecureService"/>
<host>
<baseAddresses>
<add baseAddress="http://truebuddi:8080/" />
</baseAddresses>
</host>
<endpoint address="mex" binding="mexHttpBinding" contract="WcfService.ISecureService"/>
</service>
</services>
Now that we know the server is expecting username and password, we want a custom validator which checks this username and password combination against our custom repository of users. To do that, we would have to configure the service behavior this time. So binding ensures credentials are being passed and the service behavior validates them! ;)
<serviceBehaviors>
<behavior name="secureBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceCredentials>
<serviceCertificate
findValue="wcfSecureService"
storeLocation="LocalMachine"
storeName="My"
x509FindType="FindBySubjectName" />
<!--
Now in secureBinding (see in bindings section),
we set the Message security to use "UserName"
as ClientCredentialType. So we would like to
use a custom username password validator.
Here we specify that our custom validator should be used.
-->
<userNameAuthentication
userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="WcfService.CustomUserNamePasswordValidator, WcfService" />
</serviceCredentials>
<!--
The Custom Authorization policy is what used to verify the roles.
For a Role specified in the PrincipalPermission attribute,
IsInRole() method in the Principal that was set from the
CustomAuthorizationPolicy.Evaluate would be invoked.
-->
<serviceAuthorization principalPermissionMode='Custom'>
<authorizationPolicies>
<add policyType='WcfService.CustomAuthorizationPolicy, WcfService' />
</authorizationPolicies>
</serviceAuthorization>
</behavior>
</serviceBehaviors>
In the above configuration, using the serviceCredentials\userNameAuthentication element we specify that the userName/password are to be validated using custom validator type.
This is all configured there. While the username and password authenticates the client on the service, I think it does not really do anything to the communication channel.
To make the channel secure, we make use of certificates. In order to do this, the following steps are required to be done on the development machine so that the sample gets working :
- Using the makecert application (can run from Visual Studio Command), create and register the command for exchanging. Note that if you follow the MSDN article on creating certificates using makecert, it does not tell you about enabling the certificate such that it is suitable for key exchanging. So the command that worked for me is
makecert.exe -sr LocalMachine -ss MY -a sha1 -n CN="wcfSecureService" -sky exchange -pe -r wcfSecureService.cer
-
We specify the same certificate to be used in the service configuration file using the serviceCredentials\serviceCertificate element. See the configuration snippet shown previously. It basically says "find the certificate by subject name where subject name is 'wcfSecureService' in the certificate store on the local machine and the store would be Personal". For all this to work, note that HTTPS base address should be used. - While the first two steps takes care of the certificate on the server, the client should have some knowledge (basically the client should know the public key with which the messages would be encrypted) of the existence of the certificate. we specify that in the endpoint\identity section of the client configuration [see below]. The encodedValue can be obtained by adding service reference from Visual Studio which generates shit load of configuration on the client, just save the encodedValue and revamp your configuration file.
<client>
<endpoint address="http://truebuddi:8080/secureService" binding="wsHttpBinding"
bindingConfiguration="secureWsHttpBinding"
behaviorConfiguration="ignoreCert"
contract="SecurityDemo.ISecureService">
<identity>
<!-- Don't panic this key is wrong ;) for the sake of this post-->
<certificate encodedValue="AwAAAAEAAAAUAAAAee8O3PpkfSCfjaa3mDmkK+HLb4QgAAAAAQAAAAcCAAAwggIDMIIBcKADAgECAhDgA4A6S0Z/j0d3IFg04e9gMAkGBSsOAwIdBQAwGzEZMBcGA1UEAxMQd2NmU2VjdXJlU2VydmljZTAeFw0xMDA1MDUyMzQ4NTZaFw0zOTEyMzEyMzU5NTlaMBsxGTAXBgNVBAMTEHdjZlNlY3VyZVNlcnZpY2UwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALF7OJsZ6AV5yqSSQyne9j+xwdRLDRoVMleYg0vGvB7W7Bk5zBNbSDCbb+spJR3ykayDoZYpykyY8Q7qzvPuUPdHu7SkMVZ9Ng8B8yAq0zrD8sJwnaqTEY4a8mj8Dt86Yr0wK31aF4VSDRZaK+XDyFd5hWU8Eya+bohhixndMYwNAgMBAAGjUDBOMEwGA1UdAQRFMEOAEJRtYMFDVIgPHFrIf0LU5e+hHTAbMRkwFwYDVQQDExB3Y2ZTZWN1cmVTZXJ2aWNlghDgA4A6S0Z/j0d3IFg04e9gMAkGBSsOAwIdBQADgYEApQ+Hy6e4hV5rKRn93IMcEL3tW2tUYcj/oifGbEPRX329s3cc8QH6jYaNN8cgS5RN+6QffrkvupMSUauGsWia20WHTRI8lyb+1gvvX4NpTxZE6+sZkvIu6R/qIsC6V9pbRCHm3HRFnAoMNZmPTr5mJvzwAQZzOdXMFq0OwakJKEw=" />
</identity>
</endpoint>
</client>
You can also look at this link to get the public key in any case.
For testing purposes, you should also add a behaviorConfiguration on the client's endpoint such that certificates are not validated, once you deploy, you can remove this behavior.
<behaviors>
<endpointBehaviors>
<!-- ignore cerificates validation for testing purposes. -->
<behavior name="ignoreCert">
<clientCredentials>
<serviceCertificate>
<authentication certificateValidationMode="None" />
</serviceCertificate>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
On the client config, you should also give the same wsHttpBinding with similar behavior but with few other options added. See snippet and compare it with the binding snippet that was shown earlier for the server
<bindings>
<wsHttpBinding>
<binding name="secureWsHttpBinding">
<security mode="Message">
<message clientCredentialType="UserName"
negotiateServiceCredential="true"
establishSecurityContext="true"/>
</security>
</binding>
</wsHttpBinding>
</bindings>
With this the communication channel is secure. You might have some issues with Certificates but you should be able to use the exception messages to bing for answers online in the forums. Only other part that is left on the client end is to make sure that the client proxy used are set with Username and password. Code for the full client is shown below.
SecureServiceClient client = new SecureServiceClient();
client.ClientCredentials.UserName.UserName = "Krishna";
client.ClientCredentials.UserName.Password = "test";
User test = client.Login();
client.SafeOperationByAdmin();
2. UserName and Password Custom Validation
Implement a type that derives from UserNamePasswordValidator class. You would have to reference the System.IdentityModel.dll and if you remember, we set the custom validator in the service behavior on the service configuration file. While the code shown below does not talk to DB, it should still serve as a good example on custom username and password validation. Note that this Validate() method gets called for evevery
public class CustomUserNamePasswordValidator : UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
Console.WriteLine("Username validation started");
if (userName == "Krishna" && password == "test")
return;
throw new InvalidCredentialException("Invalid credentials passed to the service");
}
}
3. Restriction of Operations using Roles
The operations can be restricted to users of certain roles by applying a PrincipalPermission attribute on the OperationContract [see below]. The current principal would be checked to see if it is in the role specified, otherwise the operation would not allowed to be executed. Now how do we set this Principal to something? To do this, we need a CustomPrincipal which you should derive from IPrincipal. This Principal implementation has IIdentity which can be WindowsIndetity for Windows Authentication and GenericIdentity for other scenarios. Now this CustomPrincipal should be created and applied somewhere right? This is where the IAuthorizationPolicy comes into play, We should have a custom authorization policy whose Evaluate method should take care of fetching the identity and passing it to a newly created custom principal. This custom principal has to be set as the current principal. All the three code snippets : PrincipalPermission attribute on operations, CustomPrincipal and CustomAuthorizationPolicy is shown below.
///
///This authorization policy is set on the service behavior using service authorization element.
public class CustomAuthorizationPolicy : IAuthorizationPolicy
{
public bool Evaluate(EvaluationContext evaluationContext, ref object state)
{
IIdentity client = (IIdentity)(evaluationContext.Properties["Identities"] as IList)[0];
// set the custom principal
evaluationContext.Properties["Principal"] = new CustomPrincipal(client);
return true;
}
private IIdentity GetClientIdentity(EvaluationContext evaluationContext)
{
return null;
}
public System.IdentityModel.Claims.ClaimSet Issuer
{
get { throw new NotImplementedException(); }
}
public string Id
{
get { throw new NotImplementedException(); }
}
}
public class CustomPrincipal : IPrincipal
{
private IIdentity identity;
public CustomPrincipal(IIdentity identity)
{
this.identity = identity;
}
public IIdentity Identity
{
get
{
return identity;
}
}
public bool IsInRole(string role)
{
return true;
}
}
///in the WCF Service implementation
[PrincipalPermission(SecurityAction.Demand, Role = "Admin")]
public void SafeOperationByAdmin()
{
///more code
}
The newly created Authorization Policy should be configured inside the service configuration file in the serviceBehavior\serviceAuthorization as shown below.
<!--
The Custom Authorization policy is what used to verify the roles.
For a Role specified in the PrincipalPermission attribute,
IsInRole() method in the Principal that was set from the
CustomAuthorizationPolicy.Evaluate would be invoked.
-->
<serviceAuthorization principalPermissionMode='Custom'>
<authorizationPolicies>
<add policyType='WcfService.CustomAuthorizationPolicy, WcfService' />
</authorizationPolicies>
</serviceAuthorization>
To summarize, the following are the steps that should be performed to get the security working in WCF. (Message level security).
- Create and register a certificate. Configure the service configuration specifying the certificate to use. This is done in the bindingConfiguration and the binding configuration is then applied on the endpoint.
- Configure the service to make use of Message level security. Again done on the server configuration.
- Configure the client to use encodedValue for its communication which is the public key. This is done in the bindingConfiguration on the client's endpoint. For testing purposes you can make the client not validate the certificates. This is done on the end point behaviors.
- Configure the client's binding to make use of Message level security with UserName.
- The client's code should specify the username and password. To validate this information, register a custom UserNamePasswordValidator in the serviceBehavior on the server configuration.
- For roles, create a custom prinicipal and set it using a Custom Authorization policy. This authorization policy should be registered in the serviceAuthorization of the serviceBehavior in the server configuration file.
Again the code is available for download at : http://drop.io/yskic3h . Sometime, in the future, I would try to upload the code to codeplex or put it in the Windows Live Skydrive.
2 comments:
Awesome! This was really useful. Would love to know how to implement the Customer Authorisation Policy in conjunction with the Membership Provider...
The best article addressing this issue out there - Simple and effective.
I'm glad I finally found your post. I have the exact same requirements and, if you're new to security, piecing together the information from teh internet is daunting at best.
I would really like to see your sample as I start playing around with your method, but drop.io is unfortunately dead, so I hope you have teh chance to upload it somewhere else.
Cheers!
Post a Comment