Download Sample Visual Studio Solution
Scenario:
You have a web site or other http services that are secured via the ASP.Net Membership Provider and you want to access some of these resources from code – such as a class library or desktop application. Users of the web site obviously will be sent to a login page that takes their username and password and then authenticates the user. But how do you authenticate against the same Membership Provider when you are not using a web browser, and therfore cannot interact with a login page?
The answer is to utilize the System.Net.HttpWebRequest class to make the requests. As with a login page, this will be a two step process. First, send a request to a resource that specifically exists for setting the authentication cookie and ticket. This is like the login page but there is no graphical user interface. Secondly, keep a reference to the returned cookie and attach it to any future requests for restricted resources. Normally this is done for you by the browser. But when you are communicating in code only, you don’t have the browser so you must do it yourself. If you are not familiar how the cookie and tickets work, here is a ten second overview:
The web server generates a ticket which is just some information about the logged in user. Settings in the machine.config file on the server are used to to encrypt the ticket data. Then that data is put into a cookie which is sent along with the response. The ticket is not decrypted on the client (browser). It is simply sent along with any requests, and the server knows how to decrypt it and decide if the user is logged in or not.
If you arrived here from a search, you probably found other blogs or forums where the authentication ticket is retrieved by making a request to the Login.aspx page. You then have to programmatically access its controls. But if you have control over the site, you can simplify the task and increase maintainability by creating a generic http handler to set the authentication ticket.
If you’ve never used HttpHandlers before, don’t worry, it is simple simple simple. This is just a class designed for http communication and Visual Studio provides a template for you. You could use an empty aspx page as well, but aspx pages are meant to send pages for display and HttpHandlers are meant to simply accept and respond to requests with any arbitrary payload you want – text, JSON, HTML, XML, documents, images…etc.
The solution contains a VS2012 web site and console applicaton that you can run and play with.
First the web site.
We need to setup ASP.Net Membership and configure some resources to be available anonymously and some to be restricted to authenticated users.
In the sample project, everything in the root of the AuthSite website is open to anonymous access. The “users” folder is restricted to authenticated users only.
Default.aspx – simply displays some text to let you know the site is running. It is the default page to show when you run the project. You must have the site running in order to hit it with the WebAuth console project.
FakeLogin.html – This page stands in place of a real login page. The web.config is configured to direct the user here if they try to access a restricted resource and they are not signed in. I chose to implement this fake login page this way because, in the console application that makes requests to this site, I didn’t want a bunch of markup written to the console.
Handler.ashx – a generic http handler that extracts a username and password from a request and tries to authenticate the user against the ASP.Net Memberhip Provider. If it succeeds, a FormsAuthenticationTicket is created and sent along in a cookie that is what future requests will need to access restricted resources.
Visual Studio has built in templates for GenericHandlers. Right-click the project folder you want to contain this handler and Add New Item. Scroll through the list and choose Generic Handler. Make sure it is located in an anonymously accessible location. This is for the same reason that you would not put a Login.aspx file in a location that requires authentication – you wouldn’t be able to even get to the Login page. The entire code set is:
<%@ WebHandler Language="C#" Class="Handler" %> using System; using System.Web; public class Handler : IHttpHandler { public void ProcessRequest (HttpContext context) { context.Response.ContentType = &amp;quot;text/plain&amp;quot;; string un = context.Request.QueryString[&amp;quot;un&amp;quot;].ToString(); //extract user name string pw = context.Request.QueryString[&amp;quot;pw&amp;quot;].ToString(); //extract password … you should check for nulls, etc. if (System.Web.Security.Membership.ValidateUser(un, pw)) //utilize the Membership provider api to see if credentials are valid { //Create a FormsAuthenticationTicket. This is the same thing that the Membership Provider will create // and set for you when you are using a browser to access a site. System.Web.Security.FormsAuthenticationTicket ticket = new System.Web.Security.FormsAuthenticationTicket( 1, // ticket version un, // user name associated with the ticket DateTime.Now, // issue date DateTime.Now.AddMinutes(100), // expiration date true, // is persistent (saved across browser sessions) &amp;quot;user data here&amp;quot;, // user specific data to be stored with the ticket System.Web.Security.FormsAuthentication.FormsCookiePath); // path for ticket when stored in cookie //Encrypt the ticket. string encryptedticket = System.Web.Security.FormsAuthentication.Encrypt(ticket); //Create a cookie and set its data to the encrypted ticket. HttpCookie cookie = new HttpCookie(System.Web.Security.FormsAuthentication.FormsCookieName, encryptedticket); //Add the cookie to the response. It will be sent to the client. context.Response.Cookies.Add(cookie); //No response content is required, this is just for reference in the console test app. context.Response.Write(&amp;quot;Message from http handler: Access granted, here is a cookie for you..&amp;quot;); } else { context.Response.Write(&amp;quot;Denied. Could not authenticate with the given username/password.&amp;quot;); } //finish the response context.Response.Flush(); HttpContext.Current.ApplicationInstance.CompleteRequest(); } //Generated by default, just leave it. public bool IsReusable { get { return false; } } }
Web.config – sets the sites authentication mode to “Forms”, configures the login url, and gives a name to the authentication cookie. The sample site utilizes default behavior to configure the Membership Provider so you will not see connection strings and nodes for RoleProvider, etc. How to use the Membership Provider is beyond the scope of this article. Just know that the Membership database is included in the App_Data folder and will “hook” up automatically when running this project in Visual Studio 2012.
users -> Web.config – utilizes an authorization node to deny anonymous users to this folder. Any request to resources in this folder will fail if the user is not logged in. The Membership Provider will send the user to the configured login page unless they are signed in. But if the request has a valid authentication ticket in the cookie, then the requested resource will be delivered.
users -> test.aspx – Just a page against which to make a request that will either generate a response or redirect to the configured login page. The console project does not try to access this page, instead it tests against RestrictedHandler.ashx (explained below). This is because I just wanted simple text returned to the console and not a bunch of html markup. Test.aspx is included so that you can investigate further to see what happens when you send a request to that resource.
RestrictedHandler.ashx – a generic handler within the restricted “users” folder that simply returns some text via HTTP. It is just like requesting an aspx page, but it is easier to send some simple plain text in the response with a Handler than with an aspx page. The content depends on whether or not the requesting user is logged in.
The Console Application:
All the action happens in MakeRequest() which is called from Main().
First, some configuration:
Set the port if necessary, or empty string if not. When running in the Visual Studio web server, a random port will be assigned. Change this to your value:
string port = &amp;quot;:52434&amp;quot;;
A resource we want to access only if authenticated:
string secureResource = &amp;quot;http://localhost{0}/users/RestrictedHandler.ashx&amp;quot;;
The handler/service that will set our authentication cookie. This handler’s only purpose is to try and authenticate the user and create the cookie and authentication ticket. The username and password are sent as query string parameters. In a real application YOU BETTER be using https!:
string cookieSetterResource = &amp;quot;http://localhost{0}/Handler.ashx?un=Webber&amp;amp;pw=p@ssword&amp;quot;;
Try an unauthenticated request to our restricted resource. We create a failure first to see the response and prove that Membership is doing its job to keep anonymous users out. Notice we string.Format the secureResource string with the port number of your localhost site:
System.Net.HttpWebRequest unauthorizedRequest = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(string.Format(secureResource, port)); System.Net.HttpWebResponse rejectedResponse = System.Net.HttpWebResponse)unauthorizedRequest.GetResponse(); using (System.IO.Stream rejectedResponseStream = rejectedResponse.GetResponseStream()) { using (System.IO.StreamReader rejectedResponseReader = new System.IO.StreamReader(rejectedResponseStream)) { Console.WriteLine(rejectedResponseReader.ReadToEnd()); } }
Now we know that this fails, lets fix it. We create a request to our handler that is responsible for setting the cookie and ticket. This is doing manually what happens when you click the login button on a login page.
System.Net.HttpWebRequest request = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(string.Format(cookieSetterResource, port)); System.Net.CookieContainer cookiecontainer = new System.Net.CookieContainer(); request.CookieContainer = cookiecontainer; System.Net.HttpWebResponse response = (System.Net.HttpWebResponse)request.GetResponse();
We don’t need to do anything with the response, we just want the reference to the cookiecontainer because it will have the authentication cookie and ticket if the username and password were valid.
Now that we have our cookie, we can make requests to secure resources. Notice the only real difference between this request and the first one is that we are setting the CookieContainer on the request. This is the same CookieContainer reference that we set after the response from cookieSetterResource . This is critical because that instance (cookiecontainer) has a valid ticket. If we did : authenticatedrequest.CookieContainer = new CookieContainer() it would not work.
System.Net.HttpWebRequest authenticatedrequest = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(string.Format(secureResource,port)); authenticatedrequest.CookieContainer = cookiecontainer; System.Net.HttpWebResponse authenticatedResponse = (System.Net.HttpWebResponse)authenticatedrequest.GetResponse(); using (System.IO.Stream authenticatedResponseStream = authenticatedResponse.GetResponseStream()) { using (System.IO.StreamReader authenticatedResponseReader = new System.IO.StreamReader(authenticatedResponseStream)) { Console.WriteLine(authenticatedResponseReader.ReadToEnd()); } }
So you should see it is very easy to make use of ASP.Net Membership Provider from code. The resources I have found on the web do not explain the whole process, or they utilize an existing Login.aspx page to retrieve the authentication cookie. This would be necessary if the site you are communicating with is out of your control, but if you have the ability to add resources to the site, this is a better solution.