|
WCF - Alternative Authentication Mechanisms for Web Services
The problem of custom authentication
When you access a web site from your browser, the site may ask you for a username and password. Technically, this is really easy to set up within IIS. You can use Basic authentication, whereby the browser pops up a dialog box asking you for your username and password.
This will authenticate you against Active Directory, where your account is stored.
What if you want to validate those credentials against something other than AD? Well, that's easy. ASP.Net incorporates an infrastructure that allows one to use Forms based authentication. In this case, the authentication is taken away from IIS, and performed directly within the application by a web page with text boxes for username and password.
You can thus use another LDAP provider, a SQL database, an XML file or any other username/password store that you prefer. Just make a call to the provider from the web page.
But what about web services? There’s no web page that you can introduce to the application to grab credentials and pass them to your database. So how can we authenticate against a SQL user store (or any other security store) for SOAP web services?
The solution
The answer lies in the plugability of WCF. A class called the UserNamePasswordValidator can be overridden to do what you need. WS-Security supplies a universal format for sending the credentials, which any client and server can use.
Let’s start by setting up a web service using WCF. Here’s the code for the service:
namespace MyServiceHost {
[ServiceContract] public interface IMyService { [OperationContract] string GetData(string val);
}
[MyServiceBehavior] public class MyService : IMyService { string IMyService.GetData(string val) { string s = System.ServiceModel.ServiceSecurityContext.Current.PrimaryIdentity.Name;
return "You sent the text:" + val + Environment.NewLine + s; } } }
It does little more than return the string value passed. However, it also adds the name of the PrimaryIdentity passed to the web service. The Primary Identity, in our example, is the identity that has been validated by our custom authentication mechanism.
Now we’ll host the service by launching it in a Windows application. Pop the following code into a Windows Form_Load event:
try { sh = new System.ServiceModel.ServiceHost(typeof(MyService)); sh.Open(); } catch (Exception ex) { MessageBox.Show(ex.ToString()); }
Here’s the config file for the Windows Forms application:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <services> <service name ="MyServiceHost.MyService" behaviorConfiguration="serviceBehave"> <endpoint address ="https://localhost:8091/DataService" binding ="wsHttpBinding" contract="MyServiceHost.IMyService" bindingConfiguration ="serviceConfig" /> </service> </services> <bindings> <wsHttpBinding> <binding name="serviceConfig"> <security mode = "TransportWithMessageCredential" > <message clientCredentialType="UserName" negotiateServiceCredential ="False" establishSecurityContext ="False"/> </security> </binding> </wsHttpBinding> </bindings> <behaviors> <serviceBehaviors> <behavior name="serviceBehave" > <serviceCredentials> <serviceCertificate x509FindType="FindBySubjectName" storeLocation="LocalMachine" storeName="My" findValue="localhost" /> <userNameAuthentication userNamePasswordValidationMode ="Custom" customUserNamePasswordValidatorType ="MyServiceHost.MyValidator, MyServiceHost" /> </serviceCredentials> </behavior> </serviceBehaviors> </behaviors> <diagnostics>
</system.serviceModel> </configuration>
Let’s look at the different aspects of the file. Firstly, the address, binding and contract for the service:
<service name ="MyServiceHost.MyService" behaviorConfiguration="serviceBehave"> <endpoint address ="https://localhost:8091/DataService" binding ="wsHttpBinding" contract="MyServiceHost.IMyService" bindingConfiguration ="serviceConfig" /> </service>
wsHttpBinding is used, as we wish to use WS-Security. The address is https, so we’re using Secure Sockets to protect the data as it passes across the network.
Now let’s look at the configuration for the binding:
<binding name="serviceConfig"> <security mode = "TransportWithMessageCredential" > <message clientCredentialType="UserName" negotiateServiceCredential ="False" establishSecurityContext ="False"/> </security> </binding>
The important bit here is the mode. TransportWithMessageCredential means that the transport (in this case, SSL) will provide confidentiality. The credentials (username and password) will be sent in the XML of the SOAP message.
So now we’ve protected the data, and sent the credentials. How do we validate these credentials against our custom data store? We need to introduce a class derived from the UserNamePasswordValidator class:
namespace MyServiceHost { class MyValidator : UserNamePasswordValidator { public override void Validate(string userName, string password) { bool valid = false; if (userName == "MyUser") { if (password == "Custard") { valid = true; } } if (valid == false) { throw new SecurityTokenException("Incorrect username or password"); ; } } } }
As we can see, it’s not doing anything sophisticated here when validating. It simply checks that the username is ‘MyUser’, and the password is ‘Custard’. The beauty of this is that we can write whatever code we like to access a SQL database, an XML file, a text file or any other store to validate the credentials.
The userNameAuthentication element in the configuration file plugs in our custom validator.
<userNameAuthentication userNamePasswordValidationMode ="Custom" customUserNamePasswordValidatorType ="MyServiceHost.MyValidator, MyServiceHost" />
I created a Windows client that calls the service. svcutil.exe will create the proxy , and the code simply calls the methods on the proxy:
private void btnGetData_Click(object sender, EventArgs e) { try { //generate an instance of the proxy to access the web service MyServiceClient msc = new MyServiceClient();
//add the credentials to the proxy msc.ClientCredentials.UserName.UserName = "MyUser"; msc.ClientCredentials.UserName.Password = "Custard";
//set the certificate used for SSL msc.ClientCredentials.ServiceCertificate.SetDefaultCertificate( System.Security.Cryptography.X509Certificates.StoreLocation.LocalMachine, System.Security.Cryptography.X509Certificates.StoreName.My, System.Security.Cryptography.X509Certificates.X509FindType.FindBySubjectName, "localhost");
//call the service and get the result string s = msc.GetData("stuff");
//show the result of the call in a text box on screen txtRes.Text = s;
} catch (Exception ex) { txtRes.Text = ex.ToString();
} }
When we press the button, we get the following displayed on the Windows Forms application. A browser control was added, to display the XML sent. Note the username and password in the XML:
Conclusion
Previously, with the older web service technology available with .Net 1.x, the only tool for authenticating credentials was Active Directory. Using a different data store involved using custom SOAP headers, which were not standard and would have to be written again for every client and server.
By combining the pluggablity of WCF and the globally accepted WS-Security schema, custom authentication mechanisms can be implemented with a small amount of configuration and one class embodying the call to your security credential store.
|