Posted by AlexanderZeitler on 3/2/2011 9:34 PM | Comments (1)

Since a while Microsoft is working on WCF to ease the usage in RESTful scenarios.

The new WCF HTTP APIs make hosting WCF services in (existing) ASP.NET (MVC) Websites easier without having the configuration overhead as before.

The current builds actually lack of simple authentication and authorization, but there are plans to support OAuth in the near future.

I’ve been asking myself, why not just using existing and reliable techniques like ASP.NET Forms Authentication.

After some attempts I have been able to get a (almost for my use cases) working solution running

Requirements

My authorization and authentication requirements are:

  • Role based Forms Authentication both for the ASP.NET MVC 3 website and the WCF HTTP services hosted inside
  • RESTful authentication should not parse or fill forms on a website but use forms authentication credentials against a WCF HTTP authentication service
  • Inside a browser RESTful services should return XML
  • Invoked from a console test client the RESTful services should return JSON

Implementing the service host

The solution is based on  WCF Web APIs Preview 3.

First we create an empty ASP.NET MVC 3 website.

XML is returned by the WCF HTTP APIs automatically if requested.

The response JSON we need a so called JsonProcessor which is included in the APIs.

To be able to process the input from the form we need to use a FormUrlEncodedProcessor which also already exists.

This leads us to the following service configuration:

public class ContactManagerConfiguration : HttpHostConfiguration, IProcessorProvider {
	private readonly CompositionContainer _container;

	public ContactManagerConfiguration(CompositionContainer container) {
		_container = container;
	}

	public void RegisterRequestProcessorsForOperation(HttpOperationDescription operation, IList<Processor> processors, MediaTypeProcessorMode mode) {
		processors.Add(new JsonProcessor(operation,mode));
		processors.Add(new FormUrlEncodedProcessor(operation,mode));
	}

	public void RegisterResponseProcessorsForOperation(HttpOperationDescription operation, IList<Processor> processors, MediaTypeProcessorMode mode) {
		processors.Add(new JsonProcessor(operation,mode));
	}

	public object GetInstance(Type serviceType, InstanceContext instanceContext, Message message) {
		var contract = AttributedModelServices.GetContractName(serviceType);
		var identity = AttributedModelServices.GetTypeIdentity(serviceType);
		var definition = new ContractBasedImportDefinition(contract, identity, null, ImportCardinality.ExactlyOne, false,
			                                                false, CreationPolicy.NonShared);
		return _container.GetExports(definition).First().Value;
	}
}

We’ll create two services:

  • Contact Service, contained in the ContactResource class
  • Login Service, contained in the LoginResource class

The implementions will be shown later on.

Both services are registered inside Global.asax.cs

In order to register the so called ServiceRoutes an extension method AddServicesRoute<TServiceResourse>() has been introduced.

The registration is the following:

var catalog = new AssemblyCatalog(typeof(Global).Assembly);
var container = new CompositionContainer(catalog);
var configuration = new ContactManagerConfiguration(container);
RouteTable.Routes.AddServiceRoute<ContactResource>("contact", configuration);
RouteTable.Routes.AddServiceRoute<LoginResource>("login", configuration);

To be able to run WCF HTTP and normal MVC routes inside the same application, the WCF HTTP routes have to be filtered from the MVC routes which is done by an IRouteConstraint:

public class WcfRoutesConstraint : IRouteConstraint {
	public WcfRoutesConstraint(params string[] values) {
		this._values = values;
	}

	private string[] _values;

	public bool Match(HttpContextBase httpContext,
	Route route,
	string parameterName,
	RouteValueDictionary values,
	RouteDirection routeDirection) {
		// Get the value called "parameterName" from the
		// RouteValueDictionary called "value"
		string value = values[parameterName].ToString();

		// Return true is the list of allowed values contains
		// this value.
		bool match = !_values.Contains(value);
		return match;
	}
}

The WcfRouteConstraint is passed to the MapRoute definition:

routes.MapRoute(
	"Default", // Route name
	"{controller}/{action}/{id}", // URL with parameters
	new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
	new { controller = new WcfRoutesConstraint(new string[] {"contact","login"}) }
);

Die ContactResource looks as follows – to keep it simply without any database access etc.:

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceContract]
[Export]
public class ContactResource {
	[ImportingConstructor]
	public ContactResource() {
			
	}
sieht wie folgt ausschnel
	[WebGet(UriTemplate="{id}")]
	public ContactDto Get(string id, HttpResponseMessage responseMessage) {
		var contact = new ContactDto
			            {
			              	Name = "Alexander Zeitler"
			            };
		return contact;
	}
}

The LoginResource looks as follows:

[ServiceContract]
[Export]
public class LoginResource {
	[ImportingConstructor]
	public LoginResource() {
			
	}

	[WebInvoke(UriTemplate="", Method = "POST")]
	public void Login(Credentials credentials, HttpResponseMessage responseMessage) {
		bool auth = Membership.ValidateUser(credentials.Username, credentials.Password);

		if (auth) {
			FormsAuthentication.SetAuthCookie(credentials.Username,true);
		}
		else {
			responseMessage.StatusCode = HttpStatusCode.Unauthorized;
		}
	}
}

The explanation is simple: Using an self implemented Credentials parameter having two properties Username and Password the login data is passed by a POST-method into the service.

The credentials are being authentication against the ASP.NET membership database (you’ll have to setup it using aspnet_regsql.exe).

When succeeding the ASP.NET FormsAuthentication cookie is returned.

When failing HTTP error 401 unauthoried is returned.

To get the ASP.NET FormsAuthentication working the web.config has to be modified.

First the URLs being protected have to be blocked for anonymous users.

The login service (not the web page here!) has to be visible to all users:

<location path="">
	<system.web>
		<authorization>
			<allow roles="Admins"/>
			<deny users="*"/>
		</authorization>
	</system.web>
</location>
<location path="login">
	<system.web>
		<authorization>
			<allow users="*"/>
		</authorization>
	</system.web>
</location>

This is the FormsAuthentication configuration required:

<authentication mode="Forms">
    <forms loginUrl="~/Account/LogOn" timeout="2880" name=".ASPXFORMSAUTH" />
</authentication>

The account controller looks as follows:

public class AccountController : Controller
{
	[HttpGet]
    public ActionResult Logon() {
		Response.StatusCode = (int)HttpStatusCode.Unauthorized;
        return View();
    }



	[HttpPost]
	public ActionResult Logon(string username, string password) {
		if(Membership.ValidateUser(username, password)) {
			FormsAuthentication.SetAuthCookie(username,true);
		}
		return View();
	}

	public ActionResult LogOff() {
		FormsAuthentication.SignOut();
		return RedirectToAction("Logon", "Account");
	}
}

The parameterless “Logon” Action is used to block anonymous calls from RESTful clients.

The POST version of “Logon” is used for FormsAuthentication inside a browser.

The “LogOff” Action should be self-explanatory…

The appropriate Logon-View consists of a form containing both Username and Password input fields, the submit button plus an ActionLink to the LogOff Action method.

Implementing the service client

The client has to functions:

  • Authentication
  • Reading the contact data when being authenticated

Authentication is done using the HttpWebRequest class:

HttpWebRequest loginRequest = (HttpWebRequest)HttpWebRequest.Create("http://localhost:44857/login");
loginRequest.Method = "POST";


CookieContainer cookieContainer = new CookieContainer();
loginRequest.CookieContainer = cookieContainer;
loginRequest.ContentType = "application/x-www-form-urlencoded";
ASCIIEncoding encoding = new ASCIIEncoding();
string postData = "Username=foo&Password=bar";
byte[] data = encoding.GetBytes(postData);

loginRequest.ContentLength = postData.Length;
Stream dataStream = loginRequest.GetRequestStream();
dataStream.Write(data, 0, data.Length);
dataStream.Close();

loginRequest.GetResponse();

It’s important to set the ContentType and using the CookieContainer.

The CookieContainer stores the received cookies from the server (servcice) after loginRequest.GetResponse() is called.

If authentication has been successful the contact data can be read from the contact service.

To pass the request to the protected contact service through the FormsAuthentication, the cookie being received before needs to be passed within the request which is done be re-using the CookieContainer:

HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create("http://localhost:44857/contact/1");
request.CookieContainer = cookieContainer;
request.Accept = "application/json";
request.Method = "GET";
try {
	HttpWebResponse response = (HttpWebResponse)request.GetResponse();
	Stream responseStream = response.GetResponseStream();
	StreamReader reader = new StreamReader(responseStream, Encoding.UTF8);
	string result = reader.ReadToEnd();
	JavaScriptSerializer jsonDeserializer = new JavaScriptSerializer();
	ContactDto contact = jsonDeserializer.Deserialize<ContactDto>(result);
	Console.WriteLine(contact.Name);
	Console.ReadLine();
}
catch (WebException e) {
	Console.WriteLine(((HttpWebResponse)e.Response).StatusCode);
	Console.ReadLine();
}

Calling the Website (http://localhost:44857/contact/1) after a successful login (http://localhost:44857/Account/Logon) returns the contacts XML definition:

XML

The console client will return the de-serialized JSON:

JSON

If authentication fails the “Unauthorized” status code will be returned:

Unauthorized

 

Please note that I've have posted an update of the client that uses the shiny new HttpClient.

Hence all requirements have been implemented.

As well as the early bits of the WCF HTTP APIs this solution doesn’t raise a claim of being complete or working perfectly and should be used as a basis for discussion.

You can download the sample implementation from here: WcfHttpMvcAuth.zip (9.45 mb)

DotNetKicks-DE Image

Comments

Comments are closed