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 (or more precisely, the identity it
presents) has permission to perform the operation. Since it would be
impractical to program access permissions for each individual
identity, it is better to grant permissions to the roles clients play
in the application domain. A role is a symbolic
category of identities that share the same security privileges. When
you assign a role to an application resource, you are granting access
to that resource to anyone who is a member of that role. Discovering
the roles clients play in your business domain is part of your
application-requirements analysis and design, just like factoring
services and interfaces. 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.
7.1. The security principal
For security purposes, it is convenient to lump together an
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 role 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 username as well as
whether or not the caller was authenticated:
IPrincipal principal = Thread.CurrentPrincipal;
string userName = principal.Identity.Name;
bool isAuthenticated = principal.Identity.IsAuthenticated;
7.2. Selecting an 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,
principal-based authorization is impossible. 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 installs a WindowsPrincipal
with an identity matching the provided credentials. If no Windows
authentication took place (because the service did not require it),
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>
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 role membership. For the
intranet scenario, when you specify a Windows NT group as a role,
you don’t have to prefix the role name with your domain or machine
name (if you wish to authorize against its roles). You can also
explicitly specify another domain, if you have a trust relationship
with it.
In Example 4, the
declaration of the PrincipalPermission attribute grants
access to MyMethod() only to
callers whose identities belong to the Managers group.
Example 4. Declarative role-based security on the intranet
[ServiceContract] interface IMyContract { [OperationContract] void MyMethod(); } class MyService : IMyContract { [PrincipalPermission(SecurityAction.Demand,Role = "Manager")] public void MyMethod() {...} }
|
If the caller is not a member of that role, .NET throws an
exception of type SecurityException.
Note:
When experimenting with Windows role-based security, you
often add users to or remove users from user groups. Because
Windows caches user-group information 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 = "Manager")]
[PrincipalPermission(SecurityAction.Demand,Role = "Customer")]
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 caller is a member of
both roles, you need to use programmatic role membership checks,
discussed later.
While the PrincipalPermission attribute by its very
definition can be applied on methods and classes, in a WCF service
class you can apply it only on methods. The reason is that in WCF,
unlike with 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 under which the constructor is
running is unauthenticated and will always fail any kind of
authorization attempt (even if the client is a member of the role
and even when not using Windows NT groups):
//Will always fail
[PrincipalPermission(SecurityAction.Demand,Role = "...")]
class MyService : IMyContract
{...}
Warning:
Avoid sensitive work that requires authorization in the
service constructor. With a per-call 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 only to a particular user:
[PrincipalPermission(SecurityAction.Demand,Name = "John")]
or to a particular user that is a member of a particular
role:
[PrincipalPermission(SecurityAction.Demand,Name = "John",
Role = "Manager")]
These practices are inadvisable, however, because it is best
to avoid hardcoding usernames.
Note:
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.
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 at 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 50,000, only tellers are allowed to do the transfer.
Declarative role-based security can verify that the caller is either
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 5.
Example 5. Programmatic role-based security
[ServiceContract] interface IBankAccounts { [OperationContract] void TransferMoney(double sum,long sourceAccount,long destinationAccount); } static class AppRoles { public const string Customer = "Customer"; public const string Teller = "Teller"; } class BankService : IBankAccounts {
[PrincipalPermission(SecurityAction.Demand,Role = AppRoles.Customer)] [PrincipalPermission(SecurityAction.Demand,Role = AppRoles.Teller)] public void TransferMoney(double sum,long sourceAccount,long destinationAccount) { IPrincipal principal = Thread.CurrentPrincipal; Debug.Assert(principal.Identity.IsAuthenticated);
bool isCustomer = principal.IsInRole(AppRoles.Customer); bool isTeller = principal.IsInRole(AppRoles.Teller);
if(isCustomer && ! isTeller) { if(sum > 50000) { string message = "Caller does not have sufficient authority to" + "transfer this sum"; throw new SecurityException(message); } } DoTransfer(sum,sourceAccount,destinationAccount); } //Helper method void DoTransfer(double sum,long sourceAccount,long destinationAccount) {...} }
|
Example 5 also
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 Customer or
Teller 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.
Note:
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 5. 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.