Asp.Net: Authentication based on HTTP Basic protocol. HttpModule.
Author: Anatoly Lubarsky
Published: 15/11/2002
on aspnetmania.com
Many web developers use regular HTML forms in order to provide the information about user permissions to access pages of certain web-application.
This is probably due to the fact that these forms are well known. Also they can provide and request additional information, along with username and password. As soon as the user is authenticated, the application starts to follow up the session, in order to grant him access to other pages.
However this approach also has a number of disadvantages:
- Increases the level of complexity.
- Application "transparency": for example, it is often enough just to look into the login page Source to calculate target of form submit and its parameters.
- Security vulnerabilities and Cross-site attacks: http://www.cert.org/advisories/CA-2000-02.html
- attacks, using different Http Clients
Implementation of HTTP Basic protocol in Asp.Net
In this article, I will show another approach to grant the access to an anonymous user. The authentication system is based on HTTP Basic protocol. I have to mention that the down side of this approach is the fact, that in "basic" the username and the password are passed through HTTP header, but as clear text. This drawback is overcome in HTTP "digest" approach, which is the topic of another article.
Take a look at this window [Update: authentication window screenshot missing]:
Sometimes, in forums over the net, one can see questions like: "How can I show standard modal form: login, password, domain for an anonymous user ?"
If this question is answered at all, the most common answer is: "There are special ISAPI filters to perform this."
The most famous commercial ISAPI filter of that type is Authentix. In theory, you could write it yourself. However, it’ is not as easy as it sounds on one hand and on the other hand, people, having different hosting arrangements are not able to put ISAPI on remote HTTP Server.
Authentication logic, based on HTTP Basic protocol:
Step #1
- Check if HTTP "Authorization" header exists, if not go to Step #2.
- If exists, we get HTTP "Authorization" header - it is the user input in the modal window. It's a string having syntax like "basic abrakadabrasdfsdfsdabrakadabra=="
- Cut the word "basic", and decode the substring, encoded base64. The result is the string username:password
- Compare with the values, stored in the database. If everything is fine, return the page. If not, go to step 2.
In mini project, attached, I built the database similar to that described in the article on aspnetmania.com "Creating authorization system, based on roles in Asp.Net application." (slight differences). So, the example will be full, user will have roles, page will have roles also.
Step #2
- Return HTTP state code 401 (Unauthorized), and set the header, having the following syntax:
WWW-Authenticate: BASIC realm="some-name" - this response makes browser show our modal window, asking user to fill username and password for some-name, and try to connect again with username and password, gathered in a single string, encoded base64 (it is encoded automatically here).
- base64 logic is very famous, so it is obvious, that security is not the reason for coding the header. Probably it is coded in order to avoid transparency.
Implementation of HTTP Basic Authentication, based on roles in Asp.Net:
I had experience in performing this in pure ASP - another way along with ISAPI.
Decoding from base64 string looks as follows:
'*** Decoding from base64 - VBScript Function Base64Decode(ByVal base64String) Const Base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789+/" Dim dataLength, sOut, groupBegin 'remove white spaces, If any base64String = Replace(base64String, vbCrLf, "") base64String = Replace(base64String, vbTab, "") base64String = Replace(base64String, " ", "") 'The source must consists from groups with Len of 4 chars dataLength = Len(base64String) If dataLength Mod 4 <> 0 Then Err.Raise 1, "Base64Decode", "Bad Base64 string." Exit Function End If ' Now decode each group: For groupBegin = 1 To dataLength Step 4 Dim numDataBytes Dim CharCounter Dim thisChar Dim thisData Dim nGroup Dim pOut ' Each data group encodes up To 3 actual bytes. numDataBytes = 3 nGroup = 0 For CharCounter = 0 To 3 ' Convert each character into 6 bits of data, ' And add it to ' an integer For temporary storage. ' If a character is a '=', there ' is one fewer data byte. ' (There can only be a maximum of 2 '=' In ' the whole string.) thisChar = Mid(base64String, groupBegin + CharCounter, 1) If thisChar = "=" Then numDataBytes = numDataBytes - 1 thisData = 0 Else thisData = InStr(Base64, thisChar) - 1 End If If thisData = -1 Then Err.Raise 2, "Base64Decode", "Bad character In Base64 string." Exit Function End If nGroup = 64 * nGroup + thisData Next 'Hex splits the long To 6 groups with 4 bits nGroup = Hex(nGroup) 'Add leading zeros nGroup = String(6 - Len(nGroup), "0") & nGroup 'Convert the 3 byte hex integer (6 chars) To 3 characters pOut = Chr(CByte("&H" & Mid(nGroup, 1, 2))) + _ Chr(CByte("&H" & Mid(nGroup, 3, 2))) + _ Chr(CByte("&H" & Mid(nGroup, 5, 2))) 'add numDataBytes characters To out string sOut = sOut & Left(pOut, numDataBytes) Next Base64Decode = sOut End Function
Fortunately, in .Net we can perform the same in 2 lines of code:
byte[] tempConverted = Convert.FromBase64String(strEncoded); string userInfo = new ASCIIEncoding().GetString(tempConverted);
Also, in good old ASP we had to perform complex manipulation of requests and responses (see "basic" authentication logic).
In order to implement HTTP "basic" authentication logic we need some component, that participates in the processing pipeline of all requests for a given ASP.NET application.
In ASP.NET we have a special tool for that kind of purpose - HttpModule.
our HttpModule
Let’s write our own HttpModule, in which we will implement "basic" authentication logic. In order for our class to be referenced as HttpModule, it is enough to implement System.Web.IHttpModule interface inside it, having 2 methods: OnAuthenticateRequest, OnEndRequest, that define Step #1 and Step #2 of Basic authentication logic accordingly.
Ok, let’s see what we have:
/* ############################################################### # # Components/AuthBasic.cs # # ############################################################### */ using System; using System.Collections; using System.Configuration; using System.Security.Principal; using System.Text; using System.Web; namespace HTTPAuth.Components { // Implementation of IHttpModule interface // class AuthBasic public class AuthBasic : IHttpModule { public AuthBasic() { } public void Dispose() { } public void Init(HttpApplication application) { application.AuthenticateRequest += new EventHandler(this.OnAuthenticateRequest); application.EndRequest += new EventHandler(this.OnEndRequest); } // First method OnAuthenticateRequest, // that implements authentication logic Step #1 /* ###################################################### # # OnAuthenticateRequest # # ###################################################### */ public void OnAuthenticateRequest(object source, EventArgs eventArgs) { HttpApplication app = (HttpApplication) source; // then follow authentication logic // get Http header "Authorization" // check if not empty string authorization = app.Request.Headers["Authorization"]; if ((authorization == null) || (authorization.Length == 0)) { AccessDenied(app); return; } // check if Http header has the syntax of basic authorization = authorization.Trim(); if (authorization.IndexOf("Basic", 0) != 0) { AccessDenied(app); return; } // cut the word "basic" and decode from base64 // get "username:password" byte[] tempConverted = Convert.FromBase64String(authorization.Substring(6)); string userInfo = new ASCIIEncoding().GetString(tempConverted); // get "username" // get "password" string[] usernamePassword = userInfo.Split(new char[] {':'}); string username = usernamePassword[0]; string password = usernamePassword[1]; // compare username, password against // the values, stored in the database // if everything is fine, // get user group list from the database // and create an instance of GenericPrincipal string[] groups; if (AuthenticateAgent(app, username, password, out groups)) { app.Context.User = new GenericPrincipal( new GenericIdentity (username, "HTTPAuth.Components.AuthBasic"), groups); } // else, AccessDenied else { AccessDenied(app); return; } } // Second method OnEndRequest, // that implements authentication logic Step #2 /* ##################################################### # # OnEndRequest # # ##################################################### */ public void OnEndRequest(object source, EventArgs eventArgs) { HttpApplication app = (HttpApplication) source; if (app.Response.StatusCode == 401) { // Show modal window, // realm string is saved in web.config string realm = String.Format("Basic Realm=\"{0}\"", ConfigurationSettings.AppSettings ["HTTPAuth.Components.AuthBasic_Realm"]); app.Response.AppendHeader("WWW-Authenticate", realm); } } // Restrict access - Unauthorized /* ###################################################### # # AccessDenied # 401 - Access Denied # ###################################################### */ private void AccessDenied(HttpApplication app) { app.Response.StatusCode = 401; app.Response.StatusDescription = "Access Denied"; // write to browser app.Response.Write("401 Access Denied"); app.CompleteRequest(); } // the following method implements user authentication // against database, based on roles // if everything is fine, returns true and user group list, // that we need in order to create an instance of // GenericPrincipal /* ###################################################### # # AuthenticateAgent # # Authenticates Agent, returns true/false # app in ; HttpApplication # User in ; username # Password in ; password # groups out; agent groups to create GenericPrincipal # ###################################################### */ protected virtual bool AuthenticateAgent(HttpApplication app, string username, string password, out string[] groups) { groups = null; int lagentID = 0; string lpageURL = ""; // instance of a class, that works with databse // source code attached SqlDataProvider dataProvider = new SqlDataProvider(); // check if the specified agent exists // get agent if exists lagentID = dataProvider.getAgentByUsernamePassword(username, password); if (lagentID == 0) return false; // check if the specified user has groups // get agent groups ArrayList arrAgentsGroups = new ArrayList(); arrAgentsGroups = dataProvider.getGroupsByAgentID(lagentID); if (arrAgentsGroups.Count == 0) return false; // check if requested page has groups // get pages groups lpageURL = app.Request.Path; ArrayList arrPagesGroups = new ArrayList(); arrPagesGroups = dataProvider.getGroupsByPageURL(lpageURL); if (arrPagesGroups.Count == 0) return false; // check if at least one agent group // is in Page Groups List // if yes, return true string[] pagegroups = (string[])arrPagesGroups.ToArray(typeof(string)); groups = (string[])arrAgentsGroups.ToArray(typeof(string)); foreach (string groupagentID in groups) { foreach (string grouppageID in pagegroups) { if (groupagentID == grouppageID) return true; } } return false; } } }
That’s it. HttpModule is ready. Let’s create a configuration repository of it. In Web.config under system.web:
< httpModules > < add name="BasicAuthenticationModule" type="HTTPAuth.Components.AuthBasic, HTTPAuth" / >
Syntax:
< httpModules > < add name="ModuleName" type="MyNameSpace, MyAssembly" / >
Cancelling built-in authentication
< authentication mode="None" / > < authorization > < deny users="?" / >
Saving realm string under appSettings key
< appSettings > < add key="HTTPAuth.Components.AuthBasic_Realm" value="Protected System" / >
That’s all.
Class that works with the database and database scripts are attached inside the source code.
Materials and credits: rfc2617, Marty Hall, Greg Reinacker, aspnetmania.com
Saturday, November 29, 2003 5:40 AM