In this post I will describe how to secure a WCF RESTful service with Forms Authentication. I blogged on WCF many a times, but usually skipped right over security aspects of the service. I will go into sufficient (hopefully) level of details now.
The idea of having an un-secured service on the internet is not an appealing one. This means that anyone can connect to it and consume the data exposed by the service. Yes, granted, that person would have to discover your service somehow, but still the aspects of security cannot be ignored. As a result, we must authenticate and authorize every consumer of the service. For authentication I will use forms authentication. One of primary reasons why I want to do that is because I do not have to litter my API with user Id and password for every method. Instead I will rely on built-in functionality in ASP.NET to do a bulk of heavy lifting for me. Once ASP.NET established the user, I will generate an authorization cookie, and that cookie will be consumed by the client, and then re-submitted with requests.
So, here is my solution at a high level.
- Create authentication WCF Service
- Create Data WCF RESTful service, which has actual API I am exposing.
- Secure the site with forms authentication.
- Client will first call authentication service, get a cookie, then submit it with requests to RESTful service.
Let’s start by creating a RESTful service. Just use built-in template in Visual Studio 2010
Now because I want a real service, I am going to use Entity Framework Code First to create some basic functionality to perform CRUD operations on a Person object.
namespace CustomWcfRestService
{
public class Person
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set;}
}
}
I am also going to have a users table I am going to use for authentication.
namespace CustomWcfRestService
{
public class User
{
public int UserID { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
}
Here is sample code for my operational RESTful service. I am using json format for everything.
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Web.Script.Services;
namespace CustomWcfRestService
{
[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class CustomService
{
[WebGet(UriTemplate = "/GetPeople", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
public List<Person> GetPeople()
{
using(var ctx = new Context())
{
ctx.Configuration.LazyLoadingEnabled = false;
ctx.Configuration.ProxyCreationEnabled = false;
return ctx.People.OrderBy(one => one.LastName).ThenBy(two => two.FirstName).ToList();
}
}
[WebInvoke(UriTemplate = "/Create", Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
public Person Create(Person person)
{
using (var ctx = new Context())
{
ctx.Entry(person).State = EntityState.Added;
ctx.SaveChanges();
return person;
}
}
[WebGet(UriTemplate = "/GetPerson?id={id}", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
public Person Get(int id)
{
using (var ctx = new Context())
{
ctx.Configuration.LazyLoadingEnabled = false;
ctx.Configuration.ProxyCreationEnabled = false;
return ctx.People.Find(id);
}
}
[WebInvoke(UriTemplate = "/UpdatePerson", Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
public Person Update(Person person)
{
using (var ctx = new Context())
{
ctx.Entry(person).State = EntityState.Modified;
ctx.SaveChanges();
return person;
}
}
[WebInvoke(UriTemplate = "/GetPerson?id={id}", Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
public void Delete(int id)
{
using (var ctx = new Context())
{
var person = new Person {ID = id};
ctx.Entry(person).State = EntityState.Deleted;
ctx.SaveChanges();
}
}
}
}
I am now going to add a new service, called LoginService that I will use for authentication. It will use the same Users table to validate user name and password.
using System;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.Web.Security;
using System.Web;
namespace CustomWcfRestService
{
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class LoginService : ILoginService
{
public bool Login(string userName, string password)
{
bool returnValue = false;
User user;
using (var ctx = new Context())
{
user = ctx.Users.Where(one => one.UserName == userName).FirstOrDefault();
if (user != null)
{
returnValue = (user.Password == password);
}
}
if (returnValue)
{
var ticket = new FormsAuthenticationTicket(
1,
userName,
DateTime.Now,
DateTime.Now.AddDays(1),
true,
user.UserID.ToString()
);
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
HttpContext.Current.Response.Cookies.Add(cookie);
}
return returnValue;
}
}
}
As you can see, I validate credentials against database, then I am creating custom cookie, encrypting it and sending back to the client. I have to use ASP.NET compatibility mode to enable HttpContext and related functionality.
Now, I need to enable actual security. I am doing this entirely in Web.Config by enabling forms authentication, denying requests from un-authenticated users, then adding a Location exception just for my login service.
<?xml version="1.0"?>
<configuration>
<connectionStrings>
<add name="SecuredServiceDemo"
connectionString="Server=.;Integrated Security=SSPI;Database=SecuredServiceDemo"
providerName="System.Data.SqlClient" />
</connectionStrings>
<system.web>
<compilation debug="true" targetFramework="4.0" />
<authentication mode="Forms">
</authentication>
<authorization>
<deny users="?"/>
</authorization>
</system.web>
<location path="LoginService.svc">
<system.web>
<authorization>
<allow users="?"/>
</authorization>
</system.web>
</location>
And that is it. Now just create a test client as in following:
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;
using TestServiceApp.LoginService;
using System.IO;
namespace TestServiceApp
{
class Program
{
static void Main(string[] args)
{
var sharedCookie = string.Empty;
bool isValid;
string data = string.Empty;
var authClient = new LoginServiceClient();
using (new OperationContextScope(authClient.InnerChannel))
{
isValid = authClient.Login("me@you.com", "pw");
if (isValid)
{
var response = (HttpResponseMessageProperty)OperationContext.Current.IncomingMessageProperties[HttpResponseMessageProperty.Name];
sharedCookie = response.Headers["Set-Cookie"];
}
}
if (isValid)
{
var request = (HttpWebRequest)WebRequest.Create("http://localhost:48090/CustomService/GetPeople");
request.Headers["Cookie"] = sharedCookie;
var responce = request.GetResponse();
using (var stream = responce.GetResponseStream())
{
using (var reader = new StreamReader(stream))
{
data = reader.ReadToEnd();
}
}
}
}
}
}
Just to confirm that everything is working, I can just comment out the part that authenticates and gets a cookie, and I get 404 as expected.
You can download the complete solution here and try yourself.
For those following along at home please note that you will need to install the WCF REST Service Application project template from here
http://visualstudiogallery.msdn.microsoft.com/fbc7e5c1-a0d2-41bd-9d7b-e54c845394cd
Wow, this is dead simple. But being curious i have certain question popping in my mind.
1. If i don’t use Membership tables (default tables provided by asp.net), and create my own from scratch. So how form authentication will be created.
2. Same form authentication can be used to consume services from devices like iPhone, WP7 etc
3. encrypt of keys is secure way or there is another option available.
Thanks for wonderful article.
1. Does not really matter, since it is up to you how you authenticate and verify a login. FormAuthenticationTicket is all you need.
2. Any device that can let you obtain and send a cookie will work.
3. Forms authentication handles ecnryption for you.
Hello Nice Article..
Please Help Me to Find Out Solution
I am Facing This Problem in My REST Service
Any Idea?
to this:
[OperationContract]
[WebGet( UriTemplate = “strCustomLabelCollection/{Name},{Type},{Id}” )]
int CheckCustomLabelMaster(List strCustomLabelCollection);
Is there a way to make the Name,Type,Id be a variable length list of parameters? So it could be
Name1,name2.. …., namen? And end up with a service method such as:
int CheckCustomLabelMaster(List strCustomLabelCollection);
Or something along those lines?
That really depends on what you are doing. I think you have to have an object to wrap the data in, such as List. You also will probably have to set BodyStyle to Wrapped. And you UriTemplate should only have a the method name and no parameters. Your JavaScript call should just use your parameter names when calling ajax, assuming that is what you are using.
Hope this helps.
Hello,
Thanks For The Reply
yes i wrap the data in,such as a list..how to Fetch that Collection Through URI..I am using Desktop application..If is it Possible to pass a List of collection,Array[] or datatable through URI? if it is possible then how?i am using RESt and SOAP both Service
Url is not a good way to pass a lot of data. You should use message body instead.
Will this restrict use of the service to just .NET clients?
@KD
No, it will not. As long as client supports cookies, you are fine.
Thx for the sample.
I presume in the real world you’d also need to secure the transport with SSL otherwise you’re passing username/password and authen cookie as a clear text, right?
Also, your Login service is not RESTful but plain old SOAP meaning clients would have to use a combination of SOAP and REST to use it. Any way to make LoginService class RESTful?
Thanks.
@KotBegemot
Yes, if you are securing something, I assume are using SSL already. Yep, login service could be restful, I just made it soap based as an example. Service is a service, it just needs to set cookie for subsequent requests.
This is a great post and I’ve been playing around with the code downloaded. There is one thing I’m not sure about though. The client win app receives the cookie from the call to the Login service. Where is this cookie stored on Windows? I have worked with cookies usually set from responses received by internet browsers like IE or Chrome. I’m not sure where to go look for a cookie received by a windows app. Any tips?
Once your app has it, you can do as you please. If you do not want to persist it, and you probably do not need to, just keep it in memory while app is running.
Sergey, thank you so much for your prompt follow-up. I got a lot of inspiration from your post and I posted my modified version of this here:
https://github.com/damianof/WCF-REST-API-With-Forms-Authentication
I’m actually planning to eventually use something like this in a production environment (with SSL/HTTPS though).
Thank you again for the great post and inspiration.
I’ve posted a full DEMO of WCF-REST that using Forms Authentication here: https://github.com/damianof/WCF-REST-API-With-Forms-Authentication
and a full DEMO on how to consume that from an iOS device here: https://github.com/damianof/WCFREST-IOS-CLIENT-DEMO
Hi
I am new to WCF restful service. can you explain how to test this with fiddler2.
@vijay
Just download and run Fiddler2 when you access your service. You should be able to see authorization cookie and the request / response data. Beyond that it is up to you what you want to monitor.
Nice Post, it helped me and save my time.
Hi,
Great article! But, and how about sign out? How can I do it?
@hoisel
Just add a sign out method to your service and call FormsAuthentication.SignOut();
in it.
Nice post. Help me a lot.
but how could i do if i want to secure wcf application and asp.net website with Forms Authentication?
@Derek
There is really no difference if you are hosting both in the same web app. You do not need to write any more or any less code. If you have two different sites and you want to share a cookie, this should work as well, provided you synchronize machine keys that are used to encrypt the cookies.
Nice post. I was wondering can I use this with third party login like Twitter and LinkedIn. Once I get the access token, get the Id and Name and store it in the cookie and use it. Or am I on totally wrong track if I were to do. Please give me your advice. I appreciate it.
Thanks
This is more of oauth scenario, but it should work just as well. You just need to create custom cookie with the info you need after you validate the login against third party. Then you can use for your own cookie.
Great example…
i have one query..
after the client is authorized and is provided the encrypted cookie, how server is going to validate that one for upcoming client request?
We need to implement server side logic for that..right?
@Paritosh
Not really, unless you need to validate each request yourself. Server will attempt to decrypt the cookie and validate to make sure it is not expired and compare with the ticket it has. You do not need to do anything.
Hi
If I want to host the same service on IIS then what changes I will have to do. please suggest
The service is hosted in IIS in the example. Just the client is a console app.
nice article. you said :- There is really no difference if you are hosting both in the same web app. You do not need to write any more or any less code. If you have two different sites and you want to share a cookie, this should work as well, provided you synchronize machine keys that are used to encrypt the cookies.
can you please explain what you trying to say. hosting both means. one is wcf service but what is another one?
how to do the synchronize machine keys that are used to encrypt the cookies.
please explain in great detail thanks
Both means service and the web application. Machine keys needs to be entered into the web.config files on all machines that are used to host your web site and web service. They need to be on the same domain / IIS web site though. You can read more about machine keys just about anywhere, here is an example http://blogs.msdn.com/b/amb/archive/2012/07/31/easiest-way-to-generate-machinekey.aspx
Hi,
Its a nice article, I have a similar requirement. Instead of REST service mine is standard WCF service. When I use following code to call my service its not working, whats wrong with this code? I am using wsHttpBinding for my service
using (new OperationContextScope(loginClient.InnerChannel))
{
bool res= loginClient.Login(“username”, “password”);
var response = (HttpResponseMessageProperty)OperationContext.Current.IncomingMessageProperties[HttpResponseMessageProperty.Name];
sharedCookie = response.Headers[“Set-Cookie”];
}
MyService.MyServiceClient serviceClient1 = new MyService.MyServiceClient();
using (new OperationContextScope(serviceClient1.InnerChannel))
{
var eab = new EndpointAddressBuilder(serviceClient1.Endpoint.Address);
eab.Headers.Add(AddressHeader.CreateAddressHeader(“Cookie”,
string.Empty,
sharedCookie));
serviceClient1.Endpoint.Address = eab.ToEndpointAddress();
DateTime dt = serviceClient1.GetDate();
}
@Ramesh.
Hard to say without you providing details? When you say not working, what exactly do you mean? Client error, server error, not wright result, what is the error?
Good example
I have a query, instead of “test service app”, am created one mvc web application as client with one button click for the authentication cookie from service, and with another button click used for the calling getpeople method by using ajax call but it’s not going to method check the below code written for the Getpople():
function test() {
var passingdata = document.getElementById(“textareas”).innerHTML;
alert(passingdata);
$.ajax({
url: “http://localhost:48090/CustomService/GetPeople”,
type: “GET”,
contentType: “application/json; charset=utf-8”,
beforeSend: function (xhr) {
xhr.setRequestHeader(“Cookie”, passingdata.toString());
},
success: function (data) {
alert(data);
},
error: function (data) {
alert(data);
}
});
};
is there any configurations need to done for the calling other methods after login.
it was nice article. i go through the code and have one question. here is ur code snippet
1)
var request = (HttpWebRequest)WebRequest.Create(“http://localhost:48090/CustomService/GetPeople”);
request.Headers[“Cookie”] = sharedCookie;
var responce = request.GetResponse();
using (var stream = responce.GetResponseStream())
{
using (var reader = new StreamReader(stream))
{
data = reader.ReadToEnd();
}
}
}
when u r calling GetPeople function then you just pass the cookie value but u did not check that user is authenticated from GetPeople function……why?
i guess we always should check that user is authenticated or not from all function call. if i want to do it then how should i code this which will compare cookie value which user send.
2)
in asp.net when we set the mode=form and there we also specify our login form name but here u use location tag to protect LoginService.svc file. so which file will be called when user will try to access LoginService.svc file?
@Tridip
You do not need to check the user inside the function because it is not possible to get into the function withoun a valid authentication cookie, since the service is protected with forms auth.
Typically I just create a text file and point to it with some text that you can check for inside your client to know that you need to authenticate.
I’m having an error with the code I’ve downloaded. It says “The provider did not return a ProviderManifestToken” string. The error is occurring at LoginService at function Login specifically in the line “user = ctx.Users.Where(one => one.UserName == userName).FirstOrDefault();” Hope you can help me. Thanks much in advance
Your connection string is probably incorrect and does not match your environment. Just check the inner exception, you will see exact reason.
Please let me know the namespace for Context- using(var cts = new Context())
There is only one Context in the solution. I think you will easily find it if you search for it or just go to definition. Hope this helps.
good Article i have tried with database first entity framework ….
here i highlight your code snippet. the below code calling service and provide cookie along with request
if (isValid)
{
var request = (HttpWebRequest)WebRequest.Create(“http://localhost:48090/CustomService/GetPeople”);
request.Headers[“Cookie”] = sharedCookie;
var responce = request.GetResponse();
using (var stream = responce.GetResponseStream())
{
using (var reader = new StreamReader(stream))
{
data = reader.ReadToEnd();
}
}
}
i like to know if i do not provide cookie data or comment this line
//request.Headers[“Cookie”] = sharedCookie; then what will happen ?
if we comment this line then can’t we be able to call GetPeople() function ?
there is no check in your code of CustomService function’s for validate user ?
please answer in detail to drive out my confusion. thanks
Since we use cookies to authenticate, if you do not provide a cookie, the request will fail. User identity was authenticated once, during login call. Hence, there is no reason to continue to re-authenticate since cookie will be validated on each request, thus ensuring that the user was authenticated at some point in the past.
Hi, thanks for writing the article, it was very helpful. Can you provide an example of how to logoff the session and have the cookie destroyed on the server? What would you recommend to not allow a user allowed to login twice? Thanks again!
FormsAuthentication.SignOut() should do the trick.
Great tutorial….super like
Thank you a lot. It’s really a wonderful post.
This is great. Honestly one of the few tutorials that provides a sample which isn’t over-the-top. I’m looking for a way to expand this concept across multiple service applications that are not stored in a single project. Thanks to you, I have a foothold on what to do.
The only piece that has me concerned is the routing portion located in the Global.asax of your source code.
This works good in same application but how to add service to loginserviceclient which is on another solution, as it is form authentication protected i cannot right click and add as service reference. How can i access LoginService pleasee anyone help…
You can temporarily remove forms auth or just hand code it
Thanx for your response. But for some reason i cannot remove auth. And how to hand code.
Hand code would take an effort. You can probably google some articles on how to create WCF proxy by hand. Removing auth for a minute would be much easier.
Pingback: Forms authentication over http in WCF – how do I know which user is calling me | ASK Dev Archives
It should be just inside your identity. Principal. Currentpricipal. Identiry