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