Introduction
Every developer knows that code reuse is good, right? I know that
I've always tried to write modular code that could be reused all
over the place in my applications. Well, maybe not really all the
time. Why? There are many reasons that code has always been hard to
build in a reusable fashion. Usually I find that code is closely
knit to the applications, unless, of course, we are talking about
basic functionality that is shared across applications. Examples of
this in my past corporate development experience have been items
such as:
- Sending E-mail from within applications
- Performing common backend tasks
- Application logging and Event writing
- Application security (Logging in/out)
The last item mentioned above is one that I find myself
implementing over and over within my current work environment, and
in general with any web application that I write. In the past,
encapsulating and reusing the code needed to log into web
applications and secure them was quite a bit harder than it is now
with the advent of .NET.
With .NET I can create a Web UserControl to encapsulate the login portion
of the logic. This coupled with use of .NET's web.config file
to implement a broad Forms Authentication scheme across my site
allows me to easily add Login/Logout cookie based Forms
Authentication to any web app.
In this article I will present my Login UserControl and additional information to
make it much easier for you to secure your ASP.NET applications
too!
System Requirements
I'm assuming that you have a basic knowledge of ASP.NET
development as well as basic knowledge of SQL Server 2000 and
general authentication topics. The release version of .NET and SQL
Server 2000 (any edition) are required to get the full benefit out
of the sample code presented.
Defining The Problem
Every time I create a new web application for my employer, I find
myself trying to reinvent the wheel with regards to controlling user
access and authorization. Although ASP.NET and IIS allow some basic
Windows Integrated Authentication schemes that can keep out unwanted
users, I typically like to control user access based on a SQL Server
database. It's common in my workplace to control our own user
database, and that's generally what is done.
I find it quite boring to retype the same code over and over.
Don't you wish that there was a way in .NET that you could create
some sort of "control" that you could place on a web page that
encapsulated functionality? You know, kind of like WindowsDNA
ActiveX controls? Wait a minute, there is such functionality...
What's a UserControl?
ASP.NET introduces a technology called Web User Controls.
An ASP.NET user control is a group of one or more server controls or
static HTML elements that encapsulate a piece of functionality. A
user control could simply be an extension of the functionality of an
existing server control. Or it could consist of several elements
that work together to perform a task. It's easiest to relate this
back to the days of ActiveX controls, where you could create a
control that extended the functionality of a certain control, or
grouped controls together.
That being said, how could I use a user control to aid in my
quest for creating a reusable login construct? Let's start with a
vision of what I'd like to create and then go from there.
My Ideal Login Form
Ideally I'd like to create a login form that could be inserted
anywhere on a site where user validation and authentication is
required. It'd also have to be pretty, because I hate ugly web sites
: )
I played around with my favorite HTML editor (notepad) and came
up with this construct. Let's pretend that we all work for Wrox, and
are creating this for their sites:
Isn't that wonderful? The next question is how do we turn that
into a UserControl that can be dropped
into web pages and reused in different company applications?
Creating the Login UserControl
First off, we need to open up VS.NET and create a new ASP.NET web
project. I started VS.NET and choose to create a new C# ASP.NET
application. This can be done in VB.NET as well, but my company
standardized on C# for .NET development, and I'm presenting it that
way.
Once the project creation is complete, we'll add a new Web User
Control to the project by right clicking on the Project in the
Project Explorer and selecting ADD | ADD Web
User Control.
We'll call our UserControl LoginUC.ascx.
What goes in there?
Ok, so now we basically have a shell project with a blank user
control added to it. At this point, I renamed the default
WebForm1.aspx page to Login.aspx page. You don't have
to do that, but in anticipation for what we are going to talk about
later, it's a good idea.
Next, we simply need to insert our code into the user control.
There are 2 parts to this. The first is the HTML code and the
ASP.NET tags that go in the user control, and then there is the code
that must go in the codebehind file to perform the authentication
checks.
HTML
When you click on the user control in the Project Explorer, you
are taken to a blank screen (design mode) for the user control. To
insert HTML into the control, we need to first click on the HTML tab
of the IDE window.
Now we can insert our HTML code into the editor window to create
the HTML table structure for our UserControl.
Notice that the first line of the file should already have been
populated by the VS.NET IDE <%@ Control Language="c#" AutoEventWireup="false"
Codebehind="LoginUC.ascx.cs" Inherits="LoginUserControl.LoginUC"
TargetSchema="http://schemas.microsoft.com/intellisense/ie5"%>
This code is needed by .NET to tell the Framework where the
codebehind is for the control.
Next I'll simply drop in the nice HTML code that I wrote in
notepad. I also created a graphic to insert into the table structure
(which I added to the project). <table borderColor="gainsboro" cellSpacing="0" cellPadding="0" width="205"
border="1">
<tr>
<td align="left" height="25"><font face="Arial" color="black"><b><IMG
src="wrox_logo.gif"></b></font></td>
</tr>
<tr>
<td align="middle" height="25">
<table width="100%" bgcolor="Gainsboro">
<tr>
<td><font face="Arial" size="-1">UserName:</font></td>
<td><b><asp:textbox id="UserName" runat="server"
size="14"></asp:textbox></b></td>
<td><asp:label id="UserMark" style="FONT: 12pt verdana, arial;
COLOR: red" runat="server" Visible="false" Text="*"></asp:label></td>
</tr>
<tr>
<td><font face="Arial" size="-1">Password:</font></td>
<td><input id="Password" type="password" size="14" name="Password"
runat="server"></td>
<td><asp:label id="PasswordMark" style="FONT: 12pt verdana, arial;
COLOR: red" runat="server" Visible="false" Text="*"></asp:label></td>
</tr>
<tr>
<td></td>
<td><input id="Submit1" type="submit" value=" Sign In "
name="Submit1" runat="server" onServerClick="Submit1_ServerClick"></td>
</tr>
<tr>
<td align="middle" colSpan="3"><asp:label id="Message" style="FONT:
8pt verdana, arial; COLOR: red" runat="server" Visible="false"></asp:label></td>
</tr>
</table>
</td>
</tr>
</table>
As you can see, the HTML is fairly straightforward. Note that we
are using ASP.NET server side web controls such as label, textbox,
and the password input for the fields. We are also hooking up our
submit button to the Submit1_ServerClick event that will be
defined in the codebehind class.
Clicking back on the design tab should give you output like
this:
Now that we've sorted out the look of the control, we must
implement the code that checks our username and password against the
SQL Server 2000 database.
For simplicities sake, I created just one table in the DB that
contains 2 fields: the username and password. I called the database
"LoginTest " I'll assume that in your applications you'll
create this however you want, and probably take the time to do some
encryption with the passwords. But here we'll just create the schema
and table to look like the following (the code download contains a
sql script called database.sql that will recreate this for
you, you do however need to create the table called
"LoginTest " before running the script to generate the
table).
Above you see a database called LoginTest and a table
called users.
Ok, so we have our database set up. Now let's complete the
codebehind file.
Let's declare our namespace and insert the required using statements. namespace LoginUserControl
{
using System;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Data.SqlClient;
using System.Web.Security;
using System.Configuration;
Actually, by default most of the using statements were there. The 3 that I
added were the System.Data.SqlClient,
System.Web.Security and System.Configuration namespaces. Why did I
add these? Well, SqlClient should be
self-explanatory, the Web.Security
namespace was added for our Forms Authentication use later on in the
article, and the Configuration
namespace was added so that I could access the Web.Config
file.
Now I need to add some more code to flush out the structure of
the class (all of this is here by default). Notice that I added two
public variables for the redirect page and a flag for the warning
message on the user control itself. /// <summary>
/// Summary description for LoginUC (Login UserControl).
/// </summary>
public abstract class LoginUC : System.Web.UI.UserControl
{
protected System.Web.UI.WebControls.TextBox UserName;
protected System.Web.UI.WebControls.Label UserMark;
protected System.Web.UI.WebControls.Label PasswordMark;
protected System.Web.UI.WebControls.Label Message;
protected System.Web.UI.HtmlControls.HtmlInputText Password;
protected System.Web.UI.HtmlControls.HtmlInputButton Submit1;
//needed variables for both redirection and an int flag
//read redirect page from web.config (more on that later)
public String RedirectPage
= ConfigurationSettings.AppSettings["redirectpage"];
public int chk;
private void Page_Load(object sender, System.EventArgs e)
{
// nothing to do here
}
Remember back in the HTML when I called a function called Submit1_ServerClick ? Well let's code that
right now.
What we want to do in this function is twofold. First we want to
validate the username and password. If they are correct, then we'll
redirect to whatever page is specified in our control. If the
authentication fails, we want to see if it was a bad password, or
invalid username. Check the comments in the code below to see what's
going on. For now, ignore the FormsAuthentication code, we'll discuss that
in a bit. public void Submit1_ServerClick(object sender, System.EventArgs e)
{
//call another method that authenticates
if (Authenticate(UserName.Text, Password.Value))
{
//redirect the page
Response.Redirect (RedirectPage);
//forget the code below for now, we'll talk about it later
//FormsAuthentication.RedirectFromLoginPage(UserName.Text,false
//);
}
else
{
//ok, an error. What error was it? Handle accordingly.
if (chk==1)
{
Message.Text="The password is invalid";
UserMark.Visible = false;
PasswordMark.Visible = true;
Message.Visible = true;
}
else
{
Message.Text="The username is invalid";
PasswordMark.Visible = false;
UserMark.Visible = true;
Message.Visible = true;
}
}
}
Now let's write the Authenticate( )
function called from the above code. Before we get to it, we should
first quickly go over some custom settings that I added to the
web.config file for the web application. These simply help me
avoid hardcoding the DSN and the Redirect Page that I showed
earlier. <appSettings>
<add key="dsn" value=
"server=USZHOIT28971\\ANDYDEV;uid=sa;pwd=;database=LoginTest" />
<add key="redirecturl" value="main.aspx" />
</appSettings>
Anyway, now onto the Authenticate
code. Read the comments below for direction on what the code is
doing. bool Authenticate(String user, String pass)
{
//set up a fall through variable for determining
//authentication
bool authenticated = false;
try
{
//read the dsn from the Web.Config file
String dsn = ConfigurationSettings.AppSettings["dsn"];
//create our SQL select statement
String sSQL = "SELECT username,password FROM users where
username='" + user.Trim() + "'";
//create a new connection with the DSN
System.Data.SqlClient.SqlConnection Conn = new
System.Data.SqlClient.SqlConnection(dsn);
//now create a sql command with the connection and SQL
System.Data.SqlClient.SqlCommand Cmd = new
System.Data.SqlClient.SqlCommand(sSQL,Conn);
//create a new SqlDataReader object
System.Data.SqlClient.SqlDataReader Read1 = null;
//open the connection and execute the reader
Conn.Open();
Read1 = Cmd.ExecuteReader();
//check results and determine what was wrong if anything
//set the chk flag (invalid username or invalid password)
if (Read1!=null)
{
if (Read1.Read())
{
if (Read1.GetString(0)==user)
{
if(Read1.GetString(1)==pass)
{
authenticated =true;
}
else
{
chk=1;
}
}
else
{
chk=2;
}
}
}
}
catch(Exception e)
{
Response.Write("Exception: " + e.ToString());
}
return authenticated;
}
}
}
We've finished the code for our control!
Testing the UserControl
Now that we've written the code for the Login UserControl, we need some way to test it.
This is quite simple. Remember the default .aspx page
that was created when we started our project? The one that I renamed
to Login.aspx ? To test the user
control, we can simply put an instance of the user control on the
Login.aspx page, set the start page to
Login.aspx and hit run (F5).
You can add the user control, by clicking and dragging the LoginUC.ascx item in the Project Explorer
over to the Login.aspx page
designer.
Starting up the project should bring up the following browser
window:
Provided that you set up your SQL Server database correctly, you
should be able to enter some information in the username and
password fields and test out the user control.
For example, entering a wrong password will give you this:
As well as entering the wrong username:
Wow, that's fine and dandy isn't it? Currently when we enter both
the correct username and password, the Login UserControl will redirect the ASP.NET page
to the page specified in the web.config redirecturl AppSetting. Which in our case would be
Main.aspx (a blank page I added to the project).
What about Security?
So, now we have a user control that validates user data against a
SQL Server 2000 database, but it really doesn't secure the site very
well. If we are going to use this in an ASP.NET web site, we need
some way to deny access to certain pages. How do we do that?
Well, luckily for us there is an easy way to do this using Forms
Authentication in .NET. As you probably already know, the
web.config file allows you to control many aspects of how
your ASP.NET web site works. One section that you can control is the
Authentication section. This section controls the authentication
settings for the application. You can set up possible authentication
schemes for "Windows", "Forms", "Passport" and "None". We're going to use Forms Authentication in ours.
I'm not going to delve deeply into the
Authentication/Authorization sections, other than to describe the
code that we are adding for this application. If you want to see all
of the available options for these sections of the web.config
file, I suggest that you take a look at the Microsoft documentation
at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vsent7/html/vxconASPNETAuthentication.asp&WROXEMPTOKEN=64823Zg6Qt4ZwJFo47xTvh87ND.
What do we want?
You can use the Login UserControl
for anything that needs to validate login information. But in the
instance where we want to lock down the site pages from users that
aren't logged in, we'd want something like the following.
Basically, to hit any of the pages in the site, we want the user
to first go through the login screen. If the user has logged in
successfully, then we would want to allow him to freely browse the
pages in our site without having to re-authenticate on each page.
How could we do that?
Well, the first thing we must do is set up the authentication and authorization sections in the web.config
file.
Let's set the authentication mode to Forms, and protect all the pages, setting
the loginURL to hit our
login.aspx page that contains the Login UserControl. The name portion sets a unique
extension for the cookie that will be written to the user's browser
upon proper forms authentication. Also note that the timeout value
has been set to 10 minutes. The authentication in ASP.NET takes care
of the timeout value, forcing another login after the timeout period
has expired. <authentication mode="Forms">
<forms name=".LOGINUSERCONTROLDEMO" loginUrl="login.aspx"
protection="All" timeout="10" path="/"></forms>
</authentication>
Let's specify that we deny all users: <authorization>
<deny users="?" />
</authorization>
Next we need to make a few changes to the code we described
earlier. We need to use the FormsAuthentication.RedirectFromLoginPage
instead of the Response.Redirect that
we had. This will allow our application to write a cookie out and
redirect back to the page that was initially being requested before
getting pushed over to the login form for authentication. //call another method that authenticates
if (Authenticate(UserName.Text, Password.Value))
{
//redirect the page
//Response.Redirect(RedirectPage);
//stick username in the cookie, and set the persitent value
//to false for our testing
FormsAuthentication.RedirectFromLoginPage(UserName.Text,false);
}
Main.aspx
Here's the code to a page called Main.aspx. It contains a
simple little message, and a button to "log out" of the forms
authentication mode and kick back to the login page.
HTML<%@ Page language="c#" Codebehind="main.aspx.cs" AutoEventWireup="false"
Inherits="LoginUserControl.main" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
<HEAD>
<title>main</title>
<meta name="GENERATOR" Content="Microsoft Visual Studio 7.0">
<meta name="CODE_LANGUAGE" Content="C#">
<meta name="vs_defaultClientScript" content="JavaScript">
<meta name="vs_targetSchema"
content="http://schemas.microsoft.com/intellisense/ie5">
</HEAD>
<body MS_POSITIONING="GridLayout">
<form id="main" method="post" runat="server">
This the main page.
<asp:Button id="logout" style="Z-INDEX: 101; LEFT: 7px;
POSITION: absolute; TOP: 64px" runat="server" Text="Logout"
Width="78px" Height="25px"></asp:Button>
</form>
</body>
</HTML>
CodeBehind
Notice that the logout_Click event
forces the logout and redirect to the login page. using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Web.Security;
namespace LoginUserControl
{
/// <summary>
/// Summary description for main.
/// </summary>
public class main : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Button logout;
private void Page_Load(object sender, System.EventArgs e)
{
// Put user code to initialize the page here
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web
//Form Designer.
//
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.logout.Click += new
System.EventHandler(this.logout_Click);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
private void logout_Click(object sender, System.EventArgs e)
{
FormsAuthentication.SignOut();
Response.Redirect("login.aspx");
}
}
}
We see that the main.aspx page looks like the
following.
Wrapping it Up
As you can see, using the login user control coupled with Forms
Authentication allows you to easily add login capabilities to an
ASP.NET site. Although the control isn't as complex as some other
Web UserControls out there, it really
helps out in keeping interdepartmental applications in sync with
their login methodologies. Which is the whole reason for code reuse
in our work environment!
Application Setup
Setting up the sample application is quite simple. These
directions are also included in the Readme.txt file of the
download.
Installation Instructions
1) The easiest way to get the sample code up and running is to
create a directory under wwwroot called
LoginUserControl
2) Use the MMC IIS Snap in and right click on the folder - go to
Properties
3) Select the Create button on the
Application tab of the property
window
4) If desired, open the LoginUserControl.sln file
DB
Using SQL Server Enterprise Manager, create a database called
LoginTest, run the sql script called database.sql
distributed with the sample code.
Configuration
Edit the web.config file to include your database
connection information. See the <appSettings> section.
Hit the location of the site, for example, http://localhost/loginusercontrol/Login.aspx?WROXEMPTOKEN=64823Zg6Qt4ZwJFo47xTvh87ND
Conclusion
In this article we presented a few topics related to login
authentication and creating a login UserControl for accessing a web site. Data
was stored in SQL Server 2000 and although the architecture was
quite simple, there are many ways this could be expanded to fit
within your ASP.NET applications and hopefully you'll find it as
useful as I have for creating consistent login screens across your
apps. |