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
Next

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
Next

End If

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

'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:
wcfSecurityTest.GetData(2)

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!

11 comments:

  1. Thanks for this post, regarding line 26:
    If operationContext.ServiceSecurityContext.IsAnonymous Then
    Return True
    End If

    Won't this return true if an anonymous user tries to call a web service method as well?

    ReplyDelete
  2. No the service dictates via the WCF configuration that WindowsCredentials must be sent and Anonymous users are disabled. That line is only applicable for mex support when the service is started. Therefore, an anonymous user can not call the service. If you want to see how it works, start up the service, and have a web test harness call it. That line only returns true upon starting the service. Thank you.

    ReplyDelete
  3. Great article but it is worth noting that the default ASP authentication behaviour works if the ASP Net compability is enabled for the WCF service.
    See http://rickgaribay.net/archive/2007/04/04/recipe-wcf-basichttpbinding-with-windows-authentication.aspx
    http://msdn.microsoft.com/en-us/library/bb332338.aspx

    ReplyDelete
  4. Enrique-
    Actually as I alluded to, setting AspNetCompatibility = True is not enough. 1st off there are several missed points in your comments.

    1. Even if the 1st link you posted did actually hold merit, you would notice it was specific to IIS hosted 'only' WCF services using basicHttpBinding. My solution will work for any hosting environment and any binding type that works with Windows Authentication; in fact my most recent use was with a WCF service hosted in a Windows Service using NetTcpBinding.

    2. In addition, this solution is more flexible in that the client can change the ClientCredentials object on the fly, and not have to mess with any machine.config settings or application pools with a set identity as alluded to on that blog post.

    3. However, it is all a moot point anyways because that blog details a procedure that I have never seen work, and neither did some of the other readers of that blog. Here is the response from the last post on that blog link you provided (http://rickgaribay.net/archive/2007/04/04/recipe-wcf-basichttpbinding-with-windows-authentication.aspx):

    From the above blog link; challenged as a better alternative to the method I detailed:

    "Are you sure this works? I found this in some MSDN documentation.

    Hosting WCF Side-by-Side with ASP.NET
    http://msdn.microsoft.com/en-us/library/aa702682.aspx

    Configuration-based URL Authorization: Similarly, the WCF security model does not adhere to any URL-based authorization rules specified in System.Web’s configuration element. These settings are ignored for WCF requests if a service resides in a URL space secured by ASP.NET’s URL authorization rules."

    As you can see, attempting to set any kind of ASP.NET 'System.Web' authentication settings in the web.config or any .config for that matter are ignored by WCF. Many HTTP-specific features of the ASP.NET application platform do not apply to WCF Services hosted inside of an AppDomain that contains ASP.NET content.

    In order to mimic this style of all-or-nothing authorization to the service, you need to follow the procedure I detailed or one similar. The method I detailed provides a truly 'centralized' method of handling authorization when accessing a WCF service regardless of the hosting environment or binding type.

    Thanks for reading

    ReplyDelete
  5. Allen.. This post is wonderful. I tried doing this and it works great. there are many other links out there, the way you have explained witht the sample is extremely very useful, and very flexible also.

    Kaushik Thandra

    ReplyDelete
  6. @Kaushik, yes I agree. This example has solved the problem of needing to use PrincipalPermissionAttribute everywhere. Thanks for a great example. I have worked through your code and am working on test harnesses now. thank you for the time you took to put this together.

    ReplyDelete
  7. excellent ..superb

    Thanks,
    Venkateswarlu Eraga

    ReplyDelete
  8. Wonderful article. I found it right after implementing my own derivation of the ServiceAuthorizationManager. In my implementation I am breaking out of the `For Each .. In Authorized..` loops immediately once the requirement has been met.

    I am also planning to cache the responses to avoid rerunning the code hundreds of times for the same user.

    In any case thank you!

    Tim

    ReplyDelete
  9. Thank you very, very much Allen. I always got the feeling Authentication using AspNetCompatibility did not work as it should. And my last project securing all WCF services for a customer proved I was right. Luckily I found your posting and I am now using it (as a default) for all my new WCF services.
    Besides from a personal feeling I guess it would be better to have AspNetCompatibility NOT enabled for modern 4.0 WCF services.

    Yours sincerely,

    Evert Wiesenekker

    ReplyDelete
  10. Brilliant! Managed to implement this model and it worked a treat. Solved a huge problem for me with so little code. Thank you sir!

    Cheers

    Rod.

    ReplyDelete
  11. operationContext.ServiceSecurityContext.IsAnonymous is always return true, I am running in my local machine where iis is not installed and I am using proxy class created by svcutil.exe
    Any suggestions?

    ReplyDelete