Authenticating your windows domain users in the cloud
Moving to the cloud can represent a big challenge for many organizations when it comes to reusing existing infrastructure. For applications that drive existing business processes in the organization, reusing IT assets like active directory represent good part of that challenge. For example, a new web mobile application that sales representatives can use for interacting with an existing CRM system in the organization.
In the case of Windows Azure, the Access Control Service (ACS) already provides some integration with ADFS through WS-Federation. That means any organization can create a new trust relationship between the STS running in the ACS and the STS running in ADFS. As the following image illustrates, the ADFS running in the organization should be somehow exposed out of network boundaries to talk to the ACS. This is usually accomplish through an ADFS proxy running in a DMZ.
This is the official story for authenticating existing domain users with the ACS. Getting an ADFS up and running in the organization, which talks to a proxy and also trust the ACS could represent a painful experience. It basically requires advance knowledge of ADSF and exhaustive testing to get everything right.
However, if you want to get an infrastructure ready for authenticating your domain users in the cloud in a matter of minutes, you will probably want to take a look at the sample I wrote for talking to an existing Active Directory using a regular WCF service through the Service Bus Relay Binding.
You can use the WCF ability for self hosting the authentication service within a any program running in the domain (a Windows service typically). The service will not require opening any port as it is opening an outbound connection to the cloud through the Relay Service. In addition, the service will be protected from being invoked by any unauthorized party with the ACS, which will act as a firewall between any client and the service. In that way, we can get a very safe solution up and running almost immediately.
To make the solution even more convenient, I implemented an STS in the cloud that internally invokes the service running on premises for authenticating the users. Any existing web application in the cloud can just establish a trust relationship with this STS, and authenticate the users via WS-Federation passive profile with regular http calls, which makes this very attractive for web mobile for example.
This is how the WCF service running on premises looks like,
[ServiceBehavior(Namespace = "http://agilesight.com/active_directory/agent")]
public class ProxyService : IAuthenticationService
{
IUserFinder userFinder;
IUserAuthenticator userAuthenticator;
public ProxyService()
: this(new UserFinder(), new UserAuthenticator())
{
}
public ProxyService(IUserFinder userFinder, IUserAuthenticator userAuthenticator)
{
this.userFinder = userFinder;
this.userAuthenticator = userAuthenticator;
}
public AuthenticationResponse Authenticate(AuthenticationRequest request)
{
if (userAuthenticator.Authenticate(request.Username, request.Password))
{
return new AuthenticationResponse
{
Result = true,
Attributes = this.userFinder.GetAttributes(request.Username)
};
}
return new AuthenticationResponse { Result = false };
}
}
Two external dependencies are used by this service for authenticating users (IUserAuthenticator) and for retrieving user attributes from the user’s directory (IUserFinder). The UserAuthenticator implementation is just a wrapper around the LogonUser Win Api.
The UserFinder implementation relies on Directory Services in .NET for searching the user attributes in an existing directory service like Active Directory or the local user store.
public UserAttribute[] GetAttributes(string username)
{
var attributes = new List<UserAttribute>();
var identity = UserPrincipal.FindByIdentity(new PrincipalContext(this.contextType, this.server, this.container), IdentityType.SamAccountName, username);
if (identity != null)
{
var groups = identity.GetGroups();
foreach(var group in groups)
{
attributes.Add(new UserAttribute { Name = "Group", Value = group.Name });
}
if(!string.IsNullOrEmpty(identity.DisplayName))
attributes.Add(new UserAttribute { Name = "DisplayName", Value = identity.DisplayName });
if(!string.IsNullOrEmpty(identity.EmailAddress))
attributes.Add(new UserAttribute { Name = "EmailAddress", Value = identity.EmailAddress });
}
return attributes.ToArray();
}
All the source code for this sample is available to download (or change) in this GitHub repository,