This post presents a barebones yet complete demonstration of security policies suitable for the Internet scenario.
Other posts in this series:
WCF Security Scenarios – Barebones (Overview)
WCF Security Scenarios – Barebones - Part 1 (Intranet)
WCF Security Scenarios – Barebones - Part 2 (Anonymous)
WCF Security Scenarios – Barebones - Part 3 (Business-to-Business)
Source code for all scenarios can be downloaded here.
Scenario: Internet
This scenario occurs when you want to provide a service to authenticated users over the internet.
The key characteristics of this scenario are:
- Interoperability: Required, client or server may not be running Windows or WCF.
- Firewall: Communication is over the internet, crossing a firewall.
- Point-to-Point: Messages may be redirected over channels which are not securable.
- Client Identity: The service and client need to authenticate each other.
The client is not a business so it does not have a certificate.
Security Policy
- We must use an http binding to allow traffic to cross a firewall.
- The security mode needs to be Message as oppose to Transport.
Message protection requires more processing than Transport but is required because there is no guarantee that messages will not travel over insecure channels on their way between the client and server. - The client will authenticate the server using the server’s X.509 certificate.
- The server will authenticate the client using a username and password which will be stored and managed in a dedicated database.
Setting up the SqlMembershipProvider and SqlRoleProvider
In this scenario client authentication will be implemented with a username and password.
It is not practical to give all users from the Internet a local account on our Windows machine (or on the domain). Instead we need to use another store implementing username/passwords and roles.
To assist us with this task, .Net provides the Membership and Roles APIs (in the System.Web.Security namespace), implements a provider pattern, allowing you to implement your own implementation of these APIs and provides a default implementation that uses a database in SQL Server. (Though the API and the elements in the configuration files for this model all use the System.Web namespace you can use them freely in any .Net application. You do have to reference, however the System.Web assembly).
In order to use the SQL providers you must first set them up using the aspnet_regsql.exe wizard. This setups a database (which is by default called aspnetdb) to host your application specific users and roles.
This demo assumes there two users in the system, one called Bill, the other Fred and that there is one Role in the system called Admin. Only Bill is a member of Admin.
To set this up I have wrote a small project called SetupMembership (provided with the source code) that makes the following calls to the Membership/Role API:
namespace SetupMembership
{
class Program
{
static void Main(string[] args)
{
try
{
string[] userNames = new string[] { "Bill", "Fred" };
string roleAdmin = "Admin";
Roles.DeleteRole("Admin", false);
foreach (string user in userNames)
{
Membership.DeleteUser(user, true);
}
Membership.CreateUser("Bill", "billbill$");
Membership.CreateUser("Fred", "fredfred$");
Roles.CreateRole(roleAdmin);
Roles.AddUserToRole("Bill", roleAdmin);
Console.WriteLine("Under application {0}", Membership.ApplicationName);
Console.WriteLine("you now have two users, Bill and Fred");
Console.WriteLine("and one role, Admin.");
Console.WriteLine("Bill is a member of Admin");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}
}
}
This code is backed up in the SetupMembership app.config file with the following settings:
<connectionStrings>
<add name="AspNetDb"
connectionString =
"Data Source=localhost;
Initial Catalog=aspnetdb;
Integrated Security=true"/>
</connectionStrings>
<system.web>
<membership defaultProvider="MySqlMembershipProvider">
<providers>
<add name="MySqlMembershipProvider"
type="System.Web.Security.SqlMembershipProvider"
connectionStringName="AspNetDb"
requiresQuestionAndAnswer="false"
requiresUniqueEmail="false"
applicationName="WcfSecurityScenarios"/>
</providers>
</membership>
<roleManager enabled="true"
defaultProvider="MySqlRoleManager">
<providers>
<add name="MySqlRoleManager"
type="System.Web.Security.SqlRoleProvider"
connectionStringName="AspNetDb"
applicationName="WcfSecurityScenarios"
/>
</providers>
</roleManager>
</system.web>
This is the explanation of the file from top to bottom.
The default provider to be used for the MemberShip API is one called MySqlMembershipProvider which is of type System.Web.Security.SqlMembershipProvider. Such an object should be instantiated and scoped to an the application named WcfSecurityScenarios (other applications won’t see the users of this application). It should use the AspNetDb connection string to connect to the data store.
The default provider to be used for the Roles API is one called MySqlRoleManager which is of type System.Web.Security.SqlRoleProvider. Such an object should be instantiated and scoped to an the application named WcfSecurityScenarios (other applications won’t see the users of this application). It should use the AspNetDb connection string to connect to the data store.
Now let’s look at how we will use the Membership and Roles.
Implementing Role-based Security
On the Server
On the server we bar access to non-Admin members like so:
namespace CalculatorService
{
[ServiceBehavior]
public class Calculator : ICalculator
{
#region ICalculator Members
public double Add(double a, double b)
{
if (! Roles.IsUserInRole("Admin"))
{
throw new Exception(
"You are not authorized to perform this operation");
}
return a + b;
}
#endregion
}
}
On the Client
On the client we present the username and password like so:
namespace Client
{
class Program
{
void Run()
{
try
{
RunAs ("Bill", "billbill$");
RunAs ("Fred", "fredfred$");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private static void RunAs(string userName, string password)
{
Console.WriteLine("Try as " + userName);
CalculatorClient calc = new CalculatorClient();
calc.ClientCredentials.UserName.UserName = userName;
calc.ClientCredentials.UserName.Password = password;
try
{
double result = calc.Add(5, 6);
Console.WriteLine("Result = {0}", result);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
static void Main(string[] args)
{
new Program().Run();
Console.ReadLine();
}
}
}
Service Configuration File
Now lets look at the configuration file at the service side:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="CalculatorService.Calculator"
behaviorConfiguration="InternetServiceBehavior">
<endpoint address="Calculator"
binding="wsHttpBinding"
bindingConfiguration="InternetBindingConfiguration"
contract="CalculatorService.ICalculator" />
<endpoint address="mex"
binding="mexHttpBinding"
contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="http://localhost/Services" />
</baseAddresses>
</host>
</service>
</services>
<bindings>
<wsHttpBinding>
<binding name="InternetBindingConfiguration">
<security mode="Message">
<message clientCredentialType="UserName"/>
</security>
</binding>
</wsHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="InternetServiceBehavior">
<serviceCredentials>
<!--makecert -n CN=TestServiceCert -sr LocalMachine -ss My -sky exchange -pe-->
<serviceCertificate
findValue="TestServiceCert"
storeLocation="LocalMachine"
x509FindType="FindBySubjectName"
storeName="My"/>
<userNameAuthentication
userNamePasswordValidationMode="MembershipProvider"/>
</serviceCredentials>
<serviceAuthorization
principalPermissionMode="UseAspNetRoles"/>
<serviceMetadata/>
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
<connectionStrings>
<add name="AspNetDb"
connectionString =
"Data Source=localhost;
Initial Catalog=aspnetdb;
Integrated Security=true"/>
</connectionStrings>
<system.web>
<membership defaultProvider="MySqlMembershipProvider">
<providers>
<add name="MySqlMembershipProvider"
type="System.Web.Security.SqlMembershipProvider"
connectionStringName="AspNetDb"
requiresQuestionAndAnswer="false"
requiresUniqueEmail="false"
applicationName="WcfSecurityScenarios"/>
</providers>
</membership>
<roleManager enabled="true"
defaultProvider="MySqlRoleManager">
<providers>
<add name="MySqlRoleManager"
type="System.Web.Security.SqlRoleProvider"
connectionStringName="AspNetDb"
applicationName="WcfSecurityScenarios"
/>
</providers>
</roleManager>
</system.web>
</configuration>
This is a long file, but we have seen most of the components already.
Reading from the top:
This application hosts a service called Calculator.Service at an end point whose address is http://localhost/Services/Calculator, uses the wsHttpBinding and exposes the CalculatorService.ICalculator contract.
The application also provides metadata on a mex endpoint (configured as an endpoint, and a serviceMetadata entry in the B2BServiceBehavior at the bottom of the file).
The configuration for the wsHttpBinding, called InternetBindingConfiguration will be implementing Message security. Clients will be authenticated using a username and password.
The service implements a behavior called “InternetServiceBehavior” which declares the service certificate we created earlier for the service as the “service credential”.
In addition, under serviceCredentials we indicate that the client username and password that will be presented for authentication will be validated against the Membership API and that Roles will be validated against the Roles API.
Outside and following <serviceModel> you can see the configuration elements that setup the providers for Membership and Roles. These are identical to those described in the SetupMembership project above.
Client Configuration File
Here is the App.config for the client:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<client>
<endpoint address="http://localhost/Services/Calculator"
binding="wsHttpBinding"
bindingConfiguration="InternetBindingConfiguration"
behaviorConfiguration="InternetEndpointBehavior"
contract="ServiceReferences.ICalculator">
<identity>
<dns value="TestServiceCert"/>
</identity>
</endpoint>
</client>
<bindings>
<wsHttpBinding>
<binding name="InternetBindingConfiguration">
<security mode="Message">
<message clientCredentialType="UserName"/>
</security>
</binding>
</wsHttpBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="InternetEndpointBehavior">
<clientCredentials>
<serviceCertificate>
<authentication certificateValidationMode="PeerTrust"/>
</serviceCertificate>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
This application will instantiate proxies for the calculator service that will communicate across the endpoint whose address is http://localhost/Services/Calculator, that uses the wsHttpBinding and exposes the CalculatorService.ICalculator contract.
Note that the Endpoint has an identity element that indicates that the expected identity of the service is not ‘localhost’ (the default name is the url of the machine) but the name of the certificate.
The configuration for the wsHttpBinding, called InternetBindingConfiguration will be implementing Message security to protect messages that crosses the channel. A username and password will be presented as client credentials to the service.
The endpoint also has a behavior called InternetEndpointBehavior in which it specifies clientCredentials which specify how the client authenticates the server. It does so using a serviceCertificate which will be trusted if the certificate can be found in the Trusted People store of the Current User.
Summary
In this series of five posts we have reviewed recommended security policies for four scenarios of security: Intranet, Anonymous, Business-to-Business, Internet that are identified in Juval Lowy’s book Programming WCF Services.
Foreach I have shown a demo that is implemented almost entirely through two configuration files, one for the client and one for the service.
The source code for the demos can be found here.