Once you start adopting
service-oriented principles for your distributed applications, the
security challenges become slightly different. You suddenly realize that
you are crossing a security boundary for every service call you make.
Typically—though
not necessarily—there will be a network between the sender and the
recipient of a service call. While authentication is usually handled
automatically by the communication framework, you still have to come up
with your own authorization strategy and infrastructure.
Windows
Communication Foundation (WCF) provides powerful facilities for
implementing authorization in services. You have the choice between an
easy-to-use, role-based system as well as a more powerful, but more
complex, claims-based API. The remainder of this article will compare
both systems and show how to use them to implement robust service
authorization.
Role-Based Authorization
The
idea behind role-based authorization is that you associate a list of
roles with a user. At run time, the service code queries the list to
make security-related decisions. These roles can come from the Windows
security system (where they are called groups) or from some custom
store, such as a database.
The
roles API in the Microsoft .NET Framework is based on two interfaces
called IIdentity (identity information) and IPrincipal (role
information). An IIdentity-derived class holds information such as the
name of the user, whether he is authenticated, and how he was
authenticated.
An
IPrincipal-derived class must implement a single method called IsInRole
where you can pass in a role name and get a Boolean response. In
addition, the principal has a reference to the identity class it wraps.
The
.NET Framework ships with several implementations of theses interfaces.
WindowsIdentity and WindowsPrincipal wrap details about a Windows user.
GenericIdentity and GenericPrincipal can be used to represent a
custom, authenticated user.
There
is also a location where you can store a principal to associate it with
the currently executing thread in your application (for example, a WCF
operation request). This is the CurrentPrincipal thread static property
on the System.Threading.Thread class. The typical course of events is
that one part of an application populates Thread.CurrentPrincipal so
that another part of the application can reach into this property to
grab the IPrincipal implementation stored there. Then the IsInRole
method can be called on that class to query the role list of the current
user to ensure that the caller is authorized for the operation.
This
is exactly how it works in WCF. Based on the configured authentication
and credential type, WCF creates a corresponding IIdentity
implementation—a WindowsIdentity for Windows authentication, a
GenericIdentity for most other cases. Then, based on the configuration
of the WCF service behavior, ServiceAuthorization, an IPrincipal
implementation is created that wraps this identity and provides role
information. This IPrincipal is then set on Thread.CurrentPrincipal so
that it is accessible to the service operations.
Aside
from programmatically calling IsInRole to perform role-based security
checks, there is also an attribute called PrincipalPermissionAttribute
that allows you to annotate your service operations with role
requirements. PrincipalPermissionAttribute will signal a negative
authorization outcome to your client by throwing a SecurityException
prior to executing the service operation. WCF will catch this exception
and turn it into an Access Denied fault message that is returned. The
WCF client plumbing will turn this special fault into a .NET exception
of type SecurityAccessDeniedException. Figure 1 shows the various ways to do a role check and perform error handling.
Figure 1 Role Check with Error Handling
Service
class Service : IService { // only 'users' role member can call this method [PrincipalPermission(SecurityAction.Demand, Role = 'users')] public string[] GetRoles(string username) { // only administrators can retrieve the role information for other users if (ServiceSecurityContext.Current.PrimaryIdentity.Name != username) { if (Thread.CurrentPrincipal.IsInRole('administrators')) { ... } else { // access denied throw new SecurityException(); } } } }
Client
var factory = new ChannelFactory<IService>('*'); factory.Credentials.UserName.UserName = 'bob'; factory.Credentials.UserName.Password = 'bob'; var proxy = factory.CreateChannel(); try { Console.WriteLine('\nBob: roles for Bob --'); proxy.GetRoles('bob').ToList().ForEach(i => Console.WriteLine(i)); Console.WriteLine('\nBob: roles for Alice --'); proxy.GetRoles('alice').ToList().ForEach(i => Console.WriteLine(i)); } catch (SecurityAccessDeniedException) { Console.WriteLine('Access Denied\n'); }
The
ServiceAuthorization service behavior controls the creation of the
IPrincipal instance to be associated with the request thread. By
default, WCF assumes Windows authentication and tries to populate
Thread.CurrentPrincipal with a WindowsPrincipal.
When
your clients are not Windows users, you have the choice between getting
roles from an ASP.NET role provider or implementing a custom
authorization policy in order to get the roles from a custom database.
Another alternative is to implement a custom IPrincipal type and then
take complete control over the IsInRole implementation.
ASP.NET Role Provider
WCF
can use an ASP.NET role provider to retrieve roles for a user. You can
either use one of the built-in providers (for SQL Server or Microsoft
Authorization Manager) or write one of your own by deriving from
System.Web.Security.RoleProvider.
To
make the connection to the role provider, WCF attaches a
RoleProviderPrincipal to the executing thread that forwards all calls to
IsInRole to the role provider's IsUserInRole method. This method takes
the user name and role in question and returns true if the user is a
member of that role and false if not.
Figure
2 shows a sample custom role provider along with the necessary
configuration entries. WCF will call the role provider for every role
check that occurs in your service. If you have a lot of role checks, it
would be beneficial to implement some sort of caching strategy to avoid
excessive round-trips to the role store.
Figure 2 Sample Role Provider and Configuration Entries
Role Provider
class CustomRoleProvider : RoleProvider { public override string[] GetRolesForUser(string username) { if (username == 'administrator') { return new string[] { 'administrators', 'users' }; } else { return new string[] { 'sales', 'marketing', 'users' }; } } public override bool IsUserInRole(string username, string roleName) { return GetRolesForUser(username).Contains(roleName); } ... }
Configuration
<behaviors> <serviceBehaviors> <behavior name='security'> <serviceAuthorization principalPermissionMode='UseAspNetRoles' roleProviderName='CustomProvider' </serviceAuthorization> </behavior> </serviceBehaviors> </behaviors> <system.web> <roleManager enabled='true' defaultProvider='CustomProvider'> <providers> <add name='CustomProvider' type='Service.CustomRoleProvider, Service' /> </providers> </roleManager> </system.web>
Customizations
like caching could be implemented in the role provider itself or via a
custom IPrincipal implementation. That's the next option we will
explore.
Custom Principal
Another
option is to supply your custom IPrincipal implementation to WCF. This
gives you the chance to implicitly run code after the authentication
stage of each request. For this you have to create your own custom
principal and return it to the WCF plumbing. The custom principal will
then be available from Thread.CurrentPrincipal to the service code.
Custom principals allow full customization of role-based security and
expose specialized security logic for the service developer to use.
Writing
a custom principal is straightforward. Simply implement the IPrincipal
interface and add your own custom functionality. To integrate the
principal with WCF, you have to set the PrincipalPermissionMode
attribute in the ServiceAuthorization element to "custom" and provide an
authorization policy that is responsible for creating the principal and
giving it back to WCF.
An
authorization policy is simply a class that implements the
System.IdentityModel.Policy.IAuthorizationPolicy interface.
Implementations of this interface must employ a method called Evaluate,
which gets called on every request. Here you can reach into the WCF
service security context and set the custom principal. Figure 3 shows a custom principal as well as the authorization policy and its corresponding configuration entries.
Figure 3 A Custom Principal and Authorization Policy
Principal
class CustomPrincipal : IPrincipal { IIdentity _identity; string[] _roles; Cache _cache = HttpRuntime.Cache; public CustomPrincipal(IIdentity identity) { _identity = identity; } // helper method for easy access (without casting) public static CustomPrincipal Current { get { return Thread.CurrentPrincipal as CustomPrincipal; } } public IIdentity Identity { get { return _identity; } } // return all roles (custom property) public string[] Roles { get { EnsureRoles(); return _roles; } } // IPrincipal role check public bool IsInRole(string role) { EnsureRoles(); return _roles.Contains(role); } // cache roles for subsequent requests protected virtual void EnsureRoles() { // caching logic omitted – see the sample download } }
Authorization Policy
class AuthorizationPolicy : IAuthorizationPolicy { // called after the authentication stage public bool Evaluate(EvaluationContext evaluationContext, ref object state) { // get the authenticated client identity from the evaluation context IIdentity client = GetClientIdentity(evaluationContext); // set the custom principal evaluationContext.Properties['Principal'] = new CustomPrincipal(client); return true; } // rest omitted }
Configuration
<behaviors> <serviceBehaviors> <behavior name='security'> <serviceAuthorization principalPermissionMode='Custom'> <authorizationPolicies> <add policyType='Service.AuthorizationPolicy, Service' /> </authorizationPolicies> </serviceAuthorization> </behavior> </serviceBehaviors> </behaviors>
Centralizing Authorization Logic
So
far you have seen how you can access the role information that is
provided by the WCF security system from within your service operations.
Sometimes, however, it is also useful to have centralized logic that
can inspect every incoming request and make authorization decisions
without having to spread that logic over all your service operations.
Enter the service authorization manager.
The
service authorization manager is a class that derives from
System.ServiceModel.ServiceAuthorizationManager. You can override the
CheckAccessCore method to run custom authorization code for each
request. In CheckAccess, you have access to the current security context
as well as the incoming message, including the headers. When you return
false from CheckAccess, WCF will create the Access Denied fault message
and send that back to the client. A return value of true will grant
access to the service operation.
You
can find the unique identifier of the operation that the client tries
to call in the WS-Addressing action header. This value can be obtained
from IncomingMessageHeader.Action property on the operation context that
is passed into CheckAccess. The format of the action value is:
ServiceNamespace/ContractName/OperationName
For example, you might see something like this:
urn:msdnmag/ServiceContract/GetRoles
WCF passes in the
complete request message as a ref parameter. This allows inspecting and
even changing the message before it reaches the service operation (or
gets bounced back because of a negative authorization outcome).
Be
aware that messages in WCF are always read-once. That means you first
have to create a copy of the message before you inspect the body. This
can be done by creating a message buffer using the following code (you
need to be careful with large or streamed messages):
MessageBuffer buffer = operationContext.RequestContext.RequestMessage.CreateBufferedCopy( int.MaxValue);
Once you have created a copy of the message, you can use the standard APIs to get access to its content. In Figure 4,
you can see a sample implementation of a service authorization manager
that moves the authorization logic from the service operation to a
central place.
Figure 4 A Service Authorization Manager
Code
class AuthorizationManager : ServiceAuthorizationManager { public override bool CheckAccess( OperationContext operationContext, ref Message message) { base.CheckAccess(operationContext, ref message); string action = operationContext.IncomingMessageHeaders.Action; if (action == 'urn:msdnmag/IService/GetRoles') { // messags in WCF are always read-once // we create one copy to work with, and one copy for WCF MessageBuffer buffer = operationContext.RequestContext.RequestMessage. CreateBufferedCopy(int.MaxValue); message = buffer.CreateMessage(); // get the username value using XPath XPathNavigator nav = buffer.CreateNavigator(); StandardNamespaceManager nsm = new StandardNamespaceManager(nav.NameTable); nsm.AddNamespace('msdn', 'urn:msdnmag'); XPathNavigator node = nav.SelectSingleNode ('s:Envelope/s:Body/msdn:GetRoles/msdn:username', nsm); string parameter = node.InnerXml; // check authorization if (operationContext.ServiceSecurityContext.PrimaryIdentity.Name == parameter) { return true; } else { return (GetPrincipal(operationContext).IsInRole( 'administrators')); } } return true; } // rest omitted }
Configuration
<serviceAuthorization serviceAuthorizationManagerType='Service.AuthorizationManager, Service'>
Claims-Based Authorization
Once
you reach a critical mass of services and complexity, role-based
security may not be powerful or flexible enough. It doesn't take much
for an enterprise scale, service-oriented application to become
extremely complicated. Various clients using various client frameworks
and credential types talk to various back-end services. These back-end
services, in turn, call other services. Sometimes this is done in a
trusted subsystem fashion where only the direct caller is authorized.
Alternately, sometimes the original caller identity has to be forwarded
throughout the call chain. The situation starts to get really complex as
soon as external clients (such as partners or customers) need access to
some of your internal services.
It
quickly becomes apparent that, in a more complex system, the
capabilities of IIdentity and IPrincipal may not be sufficient to model
identity and authorization data. Role-based security is limited to
binary decisions and does not allow arbitrary data to be associated with
a subject. A technology-neutral format is needed that allows describing
the identities of entities participating in your distributed system.
This
is why the .NET Framework, starting in version 3.0, contains a new
identity and access-control API that can handle these requirements with a
claims-based approach. You can find this new API in the
System.IdentityModel assembly and namespace. Furthermore, Microsoft just
recently released a preview of its new identity framework code-named
"Zermatt." This new framework builds upon the classes and concepts found
in System.IdentityModel and makes it easier to build claims-based
security into services and applications. It is important to note that
System.IdentityModel as well as Zermatt are not bound in any way to WCF
and can be used to claims enable any .NET application. It just happens
that these APIs are deeply integrated into WCF.
The
most important structural classes from the System.IdentityModel
namespace that you need to understand are called Claim, ClaimSet,
AuthorizationContext, and AuthorizationPolicy. We will now have a closer
look at each of them and explore how they are integrated into the WCF
security system.
A
claim is a piece of information that can be associated with an entity
in your system. This is most commonly a user but could also be a service
or some resource. A claim consists of three pieces of information: a
claim type, the claim content, and whether the claim describes the
identity of the subject or a capability of the subject. This data
structure is represented by a class known as
System.IdentityModel.Claims.Claim. Furthermore, Claim is a DataContract,
which makes it serialization-friendly (important when you plan to
transmit claims over service boundaries):
[DataContract(Namespace = 'http://schemas.xmlsoap.org/ws/2005/05/identity')] public class Claim { [DataMember(Name = 'ClaimType')] public string ClaimType; [DataMember(Name = 'Resource')] public object Resource; [DataMember(Name = 'Right')] public string Right; }
ClaimType is a URI
that identifies the claim type. While you can come up with your own type
URI, several standard claim types are available from the ClaimTypes
class. A claim representing a name could use the URI value held by
ClaimTypes.Name:
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
The Resource
property contains the actual claim value. Note that the Resource is of
type object—this means that you could associate arbitrarily complex
information with that claim (from a simple string to a complete graph).
Just keep in mind that this may become a problem when it comes to
serialization, and if that is a concern, you should probably stick to
primitive types. The purpose of the Right property will become clearer
once we talk about claim sets.
You
can create a new claim by either using the Claim constructor or taking
advantage of one of the static methods on the Claim class (for standard
claim types):
Claim purchaseLimitClaim = new Claim( 'http://www.leastprivilege.com/claims/purchaselimit', 5000, Rights.PossessProperty); Claim nameClaim = Claim.CreateNameClaim('Alice');
A claim usually
doesn't arrive by itself. Claims are typically grouped in claim sets.
The class ClaimSet holds a list of claims as well as a reference to the
issuer of those claims:
[DataContract(Namespace='http://schemas.xmlsoap.org/ws/2005/05/identity')] public abstract class ClaimSet : IEnumerable<Claim>, IEnumerable { public abstract ClaimSet Issuer { get; } public abstract Claim this[int index] { get; } public static ClaimSet System { get; } public static ClaimSet Windows { get; } }
The issuer is an
important concept once your distributed system gets more complex. Claims
could come from various sources such as the current service, the
calling service, or a security token service. Having an issuer
associated with the set allows the service to distinguish between claim
sources (which may also influence trust decisions).
An
issuer is also described as a claim set, and System.IdentityModel ships
with two predefined issuer claim sets called System (for claims coming
from the system) and Windows (for claims coming from the Windows
security subsystem). Both are available as static properties on the
ClaimSet class.
Another
important concept is the claim set's identity. A claim set typically
needs a single claim that uniquely identifies the subject it describes.
This is where the Right property on the Claim class comes into play. A
claim set should have a single claim that has a Right value of Identity
(this is the unique identifier) and a number of claims with a Right
value of PossessProperty (additional claims describing the subject).
You
can create your own claim sets by creating a DefaultClaimSet instance
or, when you need more control, by deriving from the abstract class
ClaimSet:
DefaultClaimSet myClaimSet = new DefaultClaimSet(ClaimSet.System, new List<Claim> { new Claim(ClaimTypes.Name, 'Alice', Rights.Identity), new Claim(purchaseLimitClaimType, 5000, Rights.PossessProperty), Claim.CreateMailAddressClaim(new MailAddress('alice@leastprivilege.com')) });
System.IdentityModel
ships with two claim sets for converting Windows tokens and X.509
certificates to claims—these are WindowsClaimSet and
X509CertificateClaimSet, respectively. These two claim sets are used by
WCF for the Windows and Certificate client credential types.
ClaimSet
provides two processing primitives for querying the claims: FindClaims
and ContainsClaim. FindClaims returns a collection of claims for the
specified claim type, whereas ContainsClaim gives you information about
the existence of a specific claim.
Due
to the general-purpose nature of claim sets, you will typically end up
writing your own domain-specific extensions that work against the claim
set data structure. Extension methods in C# 3.0 are a convenient way to
extend these types with custom functionality. (I have written a library
of extension methods for Claim, ClaimSet, and related types that you can
download from leastprivilege.com/IdentityModel.)
AuthorizationContext
The
last missing piece in the IdentityModel puzzle that you need to know
before doing some real work with claims is the authorization context,
which acts as a container for claim sets as well as transformation
policies (more on this later). WCF makes the authorization context
available via the thread-static ServiceSecurityContext. You can use the
code snippet shown in Figure 5 to dump the claim sets and claims that are associated with the current service operation request.
Figure 5 Finding Claims and Claim Sets
public void ShowAuthorizationContext() { AuthorizationContext context = ServiceSecurityContext.Current.AuthorizationContext; foreach (ClaimSet set in context.ClaimSets) { Console.WriteLine('\nIssuer:\n'); Console.WriteLine(set.Issuer.GetType().Name); foreach (Claim claim in set.Issuer) { ShowClaim(claim); } Console.WriteLine('\nIssued:\n'); Console.WriteLine(set.GetType().Name); foreach (Claim claim in set) { ShowClaim(claim); } } } private void ShowClaim(Claim claim) { Console.WriteLine('{0}\n{1}\n{2}\n', claim.ClaimType, claim.Resource, claim.Right); }
When you
place this code into a WCF service, you will see different output
depending on the configured client credential type. If Windows
authentication is enabled, the WCF-generated claim set will contain the
user's SID (identity claim), the group's SIDs, and the user name. For a
request that is authenticated using a client certificate, the claim set
will contain claims that describe the subject name, public key,
thumbprint (identity claim), expiration dates, and so on. For users that
authenticate with a simple user name/password pair, there will be only a
single user name identity claim.
So
you can see that the WCF integrated claims layer converts
technology-specific identity information, such as a Windows token or
certificate, to a general-purpose data structure that can be queried
using standard APIs. This makes writing services that have to support
multiple credential types much easier—you are not hardwired to any
technology-specific APIs anymore. If new credential types are added to
WCF, the corresponding plumbing will take care of converting that
proprietary format to claims as well.
Claims Transformation
Service
operations typically don't care about the user's SID or certificate
thumbprint, but they do care about domain-specific identity information
such as a user identifier, e-mail address, or purchase limit. Claims
transformation is the process of transforming the technology-specific
identity details into application-specific ones.
Now
that you have all identity information in a common format, it is easy
to parse and analyze the claims information to create new claims that
make more sense in the application context. Based on the identity claim
of the WCF-generated claim set, you can map the request to an
application user ID and add the relevant identity and authorization
information to a new claim set. The service operation would then simply
reach into the WCF authorization context to retrieve the information in
which it is interested, as illustrated in Figure 6.
Figure 6 Claims Transformation (Click the image for a larger view)
Claims
transformation is accomplished in an authorization policy. We have
already used an authorization policy to create a custom principal, but
this time it has a different purpose. Authorization policies can take
part in the claims-generation process and run after the WCF internal
claims generation is completed. That means the claims associated with
the caller's credentials are already accessible to you.
You
can query the existing claims for identity information and, based on
that, create a new claim set that models your domain-specific claims.
Then you can add it to the list of claim sets. This list will be used to
create the authorization context before the request reaches the service
operation.
There are
three members on the IAuthorizationPolicy interface that you have to
implement. Id returns a unique identifier for the policy (usually a
GUID), and Issuer returns a claim set describing the issuer of the
claims that this policy creates. The most important method, however, is
Evaluate. This method receives an evaluation context, which basically
represents the authorization context while it is still in the process of
being built.
You get
access to all currently generated claims via
EvaluationContext.ClaimSets. Don't try to touch the
ServiceSecurityContext from within the Evaluate method, as this will
trigger the authorization policies again and you'll end up in an
infinite loop. You can add a new claim set to the evaluation context by
calling EvaluationContext.AddClaimSet—this claim set will become part of
the authorization context later on.
Figure
7 shows a sample authorization policy that implements a common pattern.
It first retrieves the identity claim of the first claim set. This
claim is sent to a mapping component, which inspects the claim and
returns an application user ID. After that the policy hits a data store
using the ID to retrieve the information that should become part of the
claim set. This information is then packaged as a new claim set and
added to the evaluation context.
Figure 7 An Authorization Policy
class CustomerAuthorizationPolicy : IAuthorizationPolicy { Guid _id = Guid.NewGuid(); // custom issuer claim set ApplicationIssuerClaimSet _issuer = new ApplicationIssuerClaimSet(); public bool Evaluate(EvaluationContext evaluationContext, ref object state) { Claim id = evaluationContext.ClaimSets.FindIdentityClaim(); string userId = Map(id); evaluationContext.AddClaimSet(this, new CustomerClaimSet(userId)); return true; } public ClaimSet Issuer { get { return _issuer; } } public string Id { get { return 'CustomerAuthorizationPolicy: ' + _id.ToString(); } } }
The final
step is to add the authorization policy to the serviceAuthorization
behavior configuration. You can add multiple policies, and they are
invoked in the order you add them. This means you can write multi-step
authorization policies where one policy relies on values added by a
previous one. This is pretty powerful for more complex scenarios:
<serviceAuthorization> <authorizationPolicies> <add policyType='LeastPrivilege.CustomerAuthorizationPolicy, Service' /> <add policyType='some_other_policy' /> </authorizationPolicies> </serviceAuthorization>
If you ran the
previous code again to inspect the authorization context, you will see
the new claim set and claims. The service operation would use similar
code to check the claims for authorization (see Figure 8).
Figure 8 Checking Claims Authorization
public void PlaceOrder(Order order) { int purchaseLimit = GetPurchaseLimit(); if (Order.Total > purchaseLimit) { // do appropriate action } } private int GetPurchaseLimit() { AuthorizationContext context = ServiceSecurityContext.Current.AuthorizationContext; foreach (ClaimSet set in context.ClaimSets) { foreach (Claim claim in set.FindClaims( Constants.PurchaseLimitClaimType, Rights.PossessProperty)) { return int.Parse(claim.Resource.ToString()); } } throw new Exception('Claim not found'); }
The service
authorization manager also works with claims-based authorization. WCF
passes the operation context into the CheckAccessCore method. From there
you can reach into the service security context that will give you
access to the authorization context. This allows you to centralize
certain authorization decisions in a single place.
Security Token Services
A
security token service (STS) is a tool that allows further
consolidation of security logic. The typical task of an STS is to
authenticate users and create a security token that, in turn, can
contain claims. Clients must first authenticate with the STS and then
forward the returned token to the service with which the client wants to
communicate.
Since
the STS knows about that service (this information is part of the token
request), it can do central authorization as well as pre-generate the
claims on which the service relies. This way the claims transformation
does not need to happen on the service endpoint at all but can be done
centrally by the STS. This can dramatically streamline your security
infrastructure when your system reaches a certain level of complexity.
A
security token service is also an important infrastructure component
when it comes to federating multiple trust domains. By establishing
trust between several token services, you can exchange security tokens
over the trust boundary that can be used by services.
WCF
has automatic client/service-side support for the previous scenario as
well as all the base classes needed to write an STS. But correctly
implementing all the related WS-* specs is a complicated task. Instead,
you should either buy a commercial STS or use a higher-level toolkit,
such as Zermatt, to write a custom one. The upcoming version of
Microsoft Active Directory Federation Services is designed to be a
full-featured STS for WCF.
0 comments:
Post a Comment