I blogged previously about securing services with forms authentication. In this blog post I would like to describe a shortcut to creating a secure service using membership provider infrastructure.
To start with, we need to create a provider. It is pretty easy step, as you can inherit System.Web.Security.MembershipProvider and override any methods you need to. I am sticking to basic case, and all I would like to do is authenticate against a database for example. To do this, I just override ValidateUser method, and leave other methods blank (or with default code generated by studio).
public override bool ValidateUser(string username, string password) { var returnValue = false; using (var context = new ChinookEntities()) { var user = context.Employees.FirstOrDefault(one => one.Email == username); if (user != null) { if (ValidatePassword(user, password)) { returnValue = true; } } } return returnValue; }
Now, we can just update web.config to enable forms authentication and setup our new membership provider.
<authentication mode="Forms"> <forms cookieless="UseCookies" timeout="2800" slidingExpiration="true" loginUrl="/"></forms> </authentication> <authorization> <deny users="?" /> </authorization> <membership defaultProvider="default"> <providers> <clear/> <add name="default" type="MyWCFDataService.DefaultMemerbshipProvider, MyWCFDataService, Version=1.0.0.0, Culture=neutral" serviceUri="http://localhost/MyWCFDataService/Authentication_JSON_AppService.axd"/> </providers> </membership> </system.web> <location path="Authentication_JSON_AppService.axd"> <system.web> <authorization> <allow users="?" /> </authorization> </system.web> </location> <system.web.extensions> <scripting> <webServices> <authenticationService enabled="true" requireSSL="false" /> </webServices> </scripting> </system.web.extensions>
There are a few things to notice here. My membership provider is configured with assembly qualified name. I also setup default end point with built-in authentication. It must be named Authentication_JSON_AppService.axd to work. I also exclude this end point from forms authentication by using location tag and allowing unauthenticated users.
The next step is to enable form authentication in IIS for my application. If you skip this step, your entire effort is for not.
Now, let’s take a look at how to use this from WinRT application. I am into extension methods these days, so I added new extension method Login to my wcf proxy. I can add proxy code to my WInRT application by simply adding service reference and pointing to my WCF Service svc file. Again, you will need to disable forms authentication in web.config, since proxy generation cannot work with protected end points.
Let’s take a look at the extension method.
public static async Task<Cookie> Login(this DataServiceContext context, string userName, string password) { HttpClientHandler handler = new HttpClientHandler(); handler.CookieContainer = new CookieContainer(); var client = new HttpClient(handler); var uri = new Uri(context.BaseUri.ToString().Replace(@"MyDataService.svc/", @"Authentication_JSON_AppService.axd/Login")); string authBody = String.Format( "{{ "userName": "{0}", "password": "{1}", "createPersistentCookie":false}}", userName, password); var responce = await client.PostAsync(uri, new StringContent(authBody, new UTF8Encoding(), "application/json")); var cookies = handler.CookieContainer.GetCookies(uri); if (cookies.Count == 0) { return null; } return cookies[".ASPXAUTH"]; }
I need to call this method first thing, in my case from my view model
private async void Load() { IsBusy = true; _cookie = await _chinookEntities.Login("andrew@chinookcorp.com", "1"); if (_cookie == null) { MessageDialog dialog = new MessageDialog("Login failed"); dialog.ShowAsync(); } else { // run your code } IsBusy = false; }
You can use await when calling this method. It is written using Task<Cookie> as return value. Once I have the cookie I can inject it into subseqnet requests using SendingRequest event
public MainViewModel() { _myEntities.SendingRequest += OnSendingRequest; } void OnSendingRequest(object sender, SendingRequestEventArgs e) { e.RequestHeaders["Cookie"] = _cookie.ToString(); }
In summary, if you do not need custom authentication and you only need basic password validation, membership provider and built-in authentication services will offer you this functionality with less code.
Enjoy.
Hi Sergey,
I tried your approach and it works well with IIS and DB on premise.
After I migrate the service and the DB to Azure Cloud Services.
If I call the login method from the client, I get no cookie.
In the application eventlog of the Azure machine it get the error message:
Event code: 3005
Event message: An unhandled exception has occurred.
Event time: 4/4/2013 3:15:42 PM
Event time (UTC): 4/4/2013 3:15:42 PM
Event ID: e9eecf0d17b74832a76f93d07d71a7db
Event sequence: 2
Event occurrence: 1
Event detail code: 0
Application information:
Application domain: /LM/W3SVC/1273337584/ROOT-1-130095621418513750
Trust level: Full
Application Virtual Path: /
Application Path: E:sitesroot
Machine name: RD00155D57226D
Process information:
Process ID: 3600
Process name: w3wp.exe
Account name: NT AUTHORITYNETWORK SERVICE
Exception information:
Exception type: ArgumentException
Exception message: Unknown web method Login/.
Parameter name: methodName
at System.Web.Script.Services.RestHandler.CreateHandler(WebServiceData webServiceData, String methodName)
at System.Web.Script.Services.ScriptHandlerFactory.GetHandler(HttpContext context, String requestType, String url, String pathTranslated)
at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
Request information:
Request URL: http://538c04a574994db1b4e9d680dbc58238.cloudapp.net/Authentication_JSON_AppService.axd/Login/
Request path: /Authentication_JSON_AppService.axd/Login/
User host address: 88.153.72.88
User:
Is authenticated: False
Authentication Type:
Thread account name: NT AUTHORITYNETWORK SERVICE
Thread information:
Thread ID: 6
Thread account name: NT AUTHORITYNETWORK SERVICE
Is impersonating: False
Stack trace: at System.Web.Script.Services.RestHandler.CreateHandler(WebServiceData webServiceData, String methodName)
at System.Web.Script.Services.ScriptHandlerFactory.GetHandler(HttpContext context, String requestType, String url, String pathTranslated)
at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
thanks for any help.
Frank
Did you check your web.config to make sure it is correct and has correct URL in it?
Oh, yeah. If this is an MVC, put an exception into route table not to handle AXD extension.
I did not change the URL:
var uri = new Uri(TMSDataService.BaseUri.ToString().Replace(@”TMSDataService.svc”, @”Authentication_JSON_AppService.axd/Login”));
And it is not an MVC.
I think IIS on Azure machine cannot handle the login URL, but I don´t know why.
I compared all settings between on premise IIS and Azure IIS, but I can´t find a difference.
You might want to check all errors in event log. Anything there>
in my first comment, I copied the error of the event log.
In the meantime I tried the sample code from your Las Vegas session (WCF Data Services). There you uncomment this code:
var uri = new Uri(context.BaseUri.ToString().Replace(@”ChinooDataService.svc/”, @”LoginService.svc/Login?userName=” + userName + “&password=” + password));
I wanted to try this approach, but unfortunatly there is noch code file for LoginService.svc.
Do you still have this code?
Right here
http://dotnetspeak.com/index.php/2012/01/securing-wcf-with-forms-authentication/
kindly share the code. the given link is not working
http://dotnetspeak.com/2012/01/securing-wcf-with-forms-authentication