Introduction
This is an extension of my previous article "WCF for the Real World, Not Hello World, Part II", being focused on authentication based on ASP.NET Identity 2.x.
Background
"WCF for the Real World, Not Hello World" and "WCF for the Real World, Not Hello World, Part II" had demonstrated how to efficiently and effectively manage WCF service development in enterprise environment through team work in order to rapidly develop and deliver decent Web services.
Because authentication and authorization are important for basic security of Web services, it is better to run automatic integration tests often to assure such basic security is not broken.
Prerequisites
- Visual Studio 2013/2015
- xUnit through NuGet
- xUnit runner for Visual Studio through NuGet
- Fonlow Testing through NuGet
Hints
The DLL files of NuGet packages are included in the repository.
Authentication and Authorization with ASP.NET Identity 2.0
As of today in July 2014, there have been quite a lot of articles published about using ASP.NET MembershipProvider
and RoleProvider
in WCF services, however, there is little about using ASP.NET Identity 2.0.
ASP.NET and WCF had provided rich sets of security features, and there are varieties of choices for authentication and authorization. Here, I would like to introduce the simple one with username and password, since this is the default way in ASP.NET MVC and Web API.
If you are building ASP.NET MVC 5 applications with Identity 2.0 for B2C use cases and will provide related Web services to external developers for B2B use cases, you would probably want to share the same mechanism for related WCF services in your multi-tier architecture.
Assigning Roles to Operations
Through declarative programming, you could assign some roles to each service implementation:
public class Service1 : IService1
{
public string GetData(int value)
{
System.Diagnostics.Debug.WriteLine("GetDataCalled");
if (value == 666)
throw new FaultException<Evil666Error>(new Evil666Error()
{ Message = "Hey, this is 666." });
return string.Format("You entered: {0}", value);
}
[PrincipalPermission(SecurityAction.Demand, Role="Customer")]
public CompositeType GetDataUsingDataContract(CompositeType composite)
{
If you have done WCF authorization with RoleManager
, you will see the code base is the same with PrincipalPermissionAttribute
. In other words, your legacy codes depending on RoleManager
don't need to be changed.
Adapting Identity Model in Codes
using System;
using System.IdentityModel.Selectors;
using System.ServiceModel;
using System.Security.Principal;
using System.Diagnostics;
namespace Fonlow.Web.Identity
{
public class IdentityValidator : UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
Debug.WriteLine("Check user name");
if ((userName != "test") || (password != "tttttttt"))
{
var msg = String.Format("Unknown Username {0} or incorrect password {1}",
userName, password);
Trace.TraceWarning(msg);
throw new FaultException(msg);
}
}
}
public class RoleAuthorizationManager : ServiceAuthorizationManager
{
protected override bool CheckAccessCore(OperationContext operationContext)
{
var roleNames = new string[] { "Customer" };
operationContext.ServiceSecurityContext.AuthorizationContext.Properties["Principal"] =
new GenericPrincipal(operationContext.ServiceSecurityContext.PrimaryIdentity, roleNames);
return true;
}
}
}
If you have your own user and role tables, it should be easy for you to replace the implementation, and you can even utilize ASP.NET Identity 2.0.
Wire Authentication and Authorization in Web.Config
<behaviors>
<serviceBehaviors>
<behavior name="authBehavior">
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="Fonlow.Web.Identity.IdentityValidator,
Fonlow.RealWorldImp" />
</serviceCredentials>
<serviceAuthorization principalPermissionMode="Custom"
serviceAuthorizationManagerType="Fonlow.Web.Identity.RoleAuthorizationManager,
Fonlow.RealWorldImp">
</serviceAuthorization>
</behavior>
</serviceBehaviors>
</behaviors>
And this line wires the behavior to the service.
<service name="Fonlow.Demo.RealWorldService.Service1"
behaviorConfiguration="authBehavior">
As you can see, comparing with the usage of MembershipProvider
and RoleProvider
, the codes and the config become shorter and more elegant.
Adapting ASP.NET Identity 2.0
I would presume you have some skills in ASP.NET MVC 5 with ASP.NET Identity 2.0 and Entity Framework, so I won't repeat what you had already known and what is available in many articles.
public class IdentityValidator : UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
using (var context = new IdentityDbContext())
{
using (var userManager = new UserManager<ApplicationUser>
(new UserStore<ApplicationUser>(context)))
{
var user = userManager.Find(userName, password);
if (user == null)
{
var msg = String.Format("Unknown Username {0}
or incorrect password {1}", userName, password);
Trace.TraceWarning(msg);
throw new FaultException(msg);
}
}
}
}
}
public class RoleAuthorizationManager : ServiceAuthorizationManager
{
protected override bool CheckAccessCore(OperationContext operationContext)
{
using (var context = new IdentityDbContext())
using (var userStore = new UserStore<ApplicationUser>(context))
{
using (var userManager = new UserManager<ApplicationUser>(userStore))
{
var identity =operationContext.ServiceSecurityContext.PrimaryIdentity;
var user = userManager.FindByName(identity.Name);
if (user == null)
{
var msg = String.Format("Unknown Username {0} .", user.UserName);
Trace.TraceWarning(msg);
throw new FaultException(msg);
}
var roleNames = userManager.GetRoles(user.Id).ToArray();
operationContext.ServiceSecurityContext.AuthorizationContext.Properties
["Principal"] = new GenericPrincipal
(operationContext.ServiceSecurityContext.PrimaryIdentity, roleNames);
return true;
}
}
}
}
As you can see, comparing 2 different implementations of class IdentifyValidator
on IdentityModel
, the whole structures are the same with or without Identity 2.0,
And you have probably recognized that IdentityDbContext
is the DbContext
class generated by VS IDE when creating the MVC 5 project. It doesn't sound elegant that your WCF projects depend on the MVC 5 project. So I would move the respective models and DbContext
out of the MVC 5 project into a project called MyCompany.Security.Identity
. Then the MVC 5 project and WCF projects could share the same auth mechanism.
Remarks
You won't see the above codes with Identity 2.0 in the demo codes attached in this article, since the extra dependencies on Identity 2.0, Entity Framework and the SQL database should be available in your MVC 5 project, and it is better to keep the demo codes small serving as demo.
And after you download and run the project, you may be seeing warning messages about SSL certificates, I will let you Google or SO to find out solutions. :)
Points of Interest
WCF and MVC are very sophisticated, comprehensive and mature, we should study more in depth, rather than write clumsy codes that work but generate huge technical debts.
Entity Framework and Identity 2.0 make things even better, so you could easily get rid of the clumsiness of legacy providers.
Obviously, it is not efficient to verify username and password through querying the db context per request, especially when the traffic is heavy. And there could be many ways to secure a WCF service in real world projects, while this article is just giving you a starting point for using Identity 2.0 in WCF.