Foundations
Declarative WCF Security
Juval Lowy
Code download available at: Foundations 2007_08.exe(231 KB)
Contents
Security Scenarios
Intranet Application
Internet Application
Business-to-Business Application
Anonymous Application
No Security
The Declarative Security Framework
Implementing SecurityBehaviorAttribute
Client-Side Declarative Security
SecurityHelper and SecureClientBase<T>
What It All Means
Security is by far the most intricate area of the Windows® Communication Foundation (WCF). In every WCF operation call on the service side, security is governed by the service contract, the operation contract, the fault contract (if present), the service behavior, the operation behavior, the host configuration, and the method configuration and its code. Each of these items may have as many as a dozen or more security-related properties, as illustrated in Figure 1, depicting the security properties of ServiceHostBase (the base of ServiceHost).
Figure 1** Security-Related Properties of ServiceHostBase **(Click the image for a larger view)
Not only are there a daunting number of details to master, there are also complicated relationships between the various parts, producing an overwhelming number of possible permutations. Further complicating matters, not all combinations are allowed or supported, and not all of the allowed combinations are consistent or make sense.
It’s not surprising that the programming model is very complex and there are severe implications, both at the application level and at the business level, for getting things wrong. This is the result of the underlying complexity of the objective—enabling secure communication across vendor, trust, platform, and technology boundaries. It’s not a trivial task by any stretch. But unlike the other aspects of WCF, such as transactions, synchronization, and instancing, security doesn’t have attributes or a single one-stop way to do configuration. Out of the box, developers simply have to master the raw security model.
There is good reason for not including a high-level programming model for security: any such model could not possibly address all applications and scenarios, yet it would have been unwise to leave out any applications as security is important in all scenarios.
Recognizing that it’s not feasible to come up with a unified high- level security model optimized for all applications, I asked myself this question: is there a model that is good enough for most applications? Although a thorough framework that addresses all scenarios is unlikely, I wanted to create a solution that would make it easy to configure security settings for the majority of cases. In this column, I present my declarative security framework.
For the service, I’ve provided a security attribute and for the client, a few helper classes and secure proxy classes. My declarative framework makes security configuration on par with the other aspects of WCF configuration. I wanted a declarative model that would be simple to use and would minimize the need to understand the many details of security. As a developer, all you need to do is select the correct scenario and my framework will automate the configuration. In addition, my framework mandates correct options and enforces best practices. At the same time, I wanted the model to provide granularity and control of the underlying configuration in case the need for such control arises.
In this article, I focus on the target scenarios, how my framework was implemented, and how to use it. I also present some interesting WCF programming techniques that demonstrate WCF extensibility. (You can download the security framework from the MSDN® Magazine Web site.)
Security Scenarios
My framework supports five key scenarios, along with some slight variations, that address the security needs of the majority of applications today. These scenarios are:
- Intranet application
- Internet application
- Business-to-business application
- Anonymous application
- No security
If you need to address a different scenario, you can follow my approach to derive the required security aspects and settings using the accompanying source code.
Intranet Application
In the intranet application, both the client and service use WCF and both are deployed on the same intranet. No firewall separates the client and service, and you can use Windows-based security for transfer security and authentication. You can rely on Windows accounts and groups to store the client’s credentials and you can use principal-based, role-based security verifying membership against Windows groups.
For this scenario, my framework allows the use of the intranet bindings—namely, NetTcpBinding, NetNamedPipeBinding, and NetMsmqBinding. The bindings that are used are configured for the highest protection level (encrypted and signed). You can rely on Transport mode for Transfer security because the calls are invariably point-to-point. In addition, you can control the impersonation level and even request automatic impersonation for all operations.
Internet Application
In the Internet scenario, the client or service may not use WCF or even Windows. In addition, an Internet application typically has a relatively large number of clients calling the service. These client calls originate from outside the firewall. You need to rely on HTTP for transport and multiple intermediaries are likely.
In an Internet application, you typically do not want to use Windows accounts and groups for credentials. Instead, the application will need to access a custom credentials store. (That said, you could still use Windows security, as I’ll demonstrate later on.) Here, my framework uses message security for transfer security, providing security across all intermediaries. The client provides credentials in the form of username and password.
For this scenario, you should use WSHttpBinding or WSDualHttpBinding. In addition, if you have an intranet application that uses NetTcpBinding but do not wish to use Windows security for user accounts and groups, you can use message security and custom username and password in the intranet.
Since in the Internet scenario the client’s message that is sent to the service is transferred over plain HTTP, it is critical to protect the message’s content (both the client’s credentials and the body of the message) by encrypting it using a certificate provided by the service. WCF supports several options for obtaining that certificate. For instance, the client developer can obtain the service certificate using any out-of-band mechanism, such as an e-mail message or via a public Web page.
Using the WCF configuration schema, the client can include in its config file (specifically, in the endpoint behavior section) detailed information about the service certificate, such as where it’s stored on the client-side and how to find it. This is by far the most secure option from the client’s perspective because any attempt to subvert the client’s address resolution and redirect the call to a malicious service will fail since the other service will not have the correct certificate. This, however, is also the least flexible option since every time the client needs to interact with a different service, the client administrator will need to rework the client’s config file.
A reasonable alternative to explicitly referencing the certificates of all services the client may interact with is to store those certificates in the client’s Trusted People certificate folder and instruct WCF to allow calls only to a service whose certificate is in that folder. The client in that case will need to obtain the service certificate at run time as part of the initial pre-call negotiation, check to see if it is in the Trusted People store, and, if so, proceed to use it to protect the message. This form of certificate negotiation is the default behavior of the WS bindings. For the Internet scenario, I use certificate negotiation coupled with validating the service certificate in the client’s Trusted People store. This is called peer-trust validation.
Once the client’s user name and password credentials are received by the service, the host authenticates them. By default, the credentials are authenticated as Windows credentials (against the host domain or machine), but you can configure the service to use the ASP.NET membership providers.
The service can use principal-based, role-based security: if the credentials are Windows credentials, then the roles specified have to be Windows groups. When the ASP.NET providers are used, then the roles are verified using ASP.NET roles providers.
Business-to-Business Application
In the business-to-business scenario, the service and its clients are disparate business entities. They do not share credentials or accounts and the communication between them is typically closed to the public. There are relatively few clients interacting with the service and the client can only interact with the service after an elaborate business agreement and other conditions have been met.
Rather than using Windows accounts or usernames, the clients identify themselves to the service using X509 certificates, which typically are already known to the service. The client or service may not use WCF or even Windows. Client calls originate from outside the firewall, you need to rely on HTTP for transport, and multiple intermediaries are possible.
For the business-to-business scenario, my framework allows the use of the BasicHttpBinding, WSHttpBinding, or WSDualHttpBinding. You must use message security for transfer security to provide security across all intermediaries. The message will be protected using a service-side certificate, just as in the Internet scenario. However, unlike in the Internet scenario, the clients in this scenario provide credentials in the form of a certificate. The client supplies the certificate in the config file (or programmatically) to the proxy, and the proxy bundles the certificate in the message and sends it to the service.
WCF offers the service administrator a number of options for validating the certificates sent by the client. If the certificate is validated, the client is considered authenticated. My framework uses peer-trust validation on the service side so the service administrator should install all the certificates of the clients allowed to interact with the service in the Trusted People store on the service’s local machine.
When the client’s certificate is received by the service, if the certificate is found in the trusted store, the client is authenticated. By default, the service cannot employ principal-based, role-based security. This is because the credentials provided, namely the client’s certificate, do not map to either Windows or ASP.NET user accounts. Since business-to-business endpoints are often dedicated to a small set of clients or even to a particular client, this lack of authorization support may not pose a problem. If, on the other hand, you would still like to authorize the clients, my framework takes advantage of the ASP.NET roles provider for authorization, even though the membership provider was not used for authentication. This ability to use the providers separately was a core design goal for the ASP.NET provider model in WCF.
Anonymous Application
In the anonymous scenario, the clients access the service without presenting any credentials—they are unidentified. On the other hand, the clients and the service do require secure message transfer that is protected against tampering and sniffing. Both an Internet-facing and intranet-based application may need to provide for anonymous yet secure access.
The anonymous scenario can have any number of clients. The clients may connect over HTTP or over TCP. The need to secure the message and the fact that the clients may be calling over the Internet with multiple intermediaries means you should use message security. For this scenario, you can use the NetTcpBinding, WSHttpBinding, WSDualHttpBinding, and NetMsmqBinding—a mixture of both Internet and intranet bindings. Note that you cannot use the BasicHttpBinding, NetNamedPipeBinding, NetPeerTcpBinding or WSFederationHttpBinding, as those bindings either do not support message security or do not support having no credentials in the message.
Note again that no client authentication is done in the anonymous scenario and the client need not provide credentials to the proxy. For service authentication towards the client and for message protection, the service needs to provide its certificate. Since the clients are anonymous (and unauthenticated) authorization and role-based security are precluded.
No Security
In this last scenario, your application turns off security completely. The service does not rely on any transfer security and it does not authenticate or authorize its callers. The service can accept any number of clients and clients need not provide any credentials to the proxy. Since the clients are anonymous (and unauthenticated), authorization and role-based security are precluded. Both Internet and intranet services can be configured for no security, but it goes without saying that such a service is completely exposed and you generally need a business justification for relinquishing security.
Figure 2 and Figure 3 serve as a summary of the key elements of the security scenarios. Figure 2 lists the bindings used in each scenario and Figure 3 shows how each of the security aspects relates to each scenario.
Figure 3 Security Scenarios Aspects
Aspect | Intranet | Internet | Business-to-Business | Anonymous | No Security |
---|---|---|---|---|---|
Transport | Yes | No | No | No | No |
Message | No | Yes | Yes | Yes | No |
Service Authentication | Windows | Certificate | Certificate | Certificate | No |
Client Authentication | Windows | ASP.NET | Certificate | No | No |
Authorization | Windows | ASP.NET | No/ASP.NET | No | No |
Impersonation | Yes | No | No | No | No |
Figure 2 Security Scenarios and Bindings
Binding | Intranet | Internet | Business-to-Business | Anonymous | No Security |
---|---|---|---|---|---|
BasicHttpBinding | No | No | Yes | No | Yes |
NetTcpBinding | Yes | Yes | No | Yes | Yes |
NetPeerTcpBinding | No | No | No | No | Yes |
NetNamedPipeBinding | Yes | No | No | No | Yes |
WSHttpBinding | No | Yes | Yes | Yes | Yes |
WSDualHttpBinding | No | Yes | Yes | Yes | Yes |
NetMsmqBinding | Yes | No | No | Yes | Yes |
The Declarative Security Framework
Figure 4 lists the definition of SecurityBehaviorAttribute and the ServiceSecurity enum. ServiceSecurity defines the five scenarios supported by my framework.
Figure 4 SecurityBehaviorAttribute
public enum ServiceSecurity
{
None,
Anonymous,
BusinessToBusiness,
Internet,
Intranet
}
[AttributeUsage(AttributeTargets.Class)]
public class SecurityBehaviorAttribute : Attribute,IServiceBehavior
{
public SecurityBehaviorAttribute(ServiceSecurity mode);
public SecurityBehaviorAttribute(ServiceSecurity mode,
string serviceCertificateName);
public SecurityBehaviorAttribute(ServiceSecurity mode,
StoreLocation storeLocation,
StoreName storeName,
X509FindType findType,
string serviceCertificateName);
public bool ImpersonateAll {get;set;}
public string ApplicationName {get;set;}
public bool UseAspNetProviders {get;set;}
}
When applying SecurityBehaviorAttribute, you need to provide the target scenario in the form of a ServiceSecurity value. You can use just the constructors of SecurityBehaviorAttribute or you can set the properties. Unset, the properties default to reasonable values in the context of the target scenario. When selecting a scenario, the configured behavior follows to the letter my previous description of the individual scenarios.
SecurityBehaviorAttribute yields a composable security model, allowing a few permutations and sub-scenarios. When using the attribute, you can even have a security-free host config file or you can combine settings from the config file with values driven by the attribute. Much the same way, your hosting code can be free of security or you can combine programmatic host security with the attribute.
To configure a service for the intranet security scenario, apply SecurityBehavior with ServiceSecurity.Intranet, like this:
[ServiceContract]
interface IMyContract
{
[OperationContract]
void MyMethod();
}
[SecurityBehavior(ServiceSecurity.Intranet)]
class MyService : IMyContract
{
public void MyMethod() {...}
}
Even though the service contract used may not constrain the protection level, the attribute programmatically adds that demand to enforce message protection. You can use Windows NT groups for role-based security, like so:
[SecurityBehavior(ServiceSecurity.Intranet)]
class MyService : IMyContract
{
[PrincipalPermission(SecurityAction.Demand,
Role = @”<Domain>\Customer”)]
public void MyMethod() {...}
}
The service can programmatically impersonate the callers or use the operation behavior attribute for individual methods impersonation. You can also configure the service to automatically impersonate all callers in all methods via the ImpersonateAll property. ImpersonateAll defaults to false, but when set to true the attribute impersonates all callers in all operations without the need to apply any operation behavior attribute:
[SecurityBehavior(ServiceSecurity.Intranet,ImpersonateAll = true)]
class MyService : IMyContract {...}
With the Internet scenario, you need both to configure with ServiceSecurity.Internet and to select the service certificate to use. Note in Figure 4 that the ServiceBehavior attribute constructor may take the service certificate name. Unspecified, the service certificate is loaded from the host config file as with raw WCF security:
[SecurityBehavior(ServiceSecurity.Internet)]
class MyService : IMyContract {...}
You can also specify the service certificate name, in which case the specified certificate is loaded from the LocalMachine store from the My folder by name:
[SecurityBehavior(ServiceSecurity.Internet,”MyServiceCert”)]
class MyService : IMyContract {...}
If the certificate name is set to an empty string, the attribute will infer the certificate name by using the hosting machine name (or domain) for the certificate name and load the certificate from the LocalMachine store from the My folder by name:
[SecurityBehavior(ServiceSecurity.Internet,””)]
class MyService : IMyContract {...}
Finally, the attribute lets you explicitly specify the store location, the store name, and the lookup method:
[SecurityBehavior(ServiceSecurity.Internet,
StoreLocation.LocalMachine,StoreName.My,
X509FindType.FindBySubjectName,”MyServiceCert”)]
class MyService : IMyContract {...}
Note that you can combine an explicit location with an inferred certificate name, like this:
[SecurityBehavior(ServiceSecurity.Internet,
StoreLocation.LocalMachine,StoreName.My,
X509FindType.FindBySubjectName,””)]
class MyService : IMyContract {...}
Which credentials store to authenticate the client against is indicated by the UseAspNetProviders property. This property defaults to false, meaning the default is to authenticate the client’s username and password as Windows credentials:
[SecurityBehavior(ServiceSecurity.Internet,”MyServiceCert”)]
class MyService : IMyContract
{
[PrincipalPermission(
SecurityAction.Demand,Role = @”<Domain>\Customer”)]
public void MyMethod() {...}
}
You can even impersonate all callers:
[SecurityBehavior(
ServiceSecurity.Internet,”MyServiceCert”,ImpersonateAll = true)]
class MyService : IMyContract {...}
If UseAspNetProviders is set to true, the attribute will use the ASP.NET membership and role providers as prescribed for the Internet scenario:
[SecurityBehavior(
ServiceSecurity.Internet,”MyServiceCert”,UseAspNetProviders = true)]
class MyService : IMyContract
{
[PrincipalPermission(SecurityAction.Demand,Role = “Manager”)]
public void MyMethod() {...}
}
And the attribute will programmatically enable the role manager section in the config file so there is no need for anything besides endpoints in the host config file.
Next is the issue of supplying the application name for the ASP.NET providers using the ApplicationName property. When unassigned, the attribute looks up the application name from the config file as with raw WCF. If no value is found in the host config file, the attribute will not default to using the meaningless / from machine.config. Instead, it will default to the host assembly name. If the ApplicationName property is assigned a value, it will override whatever application name is present in the host config files:
[SecurityBehavior(ServiceSecurity.Internet,”MyServiceCert”,
UseAspNetProviders = true,ApplicationName = “MyApplication”)]
class MyService : IMyContract {...}
Configuring for the business-to-business scenario requires setting ServiceSecurity to ServiceSecurity.BusinessToBusiness, just as with ServiceSecurity.Internet. For example:
[SecurityBehavior(ServiceSecurity.BusinessToBusiness)]
class MyService : IMyContract {...}
[SecurityBehavior(ServiceSecurity.BusinessToBusiness,””)]
class MyService : IMyContract {...}
[SecurityBehavior(ServiceSecurity.BusinessToBusiness,”MyServiceCert”)]
class MyService : IMyContract {...}
By default, when using ServiceSecurity.BusinessToBusiness, the service cannot authorize its callers. However, setting the UseAspNetProviders property to true enables you to employ the ASP.NET role providers:
[SecurityBehavior(ServiceSecurity.BusinessToBusiness,
UseAspNetProviders = true)]
class MyService : IMyContract {...}
When using the ASP.NET role providers, the application name will be looked up and decided upon just as it would be with the ServiceSecurity.Internet setting:
[SecurityBehavior(ServiceSecurity.BusinessToBusiness,”MyServiceCert”,
UseAspNetProviders = true,ApplicationName = “MyApplication”)]
class MyService : IMyContract {...}
To allow callers according to the anonymous scenario, you need to configure the attribute with ServiceSecurity.Anonymous. Configuring the service certificate is done just as with the ServiceSecurity.Internet:
[SecurityBehavior(ServiceSecurity.Anonymous)]
class MyService : IMyContract {...}
To turn off security completely, as in the no security scenario, just provide the attribute with ServiceSecurity.None:
[SecurityBehavior(ServiceSecurity.None)]
class MyService : IMyContract {...}
Implementing SecurityBehaviorAttribute
Figure 5 is a partial listing of the implementation of SecurityBehaviorAttribute. The three public properties of the attribute correspond to three private members where the attribute saves the configured values.
Figure 5 Implementing SecurityBehaviorAttribute
[AttributeUsage(AttributeTargets.Class)]
class SecurityBehaviorAttribute : Attribute,IServiceBehavior
{
SecurityBehavior m_SecurityBehavior;
string m_ApplicationName = String.Empty;
bool m_UseAspNetProviders;
bool m_ImpersonateAll;
public SecurityBehaviorAttribute(ServiceSecurity mode)
{
m_SecurityBehavior = new SecurityBehavior(mode);
}
public SecurityBehaviorAttribute(ServiceSecurity mode,
string serviceCertificateName)
{
m_SecurityBehavior =
new SecurityBehavior(mode,serviceCertificateName);
}
public bool ImpersonateAll {get;set;} // m_ImpersonateAll
public string ApplicationName {get;set;} // m_ApplicationName
public bool UseAspNetProviders {get;set;} // m_UseAspNetProviders
void IServiceBehavior.AddBindingParameters(
ServiceDescription description,
ServiceHostBase serviceHostBase,
Collection<ServiceEndpoint> endpoints,
BindingParameterCollection parameters)
{
m_SecurityBehavior.AddBindingParameters(
description,serviceHostBase, endpoints,parameters);
}
void IServiceBehavior.Validate(ServiceDescription description,
ServiceHostBase serviceHostBase)
{
m_SecurityBehavior.UseAspNetProviders = UseAspNetProviders;
m_SecurityBehavior.ApplicationName = ApplicationName;
m_SecurityBehavior.ImpersonateAll = ImpersonateAll;
m_SecurityBehavior.Validate(description,serviceHostBase);
}
... //Rest of the implementation
}
SecurityBehaviorAttribute is a service behavior attribute so you can apply it directly on the service class. The attribute supports the WCF extension interface IServiceBehavior, defined as:
public interface IServiceBehavior
{
void AddBindingParameters(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase,
Collection<ServiceEndpoint> endpoints,
BindingParameterCollection bindingParameters);
void Validate(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase);
void ApplyDispatchBehavior(...);
}
IServiceBehavior is a special interface. If an attribute on the service class supports it, when the host is launched and later when the service is instantiated, WCF calls the various methods of IServiceBehavior, enabling the service to hook and extend WCF and affect such things as configuration of the binding, the dispatcher, and the host.
When the AddBindingParameters method of IServiceBehavior is called, SecurityBehaviorAttribute enforces the binding configuration that matches the requested scenario. The Validate method of IServiceBehavior is where SecurityBehaviorAttribute configures the host. The actual configuration is accomplished using a helper class called SecurityBehavior. In general, I recommend separating the actual code of an extension from the attribute so you can use the code in other places. For example, the code accompanying this article also contains a host class that can apply declarative security on the hosted service class, as if that class used SecurityBehaviorAttribute. In a similar way, the client-side part of my framework uses SecurityBehavior, as well. SecurityBehaviorAttribute constructs an instance of SecurityBehavior, providing it with the scenario (the mode parameter), as well as the certificate name in the matching constructor. SecurityBehavior provides systematic, meticulous, programmatic setting of all security scenarios. By doing so, it frees the config file and the hosting code from any shred of security.
SecurityBehavior is a service behavior in its own right and is designed to be used independently of the attribute. Figure 6 contains a partial listing of SecurityBehavior.
Figure 6 Implementing SecurityBehavior
class SecurityBehavior : IServiceBehavior
{
ServiceSecurity m_Mode;
StoreLocation m_StoreLocation;
StoreName m_StoreName;
X509FindType m_FindType;
string m_SubjectName;
bool m_UseAspNetProviders;
string m_ApplicationName = String.Empty;
bool m_ImpersonateAll;
public SecurityBehavior(ServiceSecurity mode) :
this(mode,StoreLocation.LocalMachine,StoreName.My,
X509FindType.FindBySubjectName,null) {}
public SecurityBehavior(ServiceSecurity mode,
StoreLocation storeLocation,StoreName storeName,
X509FindType findType,string subjectName)
{...} //Sets the corresponding members
public bool ImpersonateAll {get;set;} // m_ImpersonateAll
public bool UseAspNetProviders {get;set;} //m_UseAspNetProviders
public string ApplicationName {get;set;} // m_ApplicationName
public void Validate(ServiceDescription description,
ServiceHostBase serviceHostBase)
{
if(m_SubjectName != null)
{
switch(m_Mode)
{
case ServiceSecurity.Anonymous:
case ServiceSecurity.BusinessToBusiness:
case ServiceSecurity.Internet:
string subjectName = m_SubjectName != String.Empty ?
m_SubjectName :
description.Endpoints[0].Address.Uri.Host;
serviceHostBase.Credentials.ServiceCertificate.
SetCertificate(m_StoreLocation,m_StoreName,
m_FindType,subjectName);
break;
}
}
...
}
public void AddBindingParameters(ServiceDescription description,
ServiceHostBase serviceHostBase,
Collection<ServiceEndpoint> endpoints,
BindingParameterCollection parameters)
{
...
switch(m_Mode)
{
case ServiceSecurity.Intranet:
ConfigureIntranet(endpoints);
break;
case ServiceSecurity.Internet:
ConfigureInternet(endpoints,UseAspNetProviders);
break;
...
}
}
internal static void ConfigureInternet(
Collection<ServiceEndpoint> endpoints)
{
foreach(ServiceEndpoint endpoint in endpoints)
{
Binding binding = endpoint.Binding;
if(binding is WSHttpBinding)
{
WSHttpBinding wsBinding = (WSHttpBinding)binding;
wsBinding.Security.Mode = SecurityMode.Message;
wsBinding.Security.Message.ClientCredentialType =
MessageCredentialType.UserName;
continue;
}
...
throw new InvalidOperationException(binding.GetType() +
“is unsupprted with ServiceSecurity.Internet”);
}
}
... //Rest of the implementation
}
The SecurityBehavior constructors store their parameters, such as the security mode and the details of the certificate, in member variables. The Validate method is a decision tree that configures the host according to the scenario and the provided information, supporting the behavior of SecurityBehaviorAttribute described earlier.
AddBindingParameters calls a dedicated helper method for each scenario to configure the host’s collection of endpoints. Each helper method (such as ConfigureInternet) iterates over that collection, and for each endpoint, verifies that the binding being used matches the scenario and then configures the binding according to the scenario.
Client-Side Declarative Security
WCF does not support applying an attribute on the proxy class. Even if it did, the client may need to provide its credentials and other settings at run time. To compensate, the first step in supporting declarative security on the client side is my SecurityHelper static helper class, defined in Figure 7.
Figure 7 SecurityHelper Helper Class
public static class SecurityHelper
{
public static void UnsecuredProxy<T>(ClientBase<T> proxy)
where T : class;
public static void AnonymousProxy<T>(ClientBase<T> proxy)
where T : class;
public static void SecureProxy<T>(ClientBase<T> proxy,
string userName,string password) where T : class;
public static void SecureProxy<T>(ClientBase<T> proxy,
string domain,string userName,string password) where T : class;
public static void SecureProxy<T>(ClientBase<T> proxy,
string clientCertificateName) where T : class;
public static void SecureProxy<T>(ClientBase<T> proxy,
StoreLocation storeLocation,StoreName storeName,
X509FindType findType,string clientCertificateName)
where T : class;
... //More members
}
SecurityHelper lets you configure a plain proxy according to the desired security scenario and behavior. You can only configure the proxy before opening it. There is no need for any security settings in the client’s config file or elsewhere in the client’s code. SecurityHelper is smart and it will select the correct security behavior based on provided parameters and the method invoked. For example, here is how to secure a proxy for the intranet scenario and provide it with the client’s Windows credentials:
MyContractClient proxy = new MyContractClient();
SecurityHelper.SecureProxy(proxy,”MyDomain”,”MyUsername”,”MyPassword”);
proxy.MyMethod();
proxy.Close();
For the Internet scenario, the client only needs to provide the username and the password (remember that the determination of whether those are Windows or ASP.NET provider credentials is a service-side decision):
MyContractClient proxy = new MyContractClient();
SecurityHelper.SecureProxy(proxy,”MyUsername”,”MyPassword”);
proxy.MyMethod();
proxy.Close();
For the business-to-business scenario, the client can specify a null or an empty string for the client certificate name if it wants to use the certificate in its config file as in raw WCF or it can list the certificate name explicitly:
MyContractClient proxy = new MyContractClient();
SecurityHelper.SecureProxy(proxy,”MyClientCert”);
proxy.MyMethod();
proxy.Close();
SecurityHelper will load the certificate from the client’s LocalMachine store from the My folder by name. The client can also specify all the information required to find and load the certificate. For an anonymous client, use the AnonymousProxy method:
MyContractClient proxy = new MyContractClient();
SecurityHelper.AnonymousProxy(proxy);
proxy.MyMethod();
proxy.Close();
And for no security, use the UnsecuredProxy method:
MyContractClient proxy = new MyContractClient();
SecurityHelper.UnsecuredProxy(proxy);
proxy.MyMethod();
proxy.Close();
SecurityHelper and SecureClientBase<T>
Internally, SecurityHelper uses SecurityBehavior to configure the proxy’s endpoint, as well as to set the credentials (see Figure 8).
Figure 8 Implementing SecurityHelper
public static class SecurityHelper
{
public static void SecureProxy<T>(ClientBase<T> proxy,
string userName,string password) where T : class
{
if(proxy.State == CommunicationState.Opened)
{
throw new InvalidOperationException(
“Proxy channel is already opened”);
}
Collection<ServiceEndpoint> endpoints =
new Collection<ServiceEndpoint>();
endpoints.Add(proxy.Endpoint);
SecurityBehavior.ConfigureInternet(endpoints);
proxy.ClientCredentials.UserName.UserName = userName;
proxy.ClientCredentials.UserName.Password = password;
proxy.ClientCredentials.ServiceCertificate.Authentication.
CertificateValidationMode = X509CertificateValidationMode.PeerTrust;
}
... //Rest of the implementation
}
The advantage of using SecurityHelper is that it can operate on any proxy—even a proxy the client developer is not responsible for creating. If you are responsible for generating the proxy, you can use the SecureClientBase<T> class, defined in Figure 9.
Figure 9 SecureClientBase<T> Class
public abstract class SecureClientBase<T> : ClientBase<T> where T : class
{
//These constructors target the default endpoint
protected SecureClientBase();
protected SecureClientBase(ServiceSecurity mode);
protected SecureClientBase(string userName,string password);
protected SecureClientBase(
string domain,string userName,string password);
protected SecureClientBase(string clientCertificateName);
protected SecureClientBase(StoreLocation storeLocation,
StoreName storeName,X509FindType findType,
string clientCertificateName);
... //More constructors for other types of endpoints
}
SecureClientBase<T> derives from the conventional ClientBase<T> and adds the declarative security support. You need to derive your proxy from SecureClientBase<T> instead of ClientBase<T>, provide constructors that match your security scenario, and call the base constructors of SecureClientBase<T> with the supplied credentials and endpoint information, as shown here:
class MyContractClient : SecureClientBase<IMyContract>,IMyContract
{
public MyContractClient(ServiceSecurity mode) : base(mode) {}
public MyContractClient(string userName,string password) :
base(userName,password) {}
... // More constructors
public void MyMethod()
{
Channel.MyMethod();
}
}
Using the derived proxy is straightforward. For example, for the Internet scenario, it would look like this:
MyContractClient proxy = new MyContractClient(“MyUsername”,
”MyPassword”);
proxy.MyMethod();
proxy.Close();
For the anonymous scenario, it would look like this:
MyContractClient proxy = new MyContractClient(ServiceSecurity. Anonymous);
proxy.MyMethod();
proxy.Close();
The implementation of SecureClientBase<T> simply uses SecurityHelper as shown in Figure 10.
Figure 10 Implementing SecureClientBase<T>
public class SecureClientBase<T> : ClientBase<T> where T : class
{
protected SecureClientBase(ServiceSecurity mode)
{
switch(mode)
{
case ServiceSecurity.None:
SecurityHelper.UnsecuredProxy(this);
break;
case ServiceSecurity.Anonymous:
SecurityHelper.AnonymousProxy(this);
break;
...
}
}
protected SecureClientBase(string userName,string password)
{
SecurityHelper.SecureProxy(this,userName,password);
}
... //More constructors
}
When not using a proxy class, you can use my SecureChannelFactory (included in the source code for this article), just like the WCF-provided channel factory. But with my SecureChannelFactory, you can utilize declarative security.
What It All Means
By enabling the widest possible set of interactions between clients and services, WCF security introduces a degree of complexity that is difficult to master. My aim with the declarative security framework is to eliminate that complexity without decreasing security or configuration flexibility for the supported scenarios. All you need to do is select the correct security scenario for your application and you can still have as much control as with raw WCF security. If you want details on how raw WCF security works, see the August 2006 Security Briefs column.
The tradeoff with a framework like mine is that it doesn’t cover all scenarios (federated security, for instance). But you can use my framework as a starting point for enabling declarative security and then customize it to your specific scenario.
Send your questions and comments to mmnet30@microsoft.com.
Juval Lowy is a software architect with IDesign providing WCF training and WCF architecture consulting. This article contains excerpts from his recent book Programming WCF Services (O’Reilly, 2007). He is also the Microsoft Regional Director for the Silicon Valley. Contact Juval at idesign.net.