Sunday, January 10, 2010

How To: Create an ASP.NET style Windows Authentication Policy for WCF Services

For those of you that program on a Windows Domain probably in a corporate environment, you were probably well familiar with using Windows Authentication in .asmx web services to restrict or permit users access. Do you remember how easy it was in a .asmx web service to allow only a specific user or group? Take a look below from the only setting needed in the web.config file associated with the web service:

That was it! Now let's not start off on the wrong foot here or give the wrong impression. I am a huge advocate of WCF over .asmx services and definently think it is the way to go. With WCF having such a broad use capability and granular level of control and functionality, came the loss of some of the bundled up functionality that was specific to IIS hosted .asmx web services. This is not a bad thing at all.

However, those of us that work with WCF came to quickly realize that with just a small configuration you could easily configure WCF to only allow Windows Authenticated users to access the service, but that was it. There was no clean direct way to restrict the Windows Users or Groups in a 'blanket' fashion as was the case with .asmx web services and the configuration shown above. Not even setting the WCF service to ASPCompatibilityMode could directly accomplish the configuration above.

So there are (2) main ways to get around this issue. The 1st method to get around this would be to define PrincipalPermission attributes on every method to define its security requirements. This means hardcoding the security requirements, and is not as flexible as info that comes from a .config file. In some cases this method by method style of security may be desired, but often our services are an all or nothing style of access.

The 2nd solution, and the focus of this article, is to intercept the authorization calls to your WCF service and examine the identity of the user making the call to determine if they are authorized. We as developers are provided the ability to extend the 'ServiceAuthorizationManager' class and override the 'CheckAccessCore()' method to define our own custom authorization policy for the service. It is within this method that you can scrutinize the individual roles, groups, or users authorized by extracting the WindowsIdentity of the context, and then either permitting or rejecting access based on your requirements. Within here you could pull the authorized roles, users, or groups from the WCF .config file.

To get right to the code, below I have provided the implementation of the overriden 'CheckAccessCore()' method. The robust comments within should detail well what each step is doing:

Imports System.Configuration
Imports System.Security.Principal
Imports System.IdentityModel.Tokens

''' The Identity Model infrastructure in Windows Communication Foundation (WCF) supports an extensible claims-based authorization
''' model. Claims are extracted from tokens and optionally processed by custom authorization policies and then placed into an
''' AuthorizationContext. An authorization manager examines the claims in the AuthorizationContext to make authorization decisions.
''' By default, authorization decisions are made by the ServiceAuthorizationManager class; however these decisions can be
''' overridden by creating a custom authorization manager. To create a custom authorization manager, create a class that derives
''' from ServiceAuthorizationManager (this class) and implement CheckAccessCore method (done in this class). Authorization
''' decisions are made in the CheckAccessCore method, which returns 'true' when access is granted and 'false' when access is denied.
''' In our case, we are examining the Windows Identity of the current user's context and checking it against some predefined
''' permitted users and roles from the AppSettings section of the associated services' .config file.

''' Because of performance issues, if possible you should design your application
''' so that the authorization decision does NOT require access to the message body.
''' Registration of the custom authorization manager for a service can be done in code or configuration.

Public Class CustomAuthorizationManager
Inherits ServiceAuthorizationManager

Protected Overloads Overrides Function CheckAccessCore(ByVal operationContext As OperationContext) As Boolean

'For mex support (starting WCF service, etc.)
'NOTE: Other than for service startup this will NOT be true because the WCF
'configuration dictates that WindowsCredentials must be sent and Anonymous users
'are NOT allowed.
If operationContext.ServiceSecurityContext.IsAnonymous Then
Return True
End If

'If Windows Authentication has been defined in the binding and either of the (2)
'predefined AppSettings are populated, proceed to authorize current user/group against allowed users or groups.
If (ConfigurationManager.AppSettings("AuthorizedGroups") IsNot Nothing) OrElse _
(ConfigurationManager.AppSettings("AuthorizedUsers") IsNot Nothing) Then

Dim IdentityIsAuthorized As Boolean = False

'Extract the identity token of the current context user making the call to this service
Dim Identity As WindowsIdentity = operationContext.ServiceSecurityContext.WindowsIdentity
'Create a WindowsPrincipal object from the identity to view the user's name and group information
Dim Principal As New WindowsPrincipal(Identity)

'Prior to proceeding, throw an exception if the user has not been authenticated at all
'if the 'AppSettings' section are populated indicating the Windows Authentication should be checked.
If Identity.IsAuthenticated = False Then
Throw New SecurityTokenValidationException("Windows authenticated user is required to call this service.")
End If

'Define (2) string arrays that will hold the values of users and groups with permitted access from the .config file:
Dim AuthorizedGroups As String() = Nothing
Dim AuthorizedUsers As String() = Nothing

'Procced to check the AuthorizedGroups if defined, against the current user's Groups
If ConfigurationManager.AppSettings("AuthorizedGroups") IsNot Nothing Then
'The values in the .config are separated by a comma, so split them out to iterate through below:
AuthorizedGroups = ConfigurationManager.AppSettings("AuthorizedGroups").ToString().Split(",")

'Iterate through all of the permitted groups from the .config file:
For Each Group As String In AuthorizedGroups
'If the user exists in one of the permitted Groups from the AppSettings "AuthorizedGroups" values,
'then set value indicating that the user Is Authorized.
If Principal.IsInRole(Group) Then IdentityIsAuthorized = True

End If

'Procced to check the AuthorizedUsers if defined, against the current user's name
If ConfigurationManager.AppSettings("AuthorizedUsers") IsNot Nothing Then
'The values in the .config are separated by a comma, so split them out to iterate through below:
AuthorizedUsers = ConfigurationManager.AppSettings("AuthorizedUsers").ToString().Split(",")

'Iterate through all of the permitted users from the .config file:
For Each User As String In AuthorizedUsers
'If the user exists as one of the permitted users from the AppSettings "AuthorizedUsers" values,
'then set value indicating that the user Is Authorized.
If Identity.Name.ToLower() = User.ToLower() Then IdentityIsAuthorized = True

End If

'Return the boolena indicating if the user is authorized to proceed:
Return IdentityIsAuthorized

'Call the base class implementation of the CheckAccessCore method to make authorization decisions
'since no defined users or groups were defined in this service's .config file.
Return MyBase.CheckAccessCore(operationContext)

End If

End Function

End Class

Notice from the code above, that we have defined (2) key values which of at least (1) must exist for the code above to work. The appSettings sections are named: AuthorizedGroups and AuthorizedUsers. Below is a sample configuration using both of these values:

In addition to the overridden method and appSettings values, we must configure the WCF behavior for our service to indicate the Service Authorization Manager name as displayed below:

You also need to specify within the binding that the clientCrendentals are of type 'Windows'. To show the node fully, I have included the full 'Security' node in which I was using Transport security, but this can be configured for your needs and is not specific to this procedure:

That will be everything you need to 'intercept' the authorization calls to your WCF service and scrutinize the users permitted. Want to see it work? The easiest way to test this is to build a WCF Service and add the code from this article into or above the Service1 default class that is generated from VS.NET. Once your WCF will compile, start the service locally (no need to install) and place a breakpoint on the 1st line in the 'CheckAccessCore()' method. You will be able to tell if the WCF service starts because service client dialog window will display with the methods in your test service. If you leave the default generated code in the service, it exposes (2) methods: GetData() and GetDateUsingDataContract().

To test this all out I created a test ASP.NET web application and consumed the WCF service. Make sure the service is running when attempting to consume it. You can find the link to consume your service in the WCF Service dialog window. For my test project, mine happend to be net.tcp://localhost:8731/Design_Time_Addresses/WcfServiceWindowsSecurityTest/Service1/mex

Once consumed, I added a little bit of code to test as shown below:

Dim wcfSecurityTest As New WindowsSecurityService.Service1Client
'Obviously NEVER hardcode credentials like the example ONLY below
wcfSecurityTest.ClientCredentials.Windows.ClientCredential = New System.Net.NetworkCredential("jsmith", "testpassword", "MyCompany")
'Upon making the call below, the WCF will call the overriden CheckAccessCore() method to allow or reject authorization:

When calling the 'GetData()' method above and having a break point in the running WCF service, you will notice the WCF service hit the breakpoint in our 'CheckAccessCore()' method (pretty cool). From here you can debug the method and see how it works.

Hopefully this gets you back to enabling whole application Windows Authentication on your WCF services!


Post a Comment