In this post, I describe how to implement Authentication and Authorization for Silverlight using AD LDS (previously known called ADAM), and the Authorization Manager (also known as AzMan).
Here is the list of steps involved in this implementation:
- Setup AS LDS
- Setup AzMan on AD LDS
- Implement a custom RoleProvider for AzMan
- Implement a custom Principal
- Implement a custom PrincipalPermission and PrincipalPermissionAttribute
- Implement a custom AuthorizationPolicy
- Implement a custom WCF Authorization Service
- Setup WCF Authentication, Role and Authorization Services
- Setup SSL
- Implement a custom Silverlight Principal and Identity
- Implement a workaround for Silverlight-enabled WCF Service Exception Handling
Here is why I chose this implementation:
- I am using Silverlight 2 with VS2008 SP1 on WS2008,
- I am using Forms Authentication without Windows Credentials (for Internet users),
- I want to have a RBAC allowing me to define Operations (no Roles in code),
- I want to have the flexibility to use ADAM, and the eventual ability to move to Federated Services,
- Silverlight only works with basicHttpBinding (Geneva and Federated Serviced use wsFederationHttpBinding),
- AzMan only works with Windows Authentication out-of-the-box.
Setup AD LDS
The first step consists of installing Active Directory Lightweight Directory Services (AD LDS), previously called Active Directory Application Mode (ADAM). AD LDS is now directly included in the Roles of Windows Server 2008.
Since, this installation is already well described in the following link, I will only add a couple instructions to this for the purpose of this post.
The following LDF files should be included in the installation:
- MS-AzMan.LDF
- MS-InetOrgPerson.LDF
- MS-User.LDF
Once the installation completed, add the "NETWORK SERVICE" account to the "Administrators" roles:
- Open ADSI Edit
- Connect to the newly created partition ("Select a well known Naming Context")
- Expand Tree View up to "CN=Roles"
- Open "CN=Administrators" Properties
- Edit "member" property
- Add the "NETWORK SERVICE" Windows Account
Furthermore, if you want to setup SSL, you can simply create a "Self-Signed Certificate" for development purpose in IIS7 (Server Certificates).
Links:
Setup AzMan
This second step consists of creating an Authorization Store in AD LDS for the Authorization Manager.
- Run "azman.msc"
- Right click "Authorization Manager"
- Select "New Authorization Store":
- Select ADAM
- Schema version 2
- Store name: msldap://localhost:389/CN=[user store name], [your AD LDS partition]
(*) Where [user store name] such as: MyUserStore
- Right click the new store (MyUserStore)
- Select "New Application" and create a new application (MyApplication)
- Open the new application Properties
- Open the Security tab and add the "NETWORK SERVICE" account to the Administrator role
From now, you can define your Operations, Tasks, and Roles.
In the following link, MSDN clearly define how to use the Authorization Manager with ASP.NET. However this implementation only uses ADAM as a repository. The users are authenticated with Windows Accounts!
Links:
Implement a custom RoleProvider for AzMan
At this stage we should have AD LDS and AzMan setup, however as indicated in the previous post, AzMan only works with Windows Authentication by default. So, we need to implement a custom RoleProvider to use it with Internet users authenticated by AD LDS.
I have added 2 new methods to MyRoleProvider:
- public string[] GetOperationsForUser(string userName)
- public bool CanUserAccessOperation(string userName, string operationName)
Using the following links, and the Reflector on the AuthorizationStoreRoleProvider class, you can easily create your own provider.
My 2 cents on this:
- Check-out the CheckParameter() and CheckArrayParameter() methods in the AuthorizationStoreRoleProvider
- Don't forget to dispose all COM objects that have been opened.
For this second point, I implemented my own internal AzManStore type as follow:
internal class AzManStore : IDisposable
{
public AzAuthorizationStore Store { get; private set; }
public IAzApplication Application { get; private set; }
public AzManStore(string applicationName, string connectionString)
{
if (string.IsNullOrEmpty(applicationName)) throw new AzManProviderException("ApplicationNameNotSpecified");
try
{
this.Store = new AzAuthorizationStore();
this.Store.Initialize(0, connectionString, null);
this.Application = this.Store.OpenApplication(applicationName, null);
}
catch (COMException ex)
{
throw new AzManProviderException("InitializeFailed", ex);
}
catch (Exception ex)
{
throw new AzManProviderException("InvalidConnectionString", ex);
}
}
public void Dispose()
{
if (this.Application == null) return;
Marshal.FinalReleaseComObject(this.Application);
Marshal.FinalReleaseComObject(this.Store);
this.Application = null;
this.Store = null;
}
}
Then, I can open my AzMan application as follow:
using (var store = new AzManStore(this.ApplicationName, this.ConnectionString))
{
var roles = store.Application.Roles;
// do something
}
Once this new RoleProvider created, you can sign you DLL, put it in the GAC, add the new Provider to the trusted list, and setup your Web Application (Silverlight Web Application Host) to enable Authentication and Authorization.
To add the new provider to the trusted list, open and edit the Administration.config file located in:
C:\Windows\System32\inetsrv\config
Here are the lines to add to your web.config, however you should be able to setup everything from IIS7:
< name="MyMembershipProvider">
type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
connectionStringName="MyAdamConnectionString"
connectionProtection="Secure"
applicationName="MyApplication"
enableSearchMethods="true" />
< name="MyRoleProvider">
type="MyLibrary.MyRoleProvider, MyLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1111111111"
connectionStringName="MyAzManConnectionString"
applicationName="MyApplication" />
At this point, you should be able to setup your users and roles directly from IIS7, or from the ASP.NET Configuration site!
Links:
Implement a custom Principal
In this step, I propose to create a custom Principal that will be used on the Server. This Principal will allow to directly access the Operations accessible by the Identity.
public class MyPrincipal : IPrincipal
{
private IIdentity _identity = null;
private string[] _operations = null;
public IIdentity Identity { get { return this._identity; } }
public MyPrincipal(IIdentity identity, string[] operations)
{
this._identity = identity;
this._operations = operations;
}
public bool IsInRole(string role)
{
throw new NotImplementedException();
}
public bool HasRequiredOperations(string[] requiredOperations)
{
if (requiredOperations == null) return true;
if (this._operations == null) return false;
var hasOperations = true;
foreach (var operation in requiredOperations)
{
if (this._operations.Contains(operation) == false)
{
hasOperations = false;
break;
}
}
return hasOperations;
}
}
Links:
Implement a custom PrincipalPermission and PrincipalPermissionAttribute
Now that our Principal is created, we can create a custom PrincipalPermission and its PrincipalPermissionAttribute. The following link clearly explains how to implement these classes for a claim-based scenario. So, instead of a CheckClaims() method, you can write a CheckOperations() method and invoke the principal.HasRequiredOperation() method.
As described in the link, this allows the use of declarative and imperative security checks as follow:
[MyPrincipalPermission(SecurityAction.Demand, Operation = "MyOperation")]
public void MyOperation()
{
// do something
}
Or
public void MyOperation()
{
var permission = new MyPrincipalPermission("MyOperation");
Permission.Demand();
// do something
}
I particularly like this implementation, because it allows decoupling the code from the RBAC used, and the type of authentication. In this scenario I am using AzMan with a "standard Role-Based" approach, but it should not be too hard to move to the "Claim-Based" model if needed.
Links:
Implement a custom AuthorizationPolicy
This step consists of creating a custom AuthorizationPolicy that will tie the things together! The AuthorizationPolicy will allow to initialize the CurrentPrincipal of the current Thread whenever a WCF Service is invoked.
public class MyAuthorizationPolicy : IAuthorizationPolicy
{
private Guid _id = Guid.NewGuid();
public string Id { get { return this._id.ToString(); } }
public ClaimSet Issuer { get { return ClaimSet.System; } }
public bool Evaluate(EvaluationContext evaluationContext, ref object state)
{
IIdentity identity = this.GetClientIdentity(evaluationContext);
var provider = (MyRoleProvider)Roles.Provider;
var operations = provider.GetOperationsForUser(identity.Name);
evaluationContext.Properties["Principal"] = new MyPrincipal(identity, operations);
return true;
}
private IIdentity GetClientIdentity(EvaluationContext evaluationContext)
{
var identity = HttpContext.Current.User.Identity;
if (identity == null)
{
throw new SecurityException("NoIdentityFound");
}
return identity;
}
}
You can notice in this code the reuse of MyRoleProvider created in step# 3, and MyPrincipal created in step# 4.
Then, the following lines should be added to the behavior of your WCF Service (or Silverlight-enabled WCF Service):
Links:
Implement a custom WCF Authorization Service
The last step left on the server side is to implement a new WCF Service to expose the following methods:
- public string[] GetOperationsForCurrentUser()
- public bool CanCurrentUserAccessOperation(string operation)
These 2 methods will allow the Silverlight application to retrieve the Operations accessible by a user, and then initialize the Silverlight Identity and Principal context.
Here again, you can use the Reflector on the System.Web.ApplicationServices.RoleService class, and rename the Role methods by the Operation methods...
Setup WCF Authentication, Role and Authorization Services
Now that all Authentication, Role and Authorization Services are ready to be used, you can set them up for your Silverlight application.
Links:
Note: It is highly recommended to secure these services with SSL.
Setup SSL
The following link describes how to enable SSL on your application. This is highly recommended to secure your application.
Links:
Implement a custom Silverlight Principal and Identity
We are almost there! At this stage, everything is ready on the Server, however on the client side there is no Identity and Principal implemented in Silverlight. Fortunately, the IIdentity and IPrincipal interfaces are exposed. So, you can create your custom classes.
For this part, I have decided to go for a solution with a SilverlightIdentity and a SilverlightPrincipal as proposed in this link. I have added a CanAccessOperation(string operation) method to allow the Silverlight application to check if a user has access to an Operation.
The SilverlightIdentity and SilverlightPrincipal classes are initialized using the Authentication, Role, and Authorization WCF Services previously added to the Web Application (host) for this purpose. So, they need to be referenced in the Silverlight project.
Links:
Implement a workaround for Silverlight-enabled WCF Service Exception Handling
WCF comes with the FaultException class to provide a clean solution to exchange error details between Client and Server. However, up to now Silverlight does not work properly with this solution. An error 404 Not Found is returned!
You will find several solutions to this (refer to links), and here is mine. Add the following classes to the Web Application exposing your Silverlight-enabled WCF Services:
[DataContract]
[KnownType(typeof(MyResult))]
public class ResponseData
{
[DataMember]public Error Error { get; private set; }
[DataMember]public object Data { get; private set; }
public ResponseData(Exception ex)
{
this.Error = new Error()
{
Type = ex.GetType().Name,
Message = ex.Message
};
}
public ResponseData(object data)
{
this.Data = data;
}
}
[DataContract]
public class Error
{
[DataMember]public string Type { get; set; }
[DataMember]public string Message { get; set; }
}
Add the type of your result to the ResponseData using the KnownType attribute. This allows the Service Utility to generate your classes with the proxy.
Then, use it to return your result to Silverlight:
[OperationContract]
public ResponseData MyOperation()
{
try
{
var result = new MyResult("Hello world!");
return new ResponseData(result);
}
catch (Exception ex)
{
return new ResponseData(ex);
}
}
Links:
Wrap-Up
Silverlight can be setup to use Forms Authentication and WCF Authentication & Role Services. In this post, I proposed an implementation combining AD LDS and the Authorization Manager. This allows Web Applications dedicated to Internet Users to benefit from the standard Administration tools (ADAM and AzMan), and also decouple the Application from these Administration tools.