Reputation: 908
We are building a system that will have a number of WCF services hosted in IIS sitting on an enterprise domain. A presentation tier server running in the DMZ will call these services. The calls to the WCF services need to be secured (i.e. require authentication). This system is a COTS system and will be deployed to a number of client sites.
WCF supports authenticating a caller using Windows authentication and x.509 certificates out-of-the-box. Windows authentication will not work for securing the WCF services in this scenario due to the fact that the DMZ presentation tier server will be in a different domain.
x.509 certificate security is an option and has been mentioned on other SO posts like the one below:
Accessing WCF Service using TCP from the DMZ (not on network or domain)
I have two concerns about x.509 certs:
Performance. I have yet to do performance analysis myself, but have heard from others that the overhead for validating x.509 certificates may make the solution a non-starter. My next task is to do performance analysis on this point.
Ease-of-deployment. I have found in the past that anytime x.509 certificates come into the picture for anything other than SSL that they cause problems for customer IT staff (procuring, generating, managing). This, in turn, causing a support issue for our product.
I'm considering using username/password security for securing the WCF calls for the reasons mentioned above. The solution would use a custom username/password validator.
https://msdn.microsoft.com/en-us/library/aa702565(v=vs.110).aspx
Credentials would be stored in a custom section of the web.config file on the presentation tier server in the DMZ. The same credentials would be stored in the web.config file on the application tier server. The sections containing the credentials would be encrypted on both servers.
Any other suggestions? Any thoughts on the custom username/password validator approach?
Upvotes: 2
Views: 627
Reputation: 908
We did a lot of testing of various options. The solution that we ended up implementing was one that was configurable. It allows us to deploy username/password security as an option or to fall back to standard security approaches like x.509 certs for those clients that are comfortable with certs and can manage them.
There are four primary components to the solution:
The abridged ServiceClientBase class is shown below. The if/else blocks can be modified to include support for whatever bindings you desire to support. The main thing to point out about this class is that if security is used and the client credential type is "username", then we will load the username/password from the .config file. Otherwise, we fallback to using standard WCF security configuration.
public class ServiceClientBase<TChannel> : ClientBase<TChannel>, IDisposable where TChannel : class
{
public const string AppTierServiceCredentialKey = "credentialKey";
public ServiceClientBase()
{
bool useUsernameCredentials = false;
Binding binding = this.Endpoint.Binding;
if (binding is WSHttpBinding)
{
WSHttpBinding wsHttpBinding = (WSHttpBinding)binding;
if (wsHttpBinding.Security != null && wsHttpBinding.Security.Mode == SecurityMode.TransportWithMessageCredential)
{
if (wsHttpBinding.Security.Message != null && wsHttpBinding.Security.Message.ClientCredentialType == MessageCredentialType.UserName)
{
useUsernameCredentials = true;
}
}
}
else if (binding is BasicHttpBinding)
{
BasicHttpBinding basicHttpBinding = (BasicHttpBinding)binding;
if (basicHttpBinding.Security != null && basicHttpBinding.Security.Mode == BasicHttpSecurityMode.TransportWithMessageCredential)
{
if (basicHttpBinding.Security.Message != null && basicHttpBinding.Security.Message.ClientCredentialType == BasicHttpMessageCredentialType.UserName)
{
useUsernameCredentials = true;
}
}
}
...
if (useUsernameCredentials)
{
ServiceCredentialsSection section = (ServiceCredentialsSection)ConfigurationManager.GetSection(ServiceCredentialsSection.SectionName);
CredentialsElement credentials = section.Credentials[AppTierServiceCredentialKey];
this.ClientCredentials.UserName.UserName = credentials.UserName;
this.ClientCredentials.UserName.Password = credentials.Password;
}
}
// http://blogs.msdn.com/b/jjameson/archive/2010/03/18/avoiding-problems-with-the-using-statement-and-wcf-service-proxies.aspx
void IDisposable.Dispose()
{
if (this.State == CommunicationState.Faulted)
{
this.Abort();
}
else if (this.State != CommunicationState.Closed)
{
this.Close();
}
}
}
The custom configuration section class for credentials is shown below.
public class ServiceCredentialsSection : ConfigurationSection
{
public const string SectionName = "my.serviceCredentials";
public const string CredentialsTag = "credentials";
[ConfigurationProperty(CredentialsTag, IsDefaultCollection = false)]
[ConfigurationCollection(typeof(CredentialsCollection), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
public CredentialsCollection Credentials
{
get
{
return (CredentialsCollection)this[CredentialsTag];
}
}
}
In addition to the ServiceCredentialsSection class, there is also a CredentialsCollection class (extending ConfigurationElementCollection) and a CredentialsElement class (extending ConfigurationElement). I won't include the CredentialsCollection class here because it's a long class and mainly full of stock code. You can find references implementations for ConfigurationElementCollection on the Internet, like at https://msdn.microsoft.com/en-us/library/system.configuration.configurationelementcollection(v=vs.110).aspx. The CredentialsElement class is shown below.
public class CredentialsElement : ConfigurationElement
{
[ConfigurationProperty("serviceName", IsKey = true, DefaultValue = "", IsRequired = true)]
public string ServiceName
{
get { return base["serviceName"] as string; }
set { base["serviceName"] = value; }
}
[ConfigurationProperty("username", DefaultValue = "", IsRequired = true)]
public string UserName
{
get { return base["username"] as string; }
set { base["username"] = value; }
}
[ConfigurationProperty("password", DefaultValue = "", IsRequired = true)]
public string Password
{
get { return base["password"] as string; }
set { base["password"] = value; }
}
}
The classes mentioned above supports a .config section like the one shown below. This section can be encrypted to secure the credentials. See Encrypting custom sections of a web.config for tips on encrypting a section of a .config file.
<my.serviceCredentials>
<credentials>
<add serviceName="credentialKey" username="myusername" password="mypassword" />
</credentials>
</my.serviceCredentials>
The third piece of the puzzle is the custom UserNamePasswordValidator. The code for this class is shown below.
public class PrivateServiceUserNamePasswordValidator : UserNamePasswordValidator
{
private IPrivateServiceAccountCache _accountsCache;
public IPrivateServiceAccountCache AccountsCache
{
get
{
if (_accountsCache == null)
{
_accountsCache = ServiceAccountsCache.Instance;
}
return _accountsCache;
}
}
public override void Validate(string username, string password)
{
if (!(AccountsCache.Validate(username, password)))
{
throw new FaultException("Unknown Username or Incorrect Password");
}
}
}
For performance reasons, we cache the sets of credentials against which the username/password pairs contained in service calls will be validated. The cache class is shown below.
public class ServiceAccountsCache : IPrivateServiceAccountCache
{
private static ServiceAccountsCache _instance = new ServiceAccountsCache();
private Dictionary<string, ServiceAccount> _accounts = new Dictionary<string, ServiceAccount>();
private ServiceAccountsCache() { }
public static ServiceAccountsCache Instance
{
get
{
return _instance;
}
}
public void Add(ServiceAccount account)
{
lock (_instance)
{
if (account == null) throw new ArgumentNullException("account");
if (String.IsNullOrWhiteSpace(account.Username)) throw new ArgumentException("Username cannot be null for a service account. Set the username attribute for the service account in the my.serviceAccounts section in the web.config file.");
if (String.IsNullOrWhiteSpace(account.Password)) throw new ArgumentException("Password cannot be null for a service account. Set the password attribute for the service account in the my.serviceAccounts section in the web.config file.");
if (_accounts.ContainsKey(account.Username.ToLower())) throw new ArgumentException(String.Format("The username '{0}' being added to the service accounts cache already exists. Verify that the username exists only once in the my.serviceAccounts section in the web.config file.", account.Username));
_accounts.Add(account.Username.ToLower(), account);
}
}
public bool Validate(string username, string password)
{
if (username == null) throw new ArgumentNullException("username");
string key = username.ToLower();
if (_accounts.ContainsKey(key) && _accounts[key].Password == password)
{
return true;
}
else
{
return false;
}
}
}
The cache above is initialized at application startup in the Global.Application_Start method as shown below.
// Cache service accounts.
ServiceAccountsSection section = (ServiceAccountsSection)ConfigurationManager.GetSection(ServiceAccountsSection.SectionName);
if (section != null)
{
foreach (AccountElement account in section.Accounts)
{
ServiceAccountsCache.Instance.Add(new ServiceAccount() { Username = account.UserName, Password = account.Password, AccountType = (ServiceAccountType)Enum.Parse(typeof(ServiceAccountType), account.AccountType, true) });
}
}
The last piece of the puzzle is the custom configuration section on the app tier for holding the list of username/password combinations. The code for this section is shown below.
public class ServiceAccountsSection : ConfigurationSection
{
public const string SectionName = "my.serviceAccounts";
public const string AccountsTag = "accounts";
[ConfigurationProperty(AccountsTag, IsDefaultCollection = false)]
[ConfigurationCollection(typeof(AccountsCollection), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
public AccountsCollection Accounts
{
get
{
return (AccountsCollection)this[AccountsTag];
}
}
}
As before, there is a custom ConfigurationElementCollection class and a custom ConfigurationElement class. The ConfigurationElement class is shown below.
public class AccountElement : ConfigurationElement
{
[ConfigurationProperty("username", IsKey = true, DefaultValue = "", IsRequired = true)]
public string UserName
{
get { return base["username"] as string; }
set { base["username"] = value; }
}
[ConfigurationProperty("password", DefaultValue = "", IsRequired = true)]
public string Password
{
get { return base["password"] as string; }
set { base["password"] = value; }
}
[ConfigurationProperty("accountType", DefaultValue = "", IsRequired = true)]
public string AccountType
{
get { return base["accountType"] as string; }
set { base["accountType"] = value; }
}
}
These configuration classes support a .config file XML snippet as shown below. As before, this section can be encrypted.
<my.serviceAccounts>
<accounts>
<add username="myusername" password="mypassword" accountType="development" />
</accounts>
</my.serviceAccounts>
Hope this may help someone.
Upvotes: 2