WIF and WF play nicely together in securing your workflow services
Read part 2 of this article series here.
This article is part one of a two-part series on how to leverage Windows Identity Foundation (WIF) to secure workflow services. In this article, I'll focus on how WIF simplifies the process of securing a workflow service using claims such as user name and role, as well as other attributes. The process is so simplified that you won’t need the activities from Workflow Services Security Pack (WFSP) until you decide to use the claims received by the service to call out to another service (e.g., the workflow service itself becomes the client of another service).
In part two, I'll explain how to leverage WIF, Claims to Windows Token Service, and Active Directory Federation Services (ADFS) 2.0 when you're dealing with identity issues resulting from services calling other services.
For the purposes of this article, I'll create a real-world scenario around the fictitious widgets supply company, Contoso. The company offers an application for ordering widgets called WidgetsNow!, a WPF thick-client application.
In this scenario, employees within the Contoso network can place orders for widgets and check the status of existing orders by using the WidgetsNow! client application (Figure 1). As you can see, when placing an order a user chooses an item, indicates a quantity, and checks whether or not the order should be expedited. Note that users do not explicitly log in. When a user runs the application, he's already logged in with his domain credentials.
The Current State
You can download the project files (both the current and envisioned state) at http://tejadanet.typepad.com/SampleCode/WidgetsNowSamples.zip. In the current state, the Visual Studio Solution contains two projects: one describing the WPF client (WidgetsNowApp); the other describing the Service (WidgetsNowService). The WidgetsNowService contains a WCF Workflow Service (XAMLX) that defines two operations (Figure 2)—PlaceOrder and GetOrderStatus—for submitting new order requests and requests for order status, respectively. Currently, the service is unsecured, so it's callable by anyone with access to it.
The crux of the WidgetsNowService is a Parallel activity containing two branches, which Figure 3 shows. This Parallel activity allows the client to place an order by calling PlaceOrder (this is the left Sequence) and, at any time, to call GetOrderStatus to check on an order's status (the right Sequence).
The PlaceOrder operation takes as input three values: a string naming the item to order, an integer indicating the quantity, and a boolean value indicating whether the order should be expedited. The operation immediately returns to the caller, via the SendResponse activity, an Order ID in the form of a new GUID converted to a string; the order status is set to "In Process." The Process Order sequence shown simulates a time-consuming process—an If activity examines whether the user requested the order to be expedited (by passing in true to the last parameter of PlaceOrder). If so, a Delay is executed that waits for only 5 seconds. If not, a Delay is executed that waits for 10 seconds. For simplicity, after processing is complete the order status is set to "Shipped."
The right parallel branch places the GetOrderStatus ReceiveRequest and SendReply in a While loop as a simple way to always allow calls to GetOrderStatus after an order has been placed. The GetOrderStatus operation takes as input the string Order ID that was previously returned to the caller in the call to PlaceOrder. Correlation has been configured on this Order ID parameter so that repeat calls to GetOrderStatus always get the up-to-date status from the appropriate workflow instance. Sending an invalid Order ID results in an error. As an aside: In a real-world implementation, you would want to configure the Condition of the While to turn false at some point so that the workflow can complete. The SendReply returns the string status to the caller, either "In Process" or "Shipped."
The Envisioned State
Contoso customers have asked to have direct access to the WidgetsNow! Application, instead of always having to rely on Contoso employees. As Contesso prepares to make the WidgetsNow! Application and Service available to its partners, it wants to secure the service so that only authorized users can access it. The goals of this phase of the project are to add new authentication and authorization policies:
- Externalize as much authentication and authorization work as possible, decoupling it from the workflow service definition.
- Use the existing identity providing infrastructure: continue to use Windows credentials, leveraging the Active Directory that’s already in place.
- Apply new authorization rules by securing the service according to following rules:
- PlaceOrder operation should be available only to users in the CONTOSO domain.
- In addition, expedited orders can be placed only by users in the SeniorManagers group.
- PlaceOrder operation should be available only to users in the CONTOSO domain.
- The GetStatus operation should be available to all users.
Figure 4 illustrates my approach to implementing the requirements described above by leveraging WIF. Let’s walk through the communication pattern shown in the diagram. Then I'll drill into how I implemented each piece.
- The client application has a reference to the Service, and from that knows that it will need to present a security token containing a name claim and a CanExpediteOrders claim in order to be able to call the Service. The client application can request this token by authenticating with another web service or the Contoso Security Token Service (STS). The client application is referred to as an active client because it proactively calls the STS first and then the Service.
- The Contoso STS accepts the security token containing the Windows-based claims, validates them, and—if everything checks out—returns the requested token containing a name and CanExpediteOrders claim. This is often referred to as an Issuer of claims.
- Now that it has the required token, the client can call the service (for example, by invoking the PlaceOrder operation) and present the token.
- A custom ClaimsAuthorizationManager (CAM) is placed in the message processing pipeline that examines the claims presented in the token and can make broad allow or deny decisions on a per operation basis. In this case, if PlaceOrder is the operation being invoked, then the user name presented is checked to see if it belongs to the Contoso domain. If it does, then the request is allowed to flow through to the workflow service. If it doesn't, then the client gets an access denied error. If GetOrderStatus is called, the CAM is configured to allow the call regardless of the user’s domain.
- The request makes it to the workflow service (also known as the Relying Party or RP in Identity terminology) for processing and possibly additional authorization. In most cases, this results in a successful call with the desired value being returned. In the case of a call to PlaceOrder where a user elected to expedite an order, the CanExpediteOrder claim is examined for a true value. If the claim’s value is false or the CanExpediteOrder claim is not present, a fault exception is returned to the client informing him that he is not allowed to place an expedited order. In the case of a call to GetOrderStatus, no additional authorization against the claims takes place in the workflow.
Now let’s look at the four major steps needed to implement this approach:
- Add the STS
- Secure Service Operations
- Authorize within the Workflow Service
- Update the Client Application
Add the STS
The primary purpose of inserting an STS into an architecture such as this is to externalize authentication and allow the Relying Party (the application being secured) to only have to worry about a single set of claims, irrespective of how callers actually authenticate and what claims they might present. Currently, the callers use Windows credentials, but if they were to switch to username/password, only the STS would need to be updated—the application would remain unchanged.
The straightforward process of adding an STS to a workflow service is similar to how you would add a reference to another web service. In this scenario, you use a custom STS based on the template provided by the WIF SDK. To modify the original solution to use an STS, I performed the following steps:
- Right-click the WidgetsNowService project and select Add STS Reference. This will launch the Federation Utility (FedUtil.exe), a wizard that helps you configure your service to use an STS.
- On the welcome screen, enter the address of your workflow service for the Application URI field. On our machine, this was http://fsweb.contoso.com/WidgetsNowService/OrderService.xamlx
- The Security Token Service screen lets you pick between two options for adding an STS. You can create a new STS project that is added to the current solution or you can point to an existing STS. For development purposes, creating an STS in the existing solution is a good starting point that lets you quickly begin mocking the claims you want issued, and the rules for issuing them. You can run FedUtil again later to point it to an existing production STS, such as ADFS 2.0.
- You're then taken to a summary screen. Clicking Finish adds the STS project to the solution and updates the WidgetsNowService project. The primary change is that the WidgetsNowService’s web.config is updated to use the development STS.
Once you have your baseline STS in place, you need to modify its implementation slightly so that it issues your custom claim, CanExpediteOrders. The implementation is defined in the CustomSecurityTokenService.cs created by FedUtil. The logic for issuance of claims is defined within the GetOutputClaimsIdentity method, Figure 5 shows the complete method definition. The thrust of the work is adding claims to the outputIdentity.Claims collection, as shown in bold.
Here the Name claim is always added with the value that ultimately is the windows account name that came from the caller (the principal parameter passed in to GetOuputClaimsIdentity). The principal is also checked for membership in the SeniorManagers role, and if so a canexpediteorders claim with a value of true is added to the Claims; otherwise it's added with a value of false. The outputIdentity returned is effectively what is contained in the token returned to the WPF client.
An important point to note is that many details need to be managed by a production grade STS, and the STS template created in Visual Studio is a long way from that. It’s enough to get you started, but you should plan on switching to a fully implemented STS like ADFS 2.0.
Secure Service Operations
With the STS in place and your RP configured to trust claims issued from that STS, you're freed from any authentication responsibilities at the Workflow Service. However, one of the goals was to authorize calls to PlaceOrder and GetOrderStatus differently. You could put this authorization logic in the workflow definition for each operation, but a convenient extensibility point exists with WIF that lets you perform such authorization before hitting the workflow, and without burdening the workflow definition with authorization logic. This is the purpose of a custom ClaimsAuthorizationManager.
In our example, I first added a reference in the WidgetsNowService project to the Microsoft.IdentityModel assembly. Then I added a single class file that implements the WidgetsNowClaimsAuthorizationManager. Figure 6 shows the complete class listing. As you can see, a custom CAM is quite straightforward. It amounts to deriving from Microsoft.IdentityModel.Claims.ClaimsAuthorizationManager and overriding the CheckAccess method. The implementation of CheckAccess needs to return true if authorized; false if not. The context parameter passed in lets you examine the requested operation (the action), the principal, and the resource (the xamlx) being accessed in making your authorization decision. In this implementation, I first check whether the operation (action) is PlaceOrder. If the operation is PlaceOrder, I return true if the principal’s identity’s name is of the form "contoso\<username>" and false if it’s not. If the operation is not PlaceOrder (for example, it’s GetOrderStatus), I always allow it by returning true.
Once you have the CAM implementation, using it amounts to adding one line to the web.config of the workflow service previously updated by FedUtil. Figure 7 shows the claimsAuthorizationManager element (the one line in bold) that needs to be added within the microsoft.IdentityModel element.
Authorize within the Workflow
To specify that only Contoso domain users can expedite orders requires that you look at incoming claims (specifically for the CanExpediteOrders claim), as well as the value of the expedite parameter in the call to PlaceOrder. The workflow definition is a reasonable place to put this authorization logic, because it has ready access to both. Figure 8 shows the Authorize Order sequence added to implement this authorization logic for the PlaceOrder operation.
In brief, when using WIF with a workflow service, you can always get at the Principal (which contains the identities and related claims) by means of System.Threading.Thread.CurrentPrincipal. The first assign in the sequence, the one labeled Get Claims, gets at the claims with the following expression and stores them in the claims workflow variable (defined at a higher scope):
You always have to cast the Principal to an IClaimsPrincipal or the Identity to an IClaimsIdentity to get at the claims collection; that's the purpose of the DirectCast. You can then use LINQ to Objects to query the claims collection for the canexpediteorders claim. The Get Expedite Claim assign activity performs this task by evaluating the following expression:
(From claim In claims Where claim.ClaimType = "http://contoso.com/claims/canexpediteorders").FirstOrDefault()
Finally, the condition for the Reject Unauthorized Expedites If activity looks at the value of the expedite parameter. If it's true, the canexpediteorders claim must be present and have a value of true; otherwise you must throw an exception. The expression used is
(Not expediteClaim Is Nothing _
And Not expediteClaim.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase)) )
Update the Client Application
Now that the workflow service is performing authorization and trusting the STS for authentication, the only piece untouched is the client application. The good news is no sweeping changes are required to go from calling the service without an STS to calling an STS and then calling the service.
WIF handles all of that under the covers. All you need to do in the WidgetsNowApp project is expand the Service References folder, right click the Contoso entry, and select Update Service Reference. That’s it. All the configuration needed for the STS is brought down as part of the workflow service’s metadata.
The End Result
The end result of this effort is that the WidgetsNowService completely externalizes authentication by relying on an STS for a standard set of claims that it can use to make authorization decisions. In addition, the use of a ClaimsAuthorizationManager removes a lot of custom authorization logic that you might otherwise need to sprinkle within the workflow service definition.
That said, sometimes you really have all the information needed for an authorization decision within a service operation, and this project demonstrates how easy it is to get at the presented claims via Thread.CurrentPrincipal.