What Gives You the Right?

Combine the Powers of AzMan and WSE 3.0 to Protect Your Web Services

Niels Flensted-Jensen

This article is based on a prerelease version of Web Service Enhancements 3.0. All information is subject to change.

This article discusses:

  • Securing Web services using Web Service Enhancements 3.0
  • Writing custom policy assertions
  • Integrating with Enterprise Library and Authorization Manager for task-based authorization
This article uses the following technologies:
.NET Framework 2.0, WSE, Authorization Manager, and Enterprise Library

Code download available at:AzManandWSE30.exe(149 KB)

Contents

Extensions to Basic Messaging
Trusted Subsystems and Authorization
WSE to the Rescue
Simple Client-to-Service Scenario
MethodAuthorization Policy Assertion
Getting the Assertion into the Policy Pipeline
Filtering Messages Passing Through the Pipeline
Enterprise Library AzMan Provider
Dynamic Authorization
Making Authorization Claims and Enabling the Client
The Bigger Picture

In the world of connected Web services, security is a central issue. However, transaction handling, reliable messaging, and performance are other issues that present significant challenges. All of these issues are helped by addressing the need for fine-grained, dynamic authorization. In this article, I show you how to engineer an approach for the declarative specification of authorization requirements for individual service endpoints. I deliberately use the verb "engineer" as opposed to craft or program because my focus here is on building a solid solution from ready-made parts of the platform and tools available.

I demonstrate how you can combine existing and new Microsoft technologies with minimal new code to provide flexible authorization for individual Web service methods. Windows® 2003 Authorization Manager, Web Service Enhancements (WSE) 3.0, and Enterprise Library all play a part. By inserting a simple policy assertion into your policy file, you get task-based authorization that goes beyond what can be achieved by, for example, applying System.Security.Permissions.PrincipalPermissionAttribute to your code. But before producing the solution, let's take a look at the foundation that allows us to do things like this in Web services.

Extensions to Basic Messaging

Central to solving the challenges faced by the service designer and implementer are the many Web service (WS) specifications. Their level of stability and adoption varies greatly. Note, however, when arguing the case for adoption, that only in a few cases are these specifications actual ratified standards. Each specification, such as WS-Addressing or WS-Security, addresses separate concerns in the communication that occurs with and between your services, and as such represents stackable protocols that may be combined as necessary according to your needs. For instance, transport encryption and traditional URLs must be augmented by message-level security (WS-Security, WS-SecureConversation) and addressing (WS-Addressing) when introducing intermediaries into the service infrastructure.

The visible manifestation of the WS specifications will be primarily the introduction of headers into the SOAP envelope carrying the Web service message. But depending on the type of WS protocol, the actual contents of the SOAP body may also be altered, as is the case when applying message-level encryption. Figure 1 shows two examples of the same SOAP envelope. The second version has the WS headers applied along with their corresponding namespaces. Note that the body stays the same. When developing solutions, you will notice the growing complexity of the header section as you start applying various aspects, such as WS-Addressing and WS-Security.

Figure 1 Applying WS Headers to a SOAP Envelope

Without WS Headers

<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema"> <soap:Body> <ViewInvoices xmlns="https://example.org/invoices" /> </soap:Body> </soap:Envelope>

With WS Headers

<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:wsa="https://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsse="https://docs.oasis-open.org/wss/2004/01/oasis-200401-wss- wssecurity-secext-1.0.xsd" xmlns:wsu="https://docs.oasis-open.org/wss/2004/01/oasis-200401-wss- wssecurity-utility-1.0.xsd"> <soap:Header> <wsa:Action>https://example.org/invoices/ViewInvoices</wsa:Action> <wsa:MessageID> uuid:3db181be-3053-4f1d-8e2b-f5c22e9a4c44</wsa:MessageID> <wsa:ReplyTo> <wsa:Address> https://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous </wsa:Address> </wsa:ReplyTo> <wsa:To> https://localhost/SecureInvoiceService/Part1/Before/ SecureInvoiceService.asmx </wsa:To> <wsse:Security> <wsu:Timestamp wsu:Id="Timestamp-c18214c0-8318-4d33-ba21-8820277ac1e4"> <wsu:Created>2005-07-08T06:34:54Z</wsu:Created> <wsu:Expires>2005-07-08T06:39:54Z</wsu:Expires> </wsu:Timestamp> </wsse:Security> </soap:Header> <soap:Body> <ViewInvoices xmlns="https://example.org/invoices" /> </soap:Body> </soap:Envelope>

Trusted Subsystems and Authorization

Before moving on to a discussion of authorization and some example code, let's look at how services are deployed and how they interoperate. Each service should run under the identity of a domain service account created specifically for that service during installation. This way, the clear text password will never be known to any user or machine, and the service may be locked down very tightly with regard to access to file system, registry, network, Active Directory®, and so on. Services may be deployed in a trusted subsystem model in which each service trusts the previous service to perform appropriate authentication. A more granular approach requires performing some custom authorization in the service code. For this, you need to extend the service infrastructure not only to check authorization of the service sending you a message, but also to check the identity of the user or machine that is requesting for the action to be performed.

Figure 2 illustrates the trusted subsystem model inside an Active Directory domain with NTFS security supplementing WS-Security-based authentication and authorization. Note that no matter which user succeeds in accessing the service, the server will never impersonate the user. This allows for tight and predictable lockdown of the service.

Figure 2 Locking Down Services While Passing User Identity

Figure 2** Locking Down Services While Passing User Identity **

If you develop ASP.NET Web applications, you know that users may be checked for role-based authorization in various ways and at various levels (application, directory, or page). But this only works for standard .NET Framework Web services if you are using Windows-integrated security. The same restriction applies to IIS 6.0 URL authorization, which takes place even before ASP.NET has a chance to kick in with something like forms-based authentication. And even if you work within the integrated security constraints, you are still not inherently able to transfer, much less authorize, identities on whose behalf a service performs work.

In ASP.NET, the default support for Web services only supports basic scenarios and does not provide the mechanisms and hooks required to build secure service-oriented architectures out of the box. WSE 3.0 addresses this issue, and Windows Server™ 2003 Authorization Manager (AzMan.msc) provides a good platform for doing fine-grained and flexible authorization.

WSE to the Rescue

WSE is a developer-focused security product for Web services. It allows you to implement message-level security without having to focus on the specification details. As I'm writing this, WSE 3.0 is in its Beta release. It promises wire-level compatibility with the Windows Communication Foundation (formerly codenamed "Indigo"). For more details on WSE 3.0 features and for related downloads, see Web Services Enhancements.

WSE features, including the various security functions, are built around the concept of a pipeline. The pipeline is where incoming and outgoing messages are intercepted and validated, transformed, logged, and possibly checked for authorization.

As Figure 3 illustrates, a pipeline handles both input and output messages and is configured entirely by its policy. Policies are made up by one or more policy assertions that apply at both input and output. Each is implemented using a fairly simple PolicyAssertion assertion that contains up to four SoapFilter filters (output client, input client, input service, output service). Here I'll concentrate on the methodAuthorization assertion illustrated in Figure 3.

Figure 3 WSE 3.0 Pipeline and Policy

Figure 3** WSE 3.0 Pipeline and Policy **

Note that WSE 3.0 already contains a standard authorization assertion, which provides functionality borrowed from ASP.NET role authorization. But this does not cover individual Web methods or task-based authorization. This feature can be enabled using the WSE 3.0 policy wizard available from Visual Studio® 2005.

Simple Client-to-Service Scenario

To demonstrate the concepts discussed here, my starting point will be the security hands-on lab (WSE 3.0 Hands On Lab—Exploring Security) available from the WSE Web site at Web Services Enhancements. Specifically, I used the Part3_After solution as the starting point for developing and configuring the new authorization features.

My example Invoice Manager uses a simple Windows Forms client application (shown in Figure 4), which communicates with a Web service with four end points: View, Submit, Approve, and Pay. Unlike the sample in the WSE hands-on lab, this version uses Kerberos tokens for both signatures and encryption, thereby removing the need to install a service certificate. My example is put together so that both the client and server simply run on the same machine, but this could easily be modified to do otherwise.

Figure 4 Invoice Manager Client

Figure 4** Invoice Manager Client **

The intention is to guard access at the service level to ensure that users attempting to View, Submit, and so on are actually authorized to do so. To this end AzMan is used to provide flexible authorization. AzMan allows for very flexible configuration of access rights in a way fully separated from both client and service development. Figure 5 shows the AzMan interface used to configure authorization for the sample application. Note the Role Assignments node, which is where you will be adding yourself or other users who will test the authorization functionality of the developed WSE 3.0 policy assertion.

Figure 5 Authorization Manager

Figure 5** Authorization Manager **

All the different elements of the solution will fit together at run time as illustrated in Figure 6. The client sends messages through its WSE proxy to the service, where the WSE pipeline intercepts the messages to perform further verification, authentication, and authorization. The authorization step will rely on the Enterprise Library AzMan authorization provider, which will access Authorization Manager through the managed AzMan API. This will be based on claims made as part of the configured policy assertion in the policy file. (See the "Enterprise Library" sidebar for background information.)

Figure 6 Sample Authorization Implementation

Figure 6** Sample Authorization Implementation **

MethodAuthorization Policy Assertion

Coding your own authorization assertion to insert into the WSE pipeline is straightforward, as is configuring it for use with an actual service policy. The notable risk is configuring the assertion in the wrong pipeline position. Placing it before the authentication assertion will cause the authorization assertion to fail because the identity for authorization has not yet been discovered.

Figure 7 shows the code required to prepare your assertion to be properly hooked into the WSE pipeline. The assertion and its corresponding filters are created once on startup, and the same instances are then used for the duration of the application's lifetime.

Figure 7 MethodAuthorization WSE 3.0 Policy Assertion

public class MethodAuthorizationAssertion : PolicyAssertion { private string authorizationProviderName = null; private Dictionary<string, AuthorizationData> actionAzList = null; public override SoapFilter CreateClientInputFilter( FilterCreationContext context) { // not applicable to the client pipeline throw new NotSupportedException( "The MethodAuthorizationAssertion assertion " + "is not to be applied on the client." ); } public override SoapFilter CreateClientOutputFilter( FilterCreationContext context) { // not applicable to the client pipeline throw new NotSupportedException( "The MethodAuthorizationAssertion assertion " + "is not to be applied on the client." ); } public override SoapFilter CreateServiceInputFilter( FilterCreationContext context) { return new MethodAuthorizationInputFilter(this); } public override SoapFilter CreateServiceOutputFilter( FilterCreationContext context) { // nothing to do on the way out return null; } public override void ReadXml(XmlReader reader, IDictionary<string, Type> extensions) { if (reader == null) throw new ArgumentNullException("reader"); // locate the name of the start element string startElementName = null; foreach (KeyValuePair<string, Type> pair in extensions) { if (pair.Value == typeof(MethodAuthorizationAssertion)) { startElementName = pair.Key; break; } } bool isEmpty = reader.IsEmptyElement; authorizationProviderName = reader.GetAttribute( "authorizationProvider"); reader.ReadStartElement(startElementName); if (!isEmpty) { string actionName = null; if (reader.MoveToContent() == XmlNodeType.Element) { do { if (reader.Name != "action") throw new FormatException( "The <methodAuthorization> assertion " + "may only contain <action> elements."); actionName = reader.GetAttribute("name"); if (actionName == null || actionName.Length == 0) throw new FormatException("The <action> " + "element must contain a 'name' attribute." ); AuthorizationData azData = new AuthorizationData(); azData.Rules = reader.GetAttribute("rules"); azData.Tasks = reader.GetAttribute("tasks"); azData.Operations = reader.GetAttribute("operations"); if (actionAzList == null) actionAzList = new Dictionary<string,AuthorizationData>(); actionAzList[actionName] = azData; } while (reader.Read() && reader.MoveToContent() == XmlNodeType.Element); } reader.ReadEndElement(); } } ... // private SoapFilter defined here }

To implement a class derived from the abstract PolicyAssertion class, you must implement the four missing filter creation methods. Since I'm creating an assertion to be used only at the service interface for authorization of incoming messages, I'm really only interested in the CreateServiceInputFilter method. Therefore, I return null from CreateServiceOutputFilter.

To configure the authorization assertion with the desired authorization claims, using the Enterprise Library Authorization Provider of my choice, I must override the ReadXml method that reads in the assertion configuration from the policy file.

I will get back to the actual implementation of the SoapFilters shortly, but let's first see how the newly implemented service is configured to fit into your policy pipeline.

Getting the Assertion into the Policy Pipeline

Once you have implemented your new policy assertion, the next step is to configure a policy to include the assertion in the pipeline. First, set up a WSE 3.0 policy for your Web service application to use. Once you have installed WSE 3.0, bring up the Web service project in Visual Studio® 2005 and use the context menu on the project in solution explorer to bring up the WSE configuration tool. Enable both WSE and the WSE SOAP protocol factory. On the Policy tab, add or remove policy definitions as necessary. Note that once a policy is set up, further modifications will have to be done by hand. This is when you will probably appreciate the simplified WSE 3.0 policy configuration format shown in Figure 3. (If you've ever looked at a WS-SecurityPolicy configuration as used in WSE 2.0, you've stared complexity right in the face—and yet you've still managed to stay at it or you wouldn't be reading this!)

First, you tell WSE that you have produced an extension, you give it a name to be referenced by, and then you tell it which type implements the assertion. That said, let me just briefly comment again on the policy format. WS-Policy has been superseded inWSE 3.0 with a simpler format that addresses common security scenarios when developing Web services today.Enterprise Library

Enterprise Library is a framework, developed by the Microsoft patterns & practices team, that consists of a set of application blocks that provide generic solutions to many of the problems you run into every day, such as configuration, security, data access, exception handling, and logging. The Configuration Console saves you from having to edit configuration files manually. As full source code is provided along with the compiled blocks, you're free to modify or extend the application blocks to meet your needs. For more information visit Enterprise Library.

Information about using the sample application with the .NET Framework 2.0 and the June 2005 release of Enterprise Library is provided in the readme file that accompanies the code download for this article.

Filtering Messages Passing Through the Pipeline

After setting up the assertion, I then implement classes derived from Microsoft.Web.Services3.SoapFilter to do the actual filtering (see Figure 8).

Figure 8 Checking the Sender's Authorization

private class MethodAuthorizationInputFilter : SoapFilter { private IAuthorizationProvider azProvider = null; private MethodAuthorizationAssertion assertion = null; public MethodAuthorizationInputFilter( MethodAuthorizationAssertion assertion ) : base() { if (assertion.authorizationProviderName == null || assertion.authorizationProviderName.Length == 0) { azProvider = AuthorizationFactory.GetAuthorizationProvider(); } else { azProvider = AuthorizationFactory.GetAuthorizationProvider( assertion.authorizationProviderName ); } if (azProvider == null) throw new SecurityFault( "Authorization configuration error" ); this.assertion = assertion; } public override SoapFilterResult ProcessMessage( SoapEnvelope envelope ) { WindowsPrincipal principal = envelope.Context.IdentityToken.Principal as WindowsPrincipal; if (principal == null) throw new SecurityFault( SecurityFault.SecurityTokenUnavailableMessage ); if (!CheckAccessToMethod( envelope.Context.Addressing.Action.Value, principal )) throw new SecurityFault( "Not authorized to access this endpoint.", SecurityFault.FailedCheckCode ); return SoapFilterResult.Continue; } ... }

MethodAuthorizationAssertion constructs MethodAuthorizationInputFilter to process an incoming SOAP message and check the authorization of the sender.

The constructor of the filter is supplied with a reference to the relevant assertion, which will be used to access the configuration data (the AzMan provider and authorization claims) during message processing. The actual processing takes place in the ProcessMessage method, which will start by picking out the principal representing the identity of the sender of the message. You should note that it will look for a WindowsPrincipal. The AzMan provider that is delivered with Enterprise Library supports only that type, though AzMan may accept any Security Identification (SID) as identification of a principal.

Once the identity of the sender has been established, the rest is simply a matter of checking the authorization claim associated with the WS-Addressing Action header as contained in the incoming SoapMessage (this example makes sure to require an Action header through the <requireActionHeader> policy assertion). Remember from Figure 3 that this claim was configured as part of the policy assertion in the policy file.

Enterprise Library AzMan Provider

When the action header has been retrieved and the identity of the sender established, it's time to perform the actual authorization check. Enterprise Library includes a nice configuration tool and provider factory engine, so I simply applied the Enterprise Library AzMan Provider to this task. The provider implements Microsoft.Practices.EnterpriseLibrary.Security.IAuthorization which has only one method to be concerned with: Authorize. Since the provider has already been hooked up in the constructor of MethodAuthorizationInputFilter, all you need to do is simply issue the call like this:

authorized = azProvider.Authorize( principal, task );

The authorization code implemented by the CheckAccessToMethod method, shown in Figure 9, has several steps that look for Rules, Tasks, and Operations. Rules are specific to another Enterprise Library authorization provider, the Authorization Rule Provider, whereas Tasks and Operations are specific to the AzMan provider. Performance hasn't been a big concern, but if it were, reuse of providers and internal optimization of the AzMan provider might be worthwhile.

Figure 9 Looking for Rules, Tasks, and Operations

private bool CheckAccessToMethod(string action, WindowsPrincipal principal ) { // No authorization claims has been made if (assertion.actionAzList == null) return true; bool authorized = true; AuthorizationData azData; if (assertion.actionAzList.TryGetValue( action, out azData ) && azData != null) { // Run through Tasks, Operations, and Rules. As long as one // evaluates to true the token.Principal is authorized. authorized = azData.Rules == null && azData.Tasks == null && azData.Operations == null; if (!authorized) { foreach (string rule in azData.GetRuleList()) if (authorized = azProvider.Authorize(principal, rule)) break; if (!authorized) { foreach (string task in azData.GetTaskList()) if (authorized = azProvider.Authorize(principal, task)) break; if (!authorized) { foreach (string op in azData.GetOperationList()) if (authorized = azProvider.Authorize( principal, string.Format("O:{0}", op))) break; } } } } return authorized; }

It should also be noted that although the AzMan provider supports only WindowsIdentities, it may well be extended to support other SIDs. This will require extending the functionality in the GetClientContext method of the AzMan provider a bit to initialize the AzMan context using IAzApplication.InitializeClientContextFromStringSid.

Dynamic Authorization

So far I have been concerned with how to check whether a principal is authorized for a specific task. But I haven't stopped to describe which principals will actually be authorized. Of course, authorizations are managed by the Authorization Manager Microsoft Management Console (MMC) snap-in, illustrated in Figure 5.

Before getting anything useful from AzMan, the application must be defined—the InvoiceApproval application in the case of this article. Operations must be defined, then grouped into tasks, which must be assigned to roles. Roles, in turn, are where you assign domain users and groups. Once this setup is in place, authorization checks, in the simple scenario, are done by checking whether one of the roles a principal is assigned to has been transitively granted the right to execute a task.

The sample includes a simple definition with four tasks and five roles (see Figure 5). Note that the Users Restricted by Business Hours not only has a set of users and groups assigned to it, but also has an associated business rule, which means that the users and groups are members of the role only if the business rule evaluates to True. (This particular business rule simply checks whether the time of day is between 9:00 AM and 5:00 PM.)

As you may imagine at this point, the implication of inserting AzMan is that you may dynamically assign users and groups to tasks, create new roles, and define new business rules. In short, you have achieved an extra degree of freedom while at the same time postponing authorization to be determined at the time of operation as opposed to at development time.

Making Authorization Claims and Enabling the Client

At this point, the entire infrastructure is basically ready. The pipeline and related functionality have been put in place and Authorization Manager is set up. All I need now is a way to specify the authorization claims at the service and to set up the Web service proxy at the client side to send along the required headers.

As described earlier, the WSE 3.0 authorization assertion I've developed is configured entirely in the policy file, as shown in Figure 3, using claims like:

<action name="https://msdn.microsoft.com/msdnmagazine/invoices/ViewInvoices" tasks="ViewInvoices" />

Note, though, that the tasks attribute may include more than one task. For example:

tasks="Task1;Task2"

With this syntax, I indicate that the sender should be authorized for at least one of these tasks. This purely declarative configuration approach enables authorization to be inserted into the WSE pipeline at any time after the service has actually been deployed, as long as the client has been configured or programmed to supply their identity as a KerberosToken in the WS-Security SOAP header. The developer of the service is therefore relieved of making the right authorization checks imperatively in code (for example, by using the IsInRole method of IPrincipal), as long as a list of endpoints is supplied for operations to configure as part of policy setup and maintenance.

At the client side, you simply need to make sure to use the WSE 3.0 proxy and then attach a Kerberos token for constructing the message according to the policy file at the client side. This file is similar to that on the service side, except no authorization assertions are present on this side of the message exchange.

The code in Figure 10 shows the code that is necessary for putting together a message with the format required by the service side so it can both authenticate and authorize the sender. Note how the policy is set and the Kerberos token is attached. After doing this, you're ready to send messages.

Figure 10 Setting Up the WSE Proxy at the Client Side

private SecureInvoiceServiceWse GetProxy() { SecureInvoiceServiceWse proxy = new SecureInvoiceServiceWse(); // Specify the policy to apply proxy.SetPolicy( "KerberosPolicy" ); string hostName = proxy.Url.IndexOf( "localhost" ) >= 0 ? System.Net.Dns.GetHostName() : new Uri( proxy.Url ).Host; KerberosToken kerberosToken = new KerberosToken( "host/" + hostName ); proxy.SetClientCredential( kerberosToken ); return proxy; }

The Bigger Picture

Software engineering is all about platforms, languages, and tools. Applying an engineering approach to service infrastructure and security within the domain of established architectures requires evaluating the existing nuts and bolts that are available to provide a solution. You cannot develop robust and secure services without thoroughly understanding the operating system, the hosting options, and the tools. In my experience, whenever you consider "rolling your own," you either don't fully understand the thinking behind your platform or you have a very compelling cost/benefit ratio for the work that needs to be done.

Apart from the .NET Framework 2.0 and WSE 3.0 being Beta technologies, there are two particular areas that stand out as needing work beyond what is presented here.

Enterprise Library AzMan Provider The AzMan provider that is delivered with Enterprise Library works fine, but is limited in support for more advanced AzMan capabilities. The challenge is to write a new provider with full AzMan support without breaking the Enterprise Library IAuthorizationProvider interface. Thread-safe caching and pooling of open connections and contexts could also be considered for reducing the overhead that is incurred by the authorization step.

Authorization Manager AzMan is a feature of Windows Server 2003 definitely worth your consideration, but be prepared to address some staging and deployment issues. Also, if you work with custom SIDs that don't exist within Active Directory, which may be the case for an Internet solution, you may run into performance issues as the number of SIDs inside the AzMan store increases.

With the .NET Framework 2.0 and WSE 3.0, if you keep with the good engineering practices related to the trusted subsystem approach, and if you generally work along the lines of the platform, you can take one more small step towards securing your services without giving up flexibility in your production environment.

Niels Flensted-Jensen, a software architect and engineer, is a founder of CI Networks, a Danish software consultancy and component developer. Niels focuses on service-and process-oriented enterprise development centered on the Microsoft platforms.