Friday, October 2, 2009

Using MD5 Encryption with C# and Microsoft SQL Server


Bookmark and Share



Background



The .NET Framework provides developers with some easy to use classes for modern encryption. One of the more popular methods these days is the use of MD5 encryption. MD5 encryption, to quote from RFC 1232, “takes as input a message of arbitrary length and produces as output a 128-bit “fingerprint” or “message digest” of the input. It is conjectured that it is computationally infeasible to produce two messages having the same message digest, or to produce any message having a given prespecified target message digest. The MD5 algorithm is intended for digital signature applications, where a large file must be “compressed” in a secure manner before being encrypted with a private (secret) key under a public-key cryptosystem such as RSA.” It was developed by Professor Ronald L. Rivest of MIT, and has become widely used as a standard encryption method for ASP.NET applications. See the Points of Interest at the bottom of this article for more practical information about MD5 usage.

Getting Started

Since .NET has made MD5 encryption so easy to use, I’m not including a demo project. I’ll just include the required C# methods, and a SQL script for creating a test database table.

Creating a Test Table

Using your local instance of MSSQL 2000 (this will probably work on 2005 as well, but I’ve not tested it as yet). You can rename the table and the columns at your leisure, just ensure you change the calls to the methods that I’ll detail below. For now, just run the following SQL script from a database you own:

if exists (select * from dbo.sysobjects where
id = object_id(N'[dbo].[tblLogins]')
and OBJECTPROPERTY(id, N'IsUserTable') = 1)
drop table [dbo].[tblLogins]
GO

CREATE TABLE [dbo].[tblLogins] (
[Login] [varchar] (25) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ,
[Password] [binary] (16) NULL
) ON [PRIMARY]
GO

All we’re doing here is creating a table called ‘tblLogins’, with two columns: a login column (varchar 25) and a password column (binary 16).

Adding a New Login

You can use the following method in your ASP.NET Web Form or C# Windows Form application. All we’re doing here is making a connection to the database, and inserting a new login. I created an enumeration called ValidationCode so that I can handle responses from the method a little more clearly, but you can just as well just a void function to the same effect.

/* Return types that are thrown when login is attempted */
public enum ValidationCode
{
LoginFailed=1,
LoginSucceeded=2,
ConnectionFailed=3,
UnspecifiedFailure=4,
LoginCreated=5
}

You will need to ensure that you have the following references added to your application:

using System.Data.SqlClient;
using System.Security.Cryptography;
using System.Text;

CreateNewLogin will accept 5 values. The first value is the name of the table in the database housing the logins (in this example, it’s tblLogin), the second and third values will be our desired login and password, in plain text, and the last two values are the names of the login column (Login) and the password column (Password) in our table, respectively.

public ValidationCode CreateNewLogin(string tableName, string strLogin,
string strPassword, string loginColumn, string passColumn)
{
//Create a connection
string strConnString = System.Configuration.ConfigurationSettings.
AppSettings["ConnString"];
SqlConnection objConn = new SqlConnection(strConnString);

// Create a command object for the query
string strSQL = "INSERT INTO " + tableName + " (" + loginColumn +
"," + passColumn + ") " + "VALUES(@Username, @Password)";

SqlCommand objCmd = new SqlCommand(strSQL, objConn);

//Create parameters
SqlParameter paramUsername;
paramUsername = new SqlParameter("@Username", SqlDbType.VarChar, 10);
paramUsername.Value = strLogin;
objCmd.Parameters.Add(paramUsername);

//Encrypt the password
MD5CryptoServiceProvider md5Hasher = new MD5CryptoServiceProvider();
byte[] hashedBytes;
UTF8Encoding encoder = new UTF8Encoding();
hashedBytes = md5Hasher.ComputeHash(encoder.GetBytes(strPassword));
SqlParameter paramPwd;
paramPwd = new SqlParameter("@Password", SqlDbType.Binary, 16);
paramPwd.Value = hashedBytes;
objCmd.Parameters.Add(paramPwd);

//Insert the record into the database
try
{
objConn.Open();
objCmd.ExecuteNonQuery();
return ValidationCode.LoginCreated;
}
catch
{
return ValidationCode.ConnectionFailed;
}
finally
{
objConn.Close();
}
}

You can test this method out by executing the function. If you attempt to select the information from the database directly, you will notice that the encryption has worked. Now, on to validation.

Validating a Login

This method will allow you to validate a login against a pre-existing login in the database. It’s important to note that we never actually return data to the requestor for evaluation. All of our evaluation is done server side; we only return row counts to the function, making it that much more secure.

//Returns a validation code based on the control's set login info
public ValidationCode ValidateLogin(string tableName, string strLogin,
string strPassword, string loginColumn, string passColumn)
{
try
{
string strConnString = this.ConnectionString;
SqlConnection objConn = new SqlConnection(strConnString);
string strSQL = "SELECT COUNT(*) FROM " + tableName +
" WHERE " + loginColumn + "=@Username AND " + passColumn +
"=@Password;";
SqlCommand objCmd = new SqlCommand(strSQL, objConn);
//Create the parameters
SqlParameter paramUsername;
paramUsername = new SqlParameter("@Username", SqlDbType.VarChar, 25);
paramUsername.Value = strLogin;
objCmd.Parameters.Add(paramUsername);

//Hash the password
MD5CryptoServiceProvider md5Hasher = new MD5CryptoServiceProvider();
byte[] hashedDataBytes;
UTF8Encoding encoder = new UTF8Encoding();
hashedDataBytes =
md5Hasher.ComputeHash(encoder.GetBytes(strPassword));

//Execute the parameterized query
SqlParameter paramPwd;
paramPwd = new SqlParameter("@Password", SqlDbType.Binary, 16);
paramPwd.Value = hashedDataBytes;
objCmd.Parameters.Add(paramPwd);
//The results of the count will be held here
int iResults;
try
{
objConn.Open();
//We use execute scalar, since we only need one cell
iResults = Convert.ToInt32(objCmd.ExecuteScalar().ToString());
}
catch
//Connection failure (most likely, though
//you can handle this exception however)
{
return ValidationCode.ConnectionFailed;
}
finally
{
objConn.Close();
}

if (iResults == 1)
return ValidationCode.LoginSucceeded;
else
return ValidationCode.LoginFailed;
}
catch
{
return ValidationCode.UnspecifiedFailure;
}
}

You probably noticed that both methods have the same signature. It would be easy to combine both into a single function, but for this example, I’m keeping them separate. But that’s all there is to it. You can now create a click event on your page or form, and call either of the functions, handling the return code appropriately. Again, you don’t have to use the return codes; you can easily just handle the exceptions or the counts.

Note: I think you already know that to make this code work you need to add these two below namespaces:

System.Security.Cryptography;

System.Text;




Bookmark and Share

Tuesday, September 29, 2009

How to Mix Forms and Windows Authentication in ASP.NET


Bookmark and Share



The situation: You have a Website that is available to both Intranet users and Internet users. You want users from the Intranet to be authenticated automatically using their domain credentials and Internet users to be authenticated using basic ASP.NET forms security.

The problem: When you configure IIS to allow both Anonymous Access and Integrated Windows Authentication, you cannot get the remote users NTLM name.

The fix: Configure IIS to use Anonymous Access for the entire site, with the exception of a single page. This page will be set to use Integrated Windows Authentication.

The gotcha is that, when you check "Anonymous Access" in IIS, you cannot get the user's NTLM name (even if you check "Integrated Windows Authentication"). The key is to specify "Integrated Windows Authentication" for exactly one page in your application. When Intranet users hit this page, we can get their NTLM name out of Request.ServerVariables["LOGON_USER"] and programmatically store that in the FormsAuthentication context. External Internet users can even hit this page and manually specify their domain credentials if they like, but these users will normally go through the standard login process.

STEP 1 - Create a new virtual directory in IIS. In the application root, only check "Anonymous Access". Do not check "Integrated Windows Authentication". This should look like:



STEP 2 - In your Web application, create a new form named "WinLogin.aspx" under the app's root. Also add a page named "Login.aspx" under the app's root and put an instance of the standard System.Web.UI.WebControls.Login control on it, e.g.:

STEP 3 - In IIS manager, select WinLogin.aspx and navigate to the "File Security" tab. Here disable "Anonymous Access" but enable "Integrated Windows Authentication" (this is crucial). This should look like:


STEP 4 - In your web application's web.config, add the following:




"Login.aspx">


"?,*" />






"WinLogin.aspx">


"?,*" />






"Forms">
"Login.aspx"/>





"?" />
"*" />




"CustomMembershipProvider">


"CustomMembershipProvider" type="DualAuthenticationExample.CustomMembershipProvider, DualAuthenticationExample" />






STEP 5 - In ~/Login.aspx add the following code:

namespace DualAuthenticationExample

{
public partial class LoginPage : System.Web.UI.Page
{
private bool IsIntranetRequest(string ip)
{
// TODO: Check for whatever interanet ip addresses, ranges, etc here...

return !string.IsNullOrEmpty(ip) && Regex.IsMatch(ip, "^127");
}

protected void Page_Load(object sender, EventArgs e)
{
if (IsIntranetRequest(Request.ServerVariables["REMOTE_ADDR"]))
{
Response.Redirect("~/WinLogin.aspx");
}
}
}
}

STEP 6 - In ~/WinLogin.aspx add the following code:

namespace DualAuthenticationExample

{
public partial class WinLoginPage : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string user = Request.ServerVariables["LOGON_USER"];

if (string.IsNullOrEmpty(user))
{
Response.Redirect("~/Login.aspx");
}
else
{
FormsAuthentication.SetAuthCookie(user, false);
Response.Redirect("~/Default.aspx");
}
}
}
}
So, as specified in the web.config, all users will be bounced to the Login.aspx page. If the user is on the intranet, they'll get bounced again to the WinLogin.aspx page, which will attempt to authenticate them with their NTLM name. If that is successful, then we programmatically log them into the FormsAuthentication context. From here, the user's name will be availble via the standard Page.User.Identity.Name property (as well as HttpContext.Current.User.Identity.Name)

If you know the IP range(s) for the Intranet requests you can really streamline what I have here and make it seamless to the user. This is technically not a requirement to get this to work but, without it, external users will be prompted with the authentication box (I believe the behavior for this varies between browsers and also depends on the user's current browser settings).





Bookmark and Share