6.3. Impersonating all operations
In the event that you need to enable impersonation in all the
service operations, 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 the service does not have any
operations 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 the service automatically impersonating the client in all
operations, but all the operations must be explicitly 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 do so only before opening the host:
ServiceHost host = new ServiceHost(typeof(MyService));
host.Authorization.ImpersonateCallerForAllOperations = true;
host.Open();
If you set it using a config file, you need to reference the
matching service behavior in 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() extension
methods:
public static class SecurityHelper
{
public static void ImpersonateAll(this ServiceHostBase host);
public static void ImpersonateAll(this ServiceDescription description);
//More members
}
The extension methods work on both ServiceHost and ServiceHost<T>.
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));
host.ImpersonateAll();
host.Open();
Example 3
shows the implementation of ImpersonateAll().
Example 3. Implementing SecurityHelper.ImpersonateAll()
public static class SecurityHelper { public static void ImpersonateAll(this ServiceHostBase host) { host.Authorization.ImpersonateCallerForAllOperations = true; host.Description.ImpersonateAll(); } public static void ImpersonateAll(this ServiceDescription description) { foreach(ServiceEndpoint endpoint in description.Endpoints) { if(endpoint.Contract.Name == "IMetadataExchange") { continue; } foreach(OperationDescription operation in endpoint.Contract.Operations) { OperationBehaviorAttribute attribute = operation.Behaviors. Find<OperationBehaviorAttribute>(); attribute.Impersonation = ImpersonationOption.Required; } } } //More members }
|
In Example 3, ImpersonateAll() (for the sake of good
manners) first sets the ImpersonateCallerForAllOperations
property of the provided host to true, then obtains the service description
from the host and calls the other overloaded extension method of
ServiceDescription. This version
explicitly configures all operations with ImpersonationOption.Required, by iterating over the
endpoints collection of the service description. For each endpoint
(except the metadata exchange endpoints), ImpersonateAll() accesses the operations collection
of the contract. For each operation, there is always
exactly one OperationBehaviorAttribute in the
collection of operation behaviors, even if you did not provide one
explicitly. The method then simply sets the Impersonation property to ImpersonationOption.Required.
6.4. Restricting impersonation
Authorization and authentication protect the service from
being accessed by unauthorized, unauthenticated, potentially
malicious clients. However, how should the client be protected from
malicious services? One of the ways an adversarial service could
abuse the client is by assuming the client’s identity and
credentials and causing harm while masquerading as the client. This
tactic enables the malicious service both to leave an identity trail
pointing back to the client and to elevate its own potentially
demoted, less-privileged credentials to the client’s level.
In some cases, the client may not want to allow the service to
obtain its identity at all. WCF therefore lets the client indicate
the degree to which the service can obtain the client’s identity and
how it can 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 use AllowedImpersonationLevel to 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, so the client provides no
identity information. This setting therefore amounts to the same
thing as TokenImpersonationLevel.Anonymous, where
the client provides no credentials at all. These two values are, of
course, the safest from the client’s perspective, but they are the
least useful options from the application’s perspective, since the
service cannot perform any authentication or authorization. Not
sharing credentials is possible only 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 an ArgumentOutOfRangeException on the client
side.
With TokenImpersonationLevel.Identification,
the service can identify the client (i.e., obtain the security
identity of the calling client). The service, however, is not
allowed to impersonate the client—everything the service does must
be done under the service’s own identity. Trying to impersonate will
throw an ArgumentOutOfRangeException on the service
side. Note, however, that if the service and the client are on the
same machine, the service will still be able to impersonate the
client, even when TokenImpersonationLevel.Identification is
used. TokenImpersonationLevel.Identification is
the default value used with Windows security and is the recommended
value for the intranet scenario.
TokenImpersonationLevel.Impersonation
grants the service permission both to obtain the client’s identity
and 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. In this
case, the service can freely access resources on any machine as the
client. If service is also configured for delegation, when it calls
other downstream services the client’s 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’s
identity along the call chain.
Delegation is extremely dangerous from the client’s
perspective, since the client has no control over who ends up using
its identity, or where. When the impersonation level is set to
TokenImpersonationLevel.Impersonation, the
client takes a calculated risk: it knows which services it is
accessing, and if those services are on a different machine, the
client identity cannot propagate across the network. I consider
delegation something that enables the service not just to
impersonate the client, but to act as an imposter; security-wise, as
far as the client is concerned, this is tantamount to waiving
security.
6.5. Avoiding impersonation
You should design your services so that they do not rely on
impersonation, and your clients should use TokenImpersonationLevel.Identification.
Impersonation is a relic of the ’90s, typically used in classic
two-tier systems in the absence of role-based security support,
where scalability was not a concern and managing a small number of
identities across resources was doable.
As a general design guideline, the further down the call chain
from the client, the less relevant the client’s identity is. If you
use some kind of 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
callers, thereby maintaining a chain of trusted, authenticated
callers. This is called the trusted subsystem
pattern. 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 resource administration, because you need to grant
access to the resources to all of 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 problems,
regardless of how many identities access that service. To control
access to the resources, you should use authorization, as discussed
next.
Multitier systems that do use impersonation typically
gravitate toward delegation, since that is the only way to propagate
the client identities across tiers and machines. In fact, the main
reason developers today use impersonation has little to do with
resource access authorization (which can easily be accomplished with
role-based security); instead, it is used as a mechanism for
auditing and identity propagation. If the application is required to
provide at lower layers the identity of the topmost client or all
clients up the chain, impersonation (if not full-fledged delegation)
may look like a viable option. There are three good solutions for
these requirements. First, if the business use cases require you to
provide the top-level identity to downstream parties, there is
nothing wrong with providing it as explicit method arguments since
they are part of the required behavior of the system. The second
solution is to use security audits (discussed later) and leave a
trail across the call chain. At any point, you can reconstruct that
chain of identities from the local audits. The third option is to
propagate the identity of the original caller (or the entire stack
of callers) in the message headers.
Finally, relying on impersonation precludes non-Windows
authentication mechanisms. If you do decide to use impersonation,
use it judiciously and only as a last resort, when there is no
other, better design approach.
Note:
Impersonation is not possible with queued
services.