Section 10.7. Intranet Application


10.7. Intranet Application

The characteristics of the intranet application are that both clients and service use WCF, and that the client and the service are deployed in the same intranet. The clients reside behind the firewall, and you can use Windows-based security for transfer security, authentication, and authorization. You can rely on Windows accounts and groups to store the client's credentials. The intranet scenario addresses a wide range of business applications, from finance to manufacturing to in-house IT applications. The intranet scenario is also the richest scenario of all in the options it offers developers for configuring security. The following section on the intranet scenario will also define the terminology, techniques, and types used in the other scenarios.

10.7.1. Securing the Intranet Bindings

For the intranet scenario, you should use the intranet bindings; namely, NetTcpBinding, NetNamedPipeBinding, and NetMsmqBinding. You can rely on Transport mode for transfer security because the calls are invariably point-to-point. Conveniently, Transport security is the default transfer mode of the intranet bindings (see Table 10-1). For client credentials type, you set the transport client credentials type to Windows, which again is the default (see Table 10-2). You need to configure this on both the client and the service.

10.7.1.1. Transport security protection level

The three intranet bindings need to be configured for transport security. Each of the three intranet bindings has a configurable protection level, which is the master switch for transport protection. The three protection levels are:


None

WCF does not protect the message on transfer from the client to the service. Any malicious party can read the content of the message or even alter it.


Signed

When configured for this protection level, WCF ensures that the message could have come only from an authenticated sender by appending an encrypted checksum to the message. Upon receiving the message, the service calculates the checksum and compares it to the original. If the two do not match, the message is rejected. As a result, the message is impervious to tampering. However, the message content is still visible during the transfer.


Encrypted and Signed

When configured for this protection level, WCF both signs the message and encrypts its content. Encrypted and Signed provides integrity, privacy, and authenticity.

The Signed protection level is a clear trade-off between a measured degree of security and performance. However, I consider this to be a trade-off to avoid, so you should always opt for the Encrypted and Signed protection level. WCF represents the protection level with the ProtectionLevel enum, defined as:

 public enum ProtectionLevel {    None,    Sign,    EncryptAndSign } 

Not all Internet bindings default to the same protection level. Both NetTcpBinding and NetNamedPipeBinding default to Encrypted and Signed, yet the NetMsmqBinding defaults to Signed only.

10.7.1.2. NetTcpBinding configuration

NetTcpBinding takes a construction parameter indicating the desired transfer security mode:

 public class NetTcpBinding : ... {    public NetTcpBinding(SecurityMode securityMode);    public NetTcpSecurity Security    {get;}    //More members } 

The Security property of the type NetTcpSecurity contains the transfer modeTransport or Messageand two respective properties for the specific settings:

 public sealed class NetTcpSecurity {    public SecurityMode Mode    {get;set;}    public MessageSecurityOverTcp Message    {get;}    public TcpTransportSecurity Transport    {get;} } 

In the intranet security scenario, select Transport security for transfer security and set the values of the TRansport property of the type TcpTransportSecurity:

 public sealed class TcpTransportSecurity {    public TcpClientCredentialType ClientCredentialType    {get;set;}    public ProtectionLevel ProtectionLevel    {get;set;} } 

The transfer property should be initialized with the client credential type set to Windows using the TcpClientCredentialType enum, defined as:

 public enum TcpClientCredentialType {    None,    Windows,    Certificate } 

The transfer property should have the protection level set to ProtectionLevel.EncryptAndSign. Since both settings are the default for this binding, these two declarations are equivalent:

 NetTcpBinding binding1 = new NetTcpBinding( ); NetTcpBinding binding2 = new NetTcpBinding(SecurityMode.Transport); binding2.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows; binding2.Security.Transport.ProtectionLevel = ProtectionLevel.EncryptAndSign; 

Or alternatively, using a config file:

 <bindings>    <netTcpBinding>       <binding name = "TCPWindowsSecurity">          <security mode = "Transport">             <transport                clientCredentialType = "Windows"                protectionLevel = "EncryptAndSign"             />          </security>       </binding>    </netTcpBinding> </bindings> 

For completeness' sake, although not required by the Internet scenario, here is how to configure NetTcpBinding for Message security with username client credentials:

 public enum MessageCredentialType {    None,    Windows,    UserName,    Certificate,    IssuedToken } public sealed class MessageSecurityOverTcp {    public MessageCredentialType ClientCredentialType    {get;set;}    //More members } NetTcpBinding binding = new NetTcpBinding(SecurityMode.Message); binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName; 

NetTcpSecurity offers the Message property of the type MessageSecurityOverTcp. You need to set the credentials type using the MessageCredentialType enum. Most bindings use the MessageCredentialType enum for representing Message security client credentials.

Figure 10-1 shows the security-related elements of the NetTcpBinding.

Figure 10-1. NetTcpBinding and security


NetTcpBinding has a reference to NetTcpSecurity, which uses the SecurityMode enum to indicate the transfer security mode. When Transport security is used, NetTcpSecurity will use an instance of TcpTransportSecurity containing the client credential type via the TcpClientCredentialType enum, and the configured protection level via the ProtectionLevel enum. When Message security is used, NetTcpSecurity will use an instance of MessageSecurityOverTcp containing the client credential type via the MessageCredentialType enum.

10.7.1.3. NetNamedPipeBinding configuration

NetNamedPipeBinding takes a construction parameter indicating the desired transfer security mode:

 public class NetNamedPipeBinding : Binding,... {    public NetNamedPipeBinding(NetNamedPipeSecurityMode securityMode);    public NetNamedPipeSecurity Security    {get;}    //More members } 

The Security property of the type NetNamedPipeSecurity contains the transfer mode, Transport or None, and a single property with the specific Transport settings:

 public sealed class NetNamedPipeSecurity {    public NetNamedPipeSecurityMode Mode    {get;set;}    public NamedPipeTransportSecurity Transport    {get;} } 

For the intranet security scenario, select Transport security for transfer security, and set the values of the transport property of the type NamedPipeTransportSecurity:

 public sealed class NamedPipeTransportSecurity {    public ProtectionLevel ProtectionLevel    {get;set;} } 

The TRansfer property should be initialized with the protection level set to ProtectionLevel.EncryptAndSign. Because this is the default for the binding, these two declarations are equivalent:

 NetNamedPipeBinding binding1 = new NetNamedPipeBinding( ); NetNamedPipeBinding binding2 = new NetNamedPipeBinding(                                               NetNamedPipeSecurityMode.Transport); binding2.Security.Transport.ProtectionLevel = ProtectionLevel.EncryptAndSign; 

Or alternatively, using a config file:

 <bindings>    <netNamedPipeBinding>       <binding name = "IPCWindowsSecurity">          <security mode = "Transport">             <transport protectionLevel = "EncryptAndSign"/>          </security>       </binding>    </netNamedPipeBinding> </bindings> 

There is no need (or option) to set the client credentials type since only Windows credentials are supported (see Table 10-2). Figure 10-2 shows the security-related elements of the NetNamedPipeBinding.

Figure 10-2. NetNamedPipeBinding and security


NetNamedPipeBinding has a reference to NetNamedPipeSecurity, which uses the NetNamedPipeSecurityMode enum to indicate the transfer security mode. When Transport security is used, NetTcpSecurity will use an instance of NamedPipeTransportSecurity containing the configured protection level via the ProtectionLevel enum.

10.7.1.4. NetMsmqBinding configuration

NetMsmqBinding offers a construction parameter for the transfer security mode and a Security property:

 public class NetMsmqBinding : MsmqBindingBase,... {    public NetMsmqBinding(NetMsmqSecurityMode securityMode);    public NetMsmqSecurity Security    {get;}    //More members } 

The Security property of the type NetMsmqSecurity contains the transfer mode, Transport or Message, and two respective properties with the specific settings:

 public sealed class NetMsmqSecurity {    public NetMsmqSecurityMode Mode    {get;set;}    public MsmqTransportSecurity Transport    {get;}    public MessageSecurityOverMsmq Message    {get;} } 

For the intranet security scenario, select transfer security for Transport security and set the values of the transport property of the type MsmqTransportSecurity:

 public sealed class MsmqTransportSecurity {    public MsmqAuthenticationMode MsmqAuthenticationMode    {get;set;}    public ProtectionLevel MsmqProtectionLevel    {get;set;}    //More members } 

The transfer property should be initialized with the client credential type set to Windows domain using the MsmqAuthenticationMode enum:

 public enum MsmqAuthenticationMode {    None,    WindowsDomain,    Certificate } 

Windows domain is the default credentials type. In addition, you need to set the protection level to ProtectionLevel.EncryptAndSign because the MSMQ binding defaults to the ProtectionLevel.Signed protection level. The following two definitions are equivalent:

 NetMsmqBinding binding1 = new NetMsmqBinding( ); binding1.Security.Transport.MsmqProtectionLevel = ProtectionLevel.EncryptAndSign; NetMsmqBinding binding2 = new NetMsmqBinding( ); binding2.Security.Mode = NetMsmqSecurityMode.Transport; binding2.Security.Transport.MsmqAuthenticationMode =                                              MsmqAuthenticationMode.WindowsDomain; binding2.Security.Transport.MsmqProtectionLevel = ProtectionLevel.EncryptAndSign; 

Or, alternatively, using a config file:

 <bindings>    <netMsmqBinding>       <binding name = "MSMQWindowsSecurity">          <security mode = "Transport">             <transport                msmqAuthenticationMode = "WindowsDomain"                msmqProtectionLevel = "EncryptAndSign"             />          </security>       </binding>    </netMsmqBinding> </bindings> 

Figure 10-3 shows the security-related elements of the NetMsmqBinding.

Figure 10-3. NetMsmqBinding and security


NetMsmqBinding has a reference to NetMsmqSecurity, which uses the NetMsmqSecurityMode enum to indicate the transfer security mode. When transport security is used, NetMsmqSecurity will use an instance of MsmqTransportSecurity containing the client credential type via the MsmqAuthenticationMode enum, and the configured protection level via the ProtectionLevel enum. In a similar manner, there are references to types controlling Message security.

10.7.2. Message Protection

While a service should use the highest level of security, it is actually at the mercy of its host because the host is the one configuring the binding. This is especially problematic if the service is to be deployed in an unknown environment with an arbitrary host. To compensate, WCF lets service developers insist on a protection level, or rather, constrain the minimum protection level their service is willing to work with. Both the service and the client can constrain the protection level independently of the other. You can constrain the protection level at three places. When applied at the service contract, all operations on the contract are considered sensitive and protected. When applied at the operation contract, then only that operation is protected; other operations on the same contract are not. Finally, you can also constrain the fault contract, because sometimes the error information returned to the client is sensitive, containing parameter values, exception messages, and the call stack. The respective contract attributes offer the ProtectionLevel property of the enum type ProtectionLevel:

 [AttributeUsage(AttributeTargets.Interface|AttributeTargets.Class,                 Inherited = false)] public sealed class ServiceContractAttribute : Attribute {    public ProtectionLevel ProtectionLevel    {get;set;}    //More members } [AttributeUsage(AttributeTargets.Method)] public sealed class OperationContractAttribute : Attribute {    public ProtectionLevel ProtectionLevel    {get;set;}    //More members } [AttributeUsage(AttributeTargets.Method,AllowMultiple = true,                 Inherited = false)] public sealed class FaultContractAttribute : Attribute {    public ProtectionLevel ProtectionLevel    {get;set;}    //More members } 

Here is how to set the protection level on a service contract:

 [ServiceContract(ProtectionLevel = ProtectionLevel.EncryptAndSign)] interface IMyContract {...} 

Setting the ProtectionLevel property on the contract attributes merely indicates the low-water mark; that is, the minimum accepted protected level by this contract. If the binding is configured for a lower protection level, it will result in an InvalidOperationException at the service load time or the time the proxy is opened. If the binding is configured for a higher level, then that is acceptable by the contract. The default of the ProtectionLevel property on the contract attributes defaults to ProtectionLevel.None, meaning it has no effect.

The desired protection constraint is considered a local implementation detail of the service and so the required protection level is not exported with the service metadata. Consequently, the client may require a different level, and enforce it separately from the service.

Even though the nonintranet bindings do not offer a protection level property, the protection level constraint at the service, operation, or fault contract is satisfied when using Transport or Message security. The constraint is not satisfied when turning off security in the None scenario.


10.7.3. Authentication

By default, when a client calls a proxy that targets an endpoint whose binding is configured for using Windows credentials with Transport security, there is nothing explicit the client needs to do to pass its credentials. WCF will automatically pass the Windows identity of the client's process to the service:

 class MyContractClient : ClientBase<IMyContract>,IMyContract {...} MyContractClient proxy = new MyContractClient( ); proxy.MyMethod( ); //Client identity passed here proxy.Close( ); 

When the call is received by the service, if the client credentials represent a valid Windows account, WCF on the service side will authenticate the caller, and the caller will be allowed to access the operation on the service.

10.7.3.1. Providing alternative Windows credentials

Instead of using the identity of the process the client happens to be running in, the client can pass alternative Windows credentials. The ClientBase<T> base class offers the ClientCredentials property of the type ClientCredentials:

 public abstract class ClientBase<T> : ... {    public ClientCredentials ClientCredentials    {get;} } public class ClientCredentials : ...,IEndpointBehavior {    public WindowsClientCredential Windows    {get;}    //More members } 

ClientCredentials contains the property Windows of the type WindowsClientCredential, defined as:

 public sealed class WindowsClientCredential {    public NetworkCredential ClientCredential    {get;set;}    //More members } 

WindowsClientCredential has the property ClientCredential of the type NetworkCredential, which is where the client needs to set the alternative credentials:

 public class NetworkCredential : ... {    public NetworkCredential( );    public NetworkCredential(string userName,string password);    public NetworkCredential(string userName,string password,string domain);    public string Domain    {get;set;}    public string UserName    {get;set;}    public string Password    {get;set;} } 

Example 10-3 demonstrates how to use these classes and properties in providing alternative Windows credentials to the process identity.

Example 10-3. Providing alternative Windows credentials

 NetworkCredential credentials = new NetworkCredential( ); credentials.Domain   = "MyDomain"; credentials.UserName = "MyUsername"; credentials.Password = "MyPassword"; MyContractClient proxy = new MyContractClient( ); proxy.ClientCredentials.Windows.ClientCredential = credentials; proxy.MyMethod( ); proxy.Close( ); 

Note in Example 10-3 that the client must instantiate a new object of the type NetworkCredential, and that it cannot simply assign the credentials in the existing ClientCredential property of WindowsClientCredential. Once using an assigned identity, the proxy cannot use any other identity later on. Clients use the technique of Example 10-3 when the credentials provided are collected dynamically at runtime, perhaps using a login dialog box. If, on the other hand, the alternative credentials are static, the client developer can encapsulate them in the proxy constructors:

 public partial class MyContractClient: ClientBase<IMyContract>,IMyContract {    public MyContractClient( )    {       SetCredentials( );    }    /* More constructors */    void SetCredentials( )    {       ClientCredentials.Windows.ClientCredential =                       new NetworkCredential("MyClient","MyPassword","MyDomain");    }    public void MyMethod( )    {       Channel.MyMethod( );    } } 

When working with a channel factory instead of a proxy class, the ChannelFactory base class offers the Credentials property of the type ClientCredentials:

 public abstract class ChannelFactory : ... {    public ClientCredentials Credentials    {get;}    //More members } public class ChannelFactory<T> : ChannelFactory,... {    public T CreateChannel( );    //More members } 

Simply set alternative credentials in the Credentials property just as in Example 10-3:

 ChannelFactory<IMyContract> factory = new ChannelFactory<IMyContract>(""); factory.Credentials.Windows.ClientCredential = new NetworkCredential(...); IMyContract proxy = factory.CreateChannel( ); using(proxy as IDisposable) {    proxy.MyMethod( ); } 

Note that you cannot use the static CreateChannel( ) methods of ChannelFactory<T> since you first have to instantiate a factory in order to access the Credentials property.

10.7.4. Identities

All Windows processes run with an authenticated security identity, and the process hosting a WCF service is no different. The identity is actually a Windows account whose security token is attached to the process and, by default, to all threads in that process. However, it is up to the application administrator to decide which identity to use. You can have the host run with an interactive user identity; that is, the identity of the user who launched the host process. Simply launching the process by a user will use that user account as the identity of the host process. Interactive identity is typically used when self-hosting, and is ideal for debugging because the debugger will automatically attach itself to the host process when launched from within Visual Studio. However, relying on an interactive identity is impractical for deployment on a server machine where there is not necessarily a logged-on user, and besides, the logged-on user may not have the necessary credentials to perform the service work. For production deployment, you typically rely on a designated accounta pre-set Windows account used primarily by your service or services. To launch the service under a designated account you can use the Run as... shell option to launch the service. However, Run as... is only useful for simple testing. You can also have an NT service as your host, and use the Control Panel Services applet to assign a designated identity to the host. If hosting in IIS6 or in the WAS, you can use those environments' configuration tools to assign a designated identity.

10.7.4.1. The IIdentity interface

In .NET, the IIdentity interface from the System.Security.Principal namespace represents a security identity:

 public interface IIdentity {    string AuthenticationType    {get;}    bool IsAuthenticated    {get;}    string Name    {get;} } 

The interface lets you know if the identity behind the interface is authenticated (and which authentication mechanism was used) as well as allows you to obtain the name of the identity. Out of the box, WCF takes advantage of three implementations of IIdentity offered by .NET. The WindowsIdentity class represents a Windows account. The GenericIdentity class is a general-purpose class whose main use is to wrap an identity name with an IIdentity. With both GenericIdentity and WindowsIdentity, if the identity name is an empty string, that identity is considered unauthenticated, and any other nonzero-length name is considered authenticated. Finally, X509Identity is an internal class that represents an identity that was authenticated using an X509 certificate. The identity behind X509Identity is always authenticated.

10.7.4.2. Working with WindowsIdentity

The WindowsIdentity class offers a few useful methods above and beyond the mere implementation of IIdentity:

 public class WindowsIdentity : IIdentity,... {    public WindowsIdentity(string sUserPrincipalName);    public static WindowsIdentity GetAnonymous( );    public static WindowsIdentity GetCurrent( );    public virtual bool IsAnonymous    {get;}    public virtual bool IsAuthenticated    {get;}    public virtual string Name    {get;}    //More members } 

The IsAnonymous Boolean property indicates if the underlying identity is anonymous and the GetAnonymous( ) method returns an anonymous Windows identity, typically used for impersonation to mask out the real identity:

 WindowsIdentity identity = WindowsIdentity.GetAnonymous( ); Debug.Assert(identity.Name == ""); Debug.Assert(identity.IsAuthenticated == false); Debug.Assert(identity.IsAnonymous == true); 

The GetCurrent( ) static method returns the identity of the process where it is called. That identity is by default nonanonymous and authenticated:

 WindowsIdentity currentIdentity = WindowsIdentity.GetCurrent( ); Debug.Assert(currentIdentity.Name != ""); Debug.Assert(currentIdentity.IsAuthenticated == true); Debug.Assert(currentIdentity.IsAnonymous == false); 

10.7.5. The Security Call Context

Every operation on a secured WCF service has a security call context. The security call context is represented by the class ServiceSecurityContext, defined as:

 public class ServiceSecurityContext {    public static ServiceSecurityContext Current    {get;}    public bool IsAnonymous    {get;}    public IIdentity PrimaryIdentity    {get;}    public WindowsIdentity WindowsIdentity    {get;}    //More members } 

The main use for the security call context is the custom security mechanism, as well as analysis and auditing. While presented here in the context of the intranet scenario, all other scenarios have use for the security call context.

Note that in spite of the name, this is a per-call security context, not per-service. The security call context is stored in the TLS, so every method on every object down the call chain from the service can access the security call context, including your service constructor. To obtain your current security call context, simply access the Current static property. Another way of accessing the security call context is via the ServiceSecurityContext property of the OperationContext:

 public sealed class OperationContext : ... {    public ServiceSecurityContext ServiceSecurityContext    {get;}    //More members } 

Regardless of which mechanism you use, you will get the same object:

 ServiceSecurityContext context1 = ServiceSecurityContext.Current; ServiceSecurityContext context2 = OperationContext.Current.ServiceSecurityContext; Debug.Assert(context1 == context2); 

Your service has a security call context only if security is enabled. When security is disabled, ServiceSecurityContext.Current returns null.


The PrimaryIdentity property of ServiceSecurityContext contains the identity of the immediate client up the call chain. If the client is unauthenticated, PrimaryIdentity will reference an implementation of IIdentity with a blank identity. When Windows authentication is used, the PrimaryIdentity property will be set to an instance of WindowsIdentity.

The WindowsIdentity property is meaningful only when using Windows authentication, and it will always be of the type WindowsIdentity. When valid Windows credentials are provided, the WindowsIdentity property will contain the corresponding client identity, and will match the value of the PrimaryIdentity.

The constructor of a singleton service does not have a security call context, since it is called when the host is launched, not as a result of a client call.


10.7.6. Impersonation

Some resources, such as the filesystem, SQL Server, sockets, and even DCOM objects, grant access to themselves based on the security token of their caller. Typically, the host process is assigned an identity with elevated credentials that are required to access such resources in order to function properly. Clients, however, typically have restricted credentials compared with those of the service. Traditionally, developers used impersonation to address this credentials gap. Impersonation lets the service assume the client's identity primarily in order to verify if the client can perform the work it asks the service to do. Impersonation has a number of key detrimental effects on your application discussed at the end of this section. Instead of impersonation, you should apply role-based security to authorize the callers. That said, many developers are used to design systems using impersonation, so both .NET and WCF have supporting this need in mind.

10.7.6.1. Manual impersonation

The service can impersonate its calling client by calling the Impersonate( ) method of the WindowsIdentity class:

 public class WindowsIdentity : IIdentity,... {    public virtual WindowsImpersonationContext Impersonate( );    //More members } public class WindowsImpersonationContext : IDisposable {    public void Dispose( );    public void Undo( ); } 

Impersonate( ) returns an instance of WindowsImpersonationContext containing the service's previous identity. To revert back to that identity, the service needs to call the Undo( ) method. To impersonate, the service needs to call Impersonate( ) on the identity of the caller available with the WindowsIdentity property of its security call context, as shown in Example 10-4.

Example 10-4. Explicit impersonation and reversion

 class MyService : IMyContract {    public void MyMethod( )    {       WindowsImpersonationContext impersonationContext =                       ServiceSecurityContext.Current.WindowsIdentity.Impersonate( );       try       {          /*  Do work as client */       }       finally       {          impersonationContext.Undo( );       }    } } 

Note in Example 10-4 that the service reverts to its old identity even in the face of exceptions by placing the call to Undo( ) in the finally statement. To somewhat simplify reverting, the implementation of Dispose( ) of WindowsImpersonationContext also reverts, which enables you to use it in conjunction with a using statement:

 public void MyMethod( ) {    using(ServiceSecurityContext.Current.WindowsIdentity.Impersonate( ))    {       /*  Do work as client */    } } 

10.7.6.2. Declarative impersonation

Instead of manual impersonation, you can instruct WCF to automatically impersonate the caller of the method. The OperationBehavior attribute offers the Impersonation property of the enum type ImpersonationOption:

 public enum ImpersonationOption {    NotAllowed,    Allowed,    Required } [AttributeUsage(AttributeTargets.Method)] public sealed class OperationBehaviorAttribute : Attribute,IOperationBehavior {    public ImpersonationOption Impersonation    {get;set;}    //More members } 

The ImpersonationOption.NotAllowed value is the default value. It indicates that WCF should not auto-impersonate, but you can write code (as in Example 10-4) that explicitly impersonates.

ImpersonationOption.Allowed instructs WCF to automatically impersonate the caller whenever Windows authentication is used. ImpersonationOption.Allowed has no effect with other authentication mechanisms. When WCF auto-impersonates, it will also auto-revert to the previous service identity once the method returns.

The ImpersonationOption.Required value mandates the use of Windows authentication and will throw an exception otherwise. As its name implies, WCF will always auto-impersonate (and revert) in every call to the operation:

 class MyService : IMyContract {    [OperationBehavior(Impersonation = ImpersonationOption.Required)]    public void MyMethod( )    {       /* Do work as client */    } } 

Note that there is no way to use declarative impersonation with the service constructor because you cannot apply the OperationBehavior attribute on a constructor. Constructors can only use manual impersonation. If you do impersonate in the constructor, always revert as well in the constructor, to avoid side effects on the operations of the service and even other services in the same host.

10.7.6.3. Impersonating all operations

If you need to enable impersonation in all the operations of the service, the ServiceHostBase class has the Authorization property of the type ServiceAuthorizationBehavior:

 public abstract class ServiceHostBase : ... {    public ServiceAuthorizationBehavior Authorization    {get;}    //More members } public sealed class ServiceAuthorizationBehavior : IServiceBehavior {    public bool ImpersonateCallerForAllOperations    {get;set;}    //More members } 

ServiceAuthorizationBehavior provides the Boolean property ImpersonateCallerForAllOperations, which is false by default. Contrary to what its name implies, when set to TRue, this property merely verifies that there is no operation on the service configured with ImpersonationOption.NotAllowed. This constraint is verified at service load time, yielding an InvalidOperationException when violated.

In effect, when Windows authentication is used, this will amount to automatically impersonating in all operations, but you must explicitly have all operations decorated with ImpersonationOption.Allowed or ImpersonationOption.Required. ImpersonateCallerForAllOperations has no effect on constructors.

You can set the ImpersonateCallerForAllOperations property programmatically or in the config file. If you set it programmatically, you can only do so before opening the host:

 ServiceHost host = new ServiceHost(typeof(MyService)); host.Authorization.ImpersonateCallerForAllOperations = true; host.Open( ); 

When set using a config file, you need to reference the matching service behavior at the service declaration:

 <services>    <service name = "MyService" behaviorConfiguration= "ImpersonateAll">       ...    </service> </services> <behaviors>    <serviceBehaviors>       <behavior name = "ImpersonateAll">          <serviceAuthorization impersonateCallerForAllOperations = "true"/>       </behavior>    </serviceBehaviors> </behaviors> 

To automate impersonating in all operations without the need to apply the OperationBehavior attribute on every method, I wrote the SecurityHelper static class with the ImpersonateAll( ) methods:

 public static class SecurityHelper {    public static void ImpersonateAll(ServiceHostBase host);    public static void ImpersonateAll(ServiceDescription description);    //More members } 

You can only call ImpersonateAll( ) before opening the host:

 //Will impersonate in all operations class MyService : IMyContract {    public void MyMethod( )    {...} } ServiceHost host = new ServiceHost(typeof(MyService)); SecurityHelper.ImpersonateAll(host); host.Open( ); 

Example 10-5 shows the implementation of ImpersonateAll( ).

Example 10-5. Implementing SecurityHelper.ImpersonateAll( )

 public static class SecurityHelper {    public static void ImpersonateAll(ServiceHostBase host)    {       host.Authorization.ImpersonateCallerForAllOperations = true;       ServiceDescription description = host.Description;       ImpersonateAll(description);    }    public static void ImpersonateAll(ServiceDescription description)    {       foreach(ServiceEndpoint endpoint in description.Endpoints)       {          foreach(OperationDescription operation in endpoint.Contract.Operations)          {             foreach(IOperationBehavior behavior in operation.Behaviors)             {                if(behavior is OperationBehaviorAttribute)                {                   OperationBehaviorAttribute attribute =                                             behavior as OperationBehaviorAttribute;                   attribute.Impersonation = ImpersonationOption.Required;                   break;                }             }          }       }    }    //More members } 

In Example 10-5, ImpersonateAll( ) sets the ImpersonateCallerForAllOperations property of the provided host to TRue, and then it obtains from the host the service description that contains all the endpoints. That description is passed to an overloaded ImpersonateAll( ) that explicitly configures all operations with ImpersonationOption.Required. This is done by iterating over the endpoints collection of the description. For each endpoint, ImpersonateAll( ) accesses the operations collection of the contract. For each operation, there could be one or more operation behaviors in the form of IOperationBehavior (there is always at least one provided by WCF in the form of OperationBehaviorAttribute, even if you did not provide one explicitly). Each operation behavior is examined until the OperationBehaviorAttribute is found, at which point the Impersonation property is set to ImpersonationOption.Required.

You can further encapsulate and streamline the use of SecurityHelper.ImpersonateAll( ) in ServiceHost<T>:

 public class ServiceHost<T> : ServiceHost {    public void ImpersonateAll( )    {       if(State == CommunicationState.Opened)       {          throw new InvalidOperationException("Host is already opened");       }       SecurityHelper.ImpersonateAll(this);    }    //More members } 

Using ServiceHost<T> to automatically impersonate all callers is straightforward:

 ServiceHost<MyService> host = new ServiceHost<MyService>( ); host.ImpersonateAll( ); host.Open( ); 

10.7.6.4. Restricting impersonation

Authorization and authentication protect the service from being accessed by unauthorized and unauthenticated, potentially malicious clients. However, how should the client be protected from malicious services? One of the ways an adversary service could abuse the client is by assuming the client's identity and credentials and causing harm as the client.

In some cases, the client may not even want to allow the service to obtain its identity at all. WCF lets the client indicate the degree to which the service can obtain the client's identity and use it. Impersonation is actually a range of options indicating the level of trust between the client and the service. The WindowsClientCredential class provides the AllowedImpersonationLevel enum of the type TokenImpersonationLevel found in the System.Security.Principal namespace:

 public enum TokenImpersonationLevel {    None,    Anonymous,    Identification,    Impersonation,    Delegation } public sealed class WindowsClientCredential {    public TokenImpersonationLevel AllowedImpersonationLevel    {get;set;}    //More members } 

The client can restrict the allowed impersonation level both programmatically and administratively. For example, to programmatically restrict the impersonation level to TokenImpersonationLevel.Identification, before opening the proxy the client would write:

 MyContractClient proxy = new MyContractClient( ); proxy.ClientCredentials.Windows.AllowedImpersonationLevel =                                             TokenImpersonationLevel.Identification; proxy.MyMethod( ); proxy.Close( ); 

When using a config file, the administrator should define the allowed impersonation level as a custom endpoint behavior and reference it from the relevant endpoint section:

 <client>    <endpoint behaviorConfiguration = "ImpersonationBehavior"       ...    /> </client> <behaviors>    <endpointBehaviors>       <behavior name = "ImpersonationBehavior">          <clientCredentials>             <windows allowedImpersonationLevel = "Identification"/>          </clientCredentials>       </behavior>    </endpointBehaviors> </behaviors> 

TokenImpersonationLevel.None simply means that no impersonation level is assigned, and it amounts to no credentials. When the impersonation level is set to TokenImpersonationLevel.Anonymous, the client provides no credentials at all. In practical terms, TokenImpersonationLevel.None and TokenImpersonationLevel.Anonymous amount to the same behaviorthe client provides no identity information. These two values are of course the safest from the client's perspective, but are the least useful options from the application perspective, since no authentication or authorization is possible by the service. Avoiding any credentials is only possible if the service is configured for anonymous access or for having no security, which is not the case with the intranet scenario. If the service is configured for Windows security, these two values yield ArgumentOutOfRangeException on the client side.

With TokenImpersonationLevel.Identification, the service can identify the client; that is, obtain the security identity of the calling client. The service, however, is not allowed to impersonate the clienteverything the service does must be done under the service's own identity. Trying to impersonate will throw an ArgumentOutOfRangeException on the service side. TokenImpersonationLevel.Identification is the default value used with Windows security and is the recommended value for the intranet scenario. Note that if the service and the client are on the same machine, then the service will still be able to impersonate the client, even when TokenImpersonationLevel.Identification is used.

TokenImpersonationLevel.Impersonation grants the service permission to both obtain the client's identity and also to impersonate the client. Impersonation indicates a great deal of trust between the client and the service, since the service can do anything the client can do, even if the service host is configured to use a less privileged identity. The only difference between the real client and the impersonating service is that if the service is on a separate machine from the client, it cannot access resources or objects on other machines as the client, because the service machine does not really have the client's password. In the case where the service and the client are on the same machine, the service impersonating the client can make one network hop to another machine, since the machine it resides on can still authenticate the impersonated client identity.

Finally, TokenImpersonationLevel.Delegation provides the service with the client's Kerberos ticket. The service can freely access resources on any machine as the client. If service is also configured for delegation, then when it calls other downstream services, the client identity could be propagated further and further down the call chain. Delegation-required Kerberos authentication is not possible on Windows workgroup installations. Both the client and server user accounts must be properly configured in Active Directory to support delegation due to the enormous trust (and hence security risk) involved. Delegation uses, by default, another security service called cloaking, which propagates the caller identity along the call chain.

Delegation is extremely dangerous from the client perspective since the client has no control over who is using its identity or where. When the impersonation level is set to TokenImpersonationLevel.Impersonation, the client takes a calculated risk, since it knows which services it is accessing and if those services are on a difference machine, the client identity cannot propagate across the network. I consider delegation as something that enables the service to act as an imposter for the client, not just to impersonate it.

10.7.6.5. Using impersonation

You should design services so that they do not rely on impersonation, and the client should use TokenImpersonationLevel.Identification.

As a general design guideline, the further from the client, the less relevant its identity is. If you use some kind of a layered approach in your system design, each layer should run under its own identity, authenticate its immediate callers, and implicitly trust its calling layer to authenticate its original callers to maintain a chain of trusted, authenticated callers. Impersonation, on the other hand, requires you to keep propagating the identity further and further down the call chain all the way to the underlying resources. Doing so impedes scalability because many resources (such as SQL Server connections) are allocated per identity. With impersonation, you will need as many resources as clients, and you will not be able to benefit from resource pooling (such as connection pooling). Impersonation also complicates resources administration, because you need to grant access to the resources to the original client identities, and there could be numerous such identities to manage. A service that always runs under its own identity poses no such issues, regardless of how many diverse identities access that service. For access control to the resources, you should use authorization, as discussed next. Finally, relying on impersonation precludes non-Windows authentication mechanisms. If you do decide to use impersonation, use it judiciously and only as a sporadic last resort when there is no other and better design approach.

Impersonation is not possible with queued services.


10.7.7. Authorization

While authentication deals with verifying that the client is indeed who the client claims to be, most applications also need to verify that the client (more precisely, the identity it presents) has permission to perform the operation. Since it is impractical to program access permission for each individual identity, is it better to grant permissions to the roles clients play in the application domain. A role is a symbolic category of identities who share the same security privileges. When you assign a role to an application resource, you are granting access to that resource to whomever is a member of that role. Discovering the roles clients play in your business domain is part of your application-requirement analysis and design, just as factoring services and interfaces is. By interacting with roles instead of particular identities, you isolate your application from changes made in real life, such as adding new users, moving existing users between positions, promoting users, or users leaving their jobs. .NET allows you to apply role-based security both declaratively and programmatically, if the need to verify role membership is based on a dynamic decision.

10.7.7.1. Security principal

For security purposes, it is convenient to lump together the identity and the information about its role membership. This representation is called the security principal.

The principal in .NET is any object that implements the IPrincipal interface, defined in the System.Security.Principal namespace:

 public interface IPrincipal {    IIdentity Identity    {get;}    bool IsInRole(string role); } 

The IsInRole( ) method simply returns TRue if the identity associated with this principal is a member of the specified role, and false otherwise. The Identity read-only property provides access to read-only information about the identity, in the form of an object implementing the IIdentity interface. Out of the box, .NET offers several implementations of IPrincipal. GenericPrincipal is a general-purpose principal that has to be preconfigured with the roles information. It is typically used when no authorization is required, in which case GenericPrincipal wraps a blank identity. The WindowsPrincipal class looks up role membership information inside the Windows NT groups.

Every .NET thread has a principal object associated with it, obtained via the CurrentPrincipal static property of the THRead class:

 public sealed class Thread {    public static IPrincipal CurrentPrincipal    {get;set;}    //More members } 

For example, here is how to discover the user name as well as whether or not the caller was authenticated:

 IPrincipal principal = Thread.CurrentPrincipal; string userName = principal.Identity.Name; bool isAuthenticated = principal.Identity.IsAuthenticated; 

10.7.7.2. Selecting authorization mode

As presented earlier, the ServiceHostBase class provides the Authorization property of the type ServiceAuthorizationBehavior. ServiceAuthorizationBehavior has the PrincipalPermissionMode property of the enum type PrincipalPermissionMode, defined as:

 public enum PrincipalPermissionMode {    None,    UseWindowsGroups,    UseAspNetRoles,    Custom } public sealed class ServiceAuthorizationBehavior : IServiceBehavior {    public PrincipalPermissionMode PrincipalPermissionMode    {get;set;}    //More members } 

Before opening the host, you can use the PrincipalPermissionMode property to select the principal mode; that is, which type of principal to install to authorize the caller.

If PrincipalPermissionMode is set to PrincipalPermissionMode.None, then principal-based authorization is possible. After authenticating the caller (if authentication is required at all), WCF installs GenericPrincipal with a blank identity and attaches it to the thread that invokes the service operation. That principal will be available via Thread.CurrentPrincipal.

When PrincipalPermissionMode is set to PrincipalPermissionMode.UseWindowsGroups, WCF will install a WindowsPrincipal with an identity matching the provided credentials. If no Windows authentication took place (because the service did not require it), then WCF will install a WindowsPrincipal with a blank identity.

PrincipalPermissionMode.UseWindowsGroups is the default value of the PrincipalPermissionMode property, so these two definitions are equivalent:

 ServiceHost host1 = new ServiceHost(typeof(MyService)); ServiceHost host2 = new ServiceHost(typeof(MyService)); host2.Authorization.PrincipalPermissionMode =                                           PrincipalPermissionMode.UseWindowsGroups; 

When using a config file, you need to reference a custom behavior section assigning the principal mode:

 <services>    <service name = "MyService" behaviorConfiguration = "WindowsGroups">       ...    </service> </services> <behaviors>    <serviceBehaviors>       <behavior name = "WindowsGroups">         <serviceAuthorization principalPermissionMode = "UseWindowsGroups"/>       </behavior>    </serviceBehaviors> </behaviors> 

10.7.7.3. Declarative role-based security

You apply service-side declarative role-based security using the attribute PrincipalPermissionAttribute, defined in the System.Security.Permissions namespace:

 public enum SecurityAction {    Demand,    //More members } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public sealed class PrincipalPermissionAttribute : CodeAccessSecurityAttribute {    public PrincipalPermissionAttribute(SecurityAction action);    public bool Authenticated    {get;set; }    public string Name    {get;set;}    public string Role    {get;set;}    //More members } 

The PrincipalPermission attribute lets you declare the required roles membership. For the intranet scenario, when you specify a Windows NT group as a role, you must prefix it with the domain name or the local machine name (if the role is defined locally only). In Example 10-6, the declaration of the PrincipalPermission attribute grants access to MyMethod( ) only for callers whose identity belongs to the Managers user group.

Example 10-6. Declarative role-based security on the intranet

 [ServiceContract] interface IMyContract {    [OperationContract]    void MyMethod( ); } class MyService : IMyContract {    [PrincipalPermission(SecurityAction.Demand,Role = @"<domain>\Managers")]    public void MyMethod( )    {...} } 

If the user is not a member of that role, .NET throws an exception of type SecurityException.

When experimenting with Windows role-based security, you often add users to or remove users from user groups. Because user-group information is cached by Windows at login time, the changes you make are not reflected until the next login.


If multiple roles are allowed to access the method, you can apply the attribute multiple times:

 [PrincipalPermission(SecurityAction.Demand,Role=@"<domain>\Managers")] [PrincipalPermission(SecurityAction.Demand,Role=@"<domain>\Customers")] public void MyMethod( ) {...} 

When multiple PrincipalPermission attributes are used, .NET verifies that the caller is a member of at least one of the demanded roles. If you want to verify that the user is a member of both roles, you need to use programmatic role membership checks, discussed later.

While the attribute by its very definition can be applied on methods and classes, in a WCF service class you can only apply it on methods. The reason is that in WCF (unlike normal classes) the service class constructor always executes under a GenericPrincipal with a blank identity, regardless of the authentication mechanisms used. As a result, the identity that the constructor is running under is unauthenticated, and will always fail any kind of authorization attempt (even when not using Windows NT groups):

 //Will always fail [PrincipalPermission(SecurityAction.Demand,Role = "...")] class MyService : IMyContract {...} 

Avoid sensitive work that requires authorization at the service constructor. With a per-session service, perform such work in the operations themselves, and with a sessionful service provide a dedicated Initialize( ) operation where you can initialize the instance and authorize the callers.


By setting the Name property of the PrincipalPermission attribute, you can even insist on granting access to a particular user. This practice is unadvisable, however, because it is wrong to hardcode usernames:

 [PrincipalPermission(SecurityAction.Demand,Name = "John")] 

You can also insist on a particular user and that the user is a member of a role:

 [PrincipalPermission(SecurityAction.Demand,Name = "John",                      Role = @"<domain>\Managers")] 

Declarative role-based security hardcodes the role name. If your application looks up role names dynamically, you have to use programmatic role verification as presented next.


10.7.7.4. Programmatic role-based security

Sometimes you need to programmatically verify role membership. Usually, you need to do that when the decision as to whether to grant access depends both on role membership and on some other values known only during call time, such as parameter values, time of day, and location. Another case in which programmatic role membership verification is needed is when you're dealing with localized user groups. To demonstrate the first category, imagine a banking service that lets clients transfer sums of money between two specified accounts. Only customers and tellers are allowed to call the TRansferMoney( ) operation, with the following business rule: if the amount transferred is greater than 5,000, only tellers are allowed to do the transfer. Declarative role-based security can verify that the caller is a teller or a customer, but it cannot enforce the additional business rule. For that, you need to use the IsInRole( ) method of IPrincipal, as shown in Example 10-7.

Example 10-7. Programmatic role-based security

 [ServiceContract] interface IBankAccounts {    [OperationContract]    void TransferMoney(double sum,long sourceAccount,long destinationAccount); } static class AppRoles {    public const string Customers = @"<domain>\Customers";    public const string Tellers   = @"<domain>\Tellers"; } class BankService : IBankAccounts {    [PrincipalPermission(SecurityAction.Demand,Role = AppRoles.Customers)]    [PrincipalPermission(SecurityAction.Demand,Role = AppRoles.Tellers)]    public void TransferMoney(double sum,long sourceAccount,long destinationAccount)    {       IPrincipal  principal = Thread.CurrentPrincipal;       Debug.Assert(principal.Identity.IsAuthenticated);       bool isCustomer = principal.IsInRole(AppRoles.Customers);       bool isTeller   = principal.IsInRole(AppRoles.Tellers);       if(isCustomer && ! isTeller)       {          if(sum > 5000)          {             string message = "Caller does not have sufficient authority to" +                              "transfer this sum";             throw(new UnauthorizedAccessException(message));          }       }       DoTransfer(sum,sourceAccount,destinationAccount);    }    //Helper method    void DoTransfer(double sum,long sourceAccount,long destinationAccount)    {...} } 

Example 10-7 demonstrates a number of other points. First, even though it uses programmatic role membership verification with the value of the sum argument, it still uses declarative role-based security as the first line of defense, allowing access only to clients who are members of the Customers or Tellers roles. Second, you can programmatically assert that the caller is authenticated using the IsAuthenticated property of IIdentity. Finally, note the use of the AppRoles static class to encapsulate the actual string used for the role to avoid hardcoding the roles in multiple places. In a similar manner, you need to employ programmatic role-based security to enforce additional rules such as that the transfer is allowed only during normal business hours.

There is a complete disconnect between role-based security and the actual principal type. When the PrincipalPermission attribute is asked to verify role membership, it simply gets hold of its thread's current principal in the form of IPrincipal, and calls its IsInRole( ) method. This is also true of programmatic role membership verification that uses only IPrincipal, as shown in Example 10-7. The separation of the IPrincipal interface from its implementation is the key to providing other role-based security mechanisms besides Windows NT groups, as you will see in the other scenarios.


10.7.8. Identity Management

In the intranet scenario, after successful authentication, WCF will attach to the operation thread a principal identity of the type WindowsIdentity, which will have the value of its Name property set to the username (or Windows account) provided by the client. Since valid credentials are provided, the security call context's two identities, the primary identity and the Windows identity, will be the set to the same identity as the principal identity. All three identities will be considered authenticated. The identities and their values are shown in Table 10-4.

Windows Roles Localization

Declarative role-based security hardcodes the role name. If your application is deployed in international markets and you use Windows groups as roles, it's likely the role names will not match. In the intranet scenario, the principal object attached to the thread accessing the service is of the type WindowsPrincipal:

 public class WindowsPrincipal : IPrincipal {    public WindowsPrincipal(WindowsIdentity ntIdentity);    //IPrincipal implementation    public virtual IIdentity Identity    {get;}    public virtual bool IsInRole(string role);    //Additional methods:    public virtual bool IsInRole(int rid);    public virtual bool IsInRole(WindowsBuiltInRole role); } 

WindowsPrincipal provides two additional IsInRole( ) methods that are intended to ease the task of localizing Windows NT groups. You can provide IsInRole( ) with an enum of type WindowsBuiltInRole, matching the built-in NT roles, such as WindowsBuiltInRole.Administrator or WindowsBuiltInRole.User. The other version of IsInRole( ) accepts an integer indexing specific roles. For example, a role index of 512 maps to the Administrators group. The MSDN Library contains a list of both the predefined indexes and ways to provide your own aliases and indexes to user groups.


Table 10-4. Identity management in the intranet scenario

Identity

Type

Value

Authenticated

Thread principal

WindowsIdentity

Username

Yes

Security context primary

WindowsIdentity

Username

Yes

Security context Windows

WindowsIdentity

Username

Yes


Note that while the host process (and the thread token) retain their designated identity, the principal identity will be that of the caller. I call this behavior soft impersonation, and when used in conjunction with role-based security it largely negates the need to ever perform real impersonation and replace the security token with that of the client.

10.7.9. Callbacks

When it comes to security on the intranet, there are several key differences between normal service operations and callbacks. First, with a callback contract you can only ascribe protection level at the operation level, not the callback contract level. For example, this protection-level constraint will be ignored:

 [ServiceContract(CallbackContract = typeof(IMyContractCallback))] interface IMyContract {...} //Demand for protection level will be ignored [ServiceContract(ProtectionLevel = ProtectionLevel.EncryptAndSign)] interface IMyContractCallback {...} 

You can only take advantage of operation-level demand for protection level:

 [ServiceContract(CallbackContract = typeof(IMyContractCallback))] interface IMyContract {...} interface IMyContractCallback {    [OperationContract(ProtectionLevel = ProtectionLevel.EncryptAndSign)]    void OnCallback( ); } 

All calls into the callback object come in with an unauthenticated principal, even if Windows security was used across the board to invoke the service. As a result, the principal identity will be set to a Windows identity with a blank identity, which will preclude authorization and role-based security.

While the callback does have a security call context, the Windows identity will be set to a WindowsIdentity instance with a blank identity that will preclude impersonation. The only meaningful information will be in the primary identity that will be set to service host process identity and machine name:

 class MyClient : IMyContractCallback {    public void OnCallback( )    {       IPrincipal principal = Thread.CurrentPrincipal;       Debug.Assert(principal.Identity.IsAuthenticated == false);       ServiceSecurityContext context = ServiceSecurityContext.Current;       Debug.Assert(context.PrimaryIdentity.Name == "MyHost/localhost");       Debug.Assert(context.IsAnonymous == false);    } } 

I recommend avoiding any sensitive work in the callback since you cannot easily use role-based security.




Programming WCF Services
Programming WCF Services
ISBN: 0596526997
EAN: 2147483647
Year: 2004
Pages: 148
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net