Click here to Skip to main content
15,878,809 members
Articles / Programming Languages / C#

Secure Password Authentication Explained Simply

Rate me:
Please Sign up or sign in to vote.
4.84/5 (34 votes)
2 Apr 2014CPOL9 min read 139.2K   1.6K   85   31
Simple code and explanations to enable secure password authentication.

Introduction

It's unbelievable to me that I am still running into websites that store passwords themselves, and you see rules like "password must be no more than 16 characters" or "passwords must not include '<> *& etc.." Just last night, I ran into a government website that had a maximum password length of 12 characters, and while they allowed certain special characters, others were disallowed. These kinds of rules sound like the site is storing the password in plain text, or worse, not using parameters for the storage and validation of the passwords from a database.

After so many years of passwords, I would have assumed most programmers would have figured these things out. In many cases, storing passwords in your own application or website is pointless; there are many alternatives.

  1. Integration with your network security, such as ActiveDirectory
  2. OpenID

This article is for those who must store usernames and passwords themselves. I am going to use C#, MS Access, and ASP.NET for this example; though the premise would obviously be able to be used with any application stack you like.

Background

Other articles have been written covering this information, but I want to keep this article very straightforward. I am not going to go into the depths of cryptography, nor am I going to cover keeping people signed in, or validating minimum complexity, or anything of that nature. My intention is to keep the information and code to only what is required to store passwords securely. For the purposes of this article, my goals are very simple.

  1. Allow passwords of unlimited length, and of any characters the user likes.
  2. Be able to store the password in a database efficiently.
  3. Prevent any SQL injection attacks.
  4. Prevent data movement attacks.

So, let's start with the first 2 items. I don't believe that passwords should ever need to be "read" or "recovered". The only exception to this would be if we need to keep passwords to other applications, and even then, only if you have no other choice, such as converting all the applications in question to OpenID. Should a password be lost or compromised, there should be a "reset", this philosophy means that I do not need to ever store passwords in plain text or a decryptable form, at least not for authentication. Thus, the simplest way to solve these two problems is by using a one way hash, that is an encryption that can't be decrypted. Hashing algorithms like MD5, SHA-1, and others can be used to create a fixed length series of values that is more of a unique signature for data rather than encrypted data.

For example, the MD5 hash of "test" is "098f6bcd4621d373cade4e832627b4f6". The nice thing about these signatures is that no matter how big the data is, the size of the data returned is always the same. This is why you will see MD5 and SHA-1 signatures for validating large files like Linux DVD ISO images. Different algorithms will have different lengths of output signatures:

  • MD5 has a length of 128 bits, which is 16 bytes. Interestingly enough this is the same length as a UUID/GUID. This means that if you are storing something non-security related, you can store the data in a UUID/GUID field in your database.
  • SHA-1 has a length of 160 bits, which is 20 bytes.
  • SHA-256 has a length of 256 bits, which is 32 bytes.
  • SHA-384 has a length of 384 bits, which is 48 bytes.
  • SHA-512 has a length of 512 bits, which is 64 bytes.

Now, if we use a signature to store our password, we get all three of my requirements in one shot. First, because the signatures don't contain anything that could be used for SQL injection or that we have to worry about encoding, any character the user wants to use can be used for a password. Second, we can store passwords of any size we like in a fixed length field, such as 64 bytes.

Before going any further, we need to look at the fact that MD5 is now easily cracked. In fact, you can decrypt many MD5 signatures at websites like this one. SHA-1 is also showing signs of being weakened, and will soon be obsolete as well. So, what can be done to "shore up" the signature?

First, we use the strongest hash available to us in our code base. Obviously, we could write our own implementation of stronger ones, but it's better to use tried and true encryption code rather than make our own.

The second thing we can do is to "salt" our passwords; in short, this means creating a random value to append to the end of the password to make it more unique. This could be a short series of bytes, but we can use as much data as is reasonable. Of course, the other reason to "salt" passwords is to prevent analysis of the passwords. If all you do is hash the password, then the password of "test" is always "098f6bcd4621d373cade4e832627b4f6". This means that should the list of passwords be compromised, everyone who has the same password would have the same hash. By using a random salt value, the stored hashes become unique, even if the same password is used.

The most common question often asked about salts is "If a salt is random, how do you reliably generate the same salt every time verification is done"? The answer is simple, you don't. You store the salt separately from the password hash.

Storing the data is fairly straightforward, but because we are using an array of bytes (an OLE object in Access), pure text SQL will not work. Instead, we need to use parameters, this is good since this also helps prevent SQL injection.

Since I want this article to cover more than just rehashing the same data that was covered everywhere else, I figured I would make this example more portable to other databases. See my previous article for more details.

Thus, I will start with a static DB class that reads the configuration file and creates the proper types based on the provider, this is obviously not a great solution for applications like the web, but this article isn't about making a really good database wrapper.

C#
public class DB
  {
      private string _connectionString = null;
      private DbProviderFactory _factory = null;
      private string _quotePrefix = string.Empty;
      private string _quoteSuffix = string.Empty;

      public DbProviderFactory Factory
      {
          get
          {
              if (_factory == null)
              {
                  ConnectionStringSettings connectionSettings = ConfigurationManager.ConnectionStrings["DSN"];
                  _factory = DbProviderFactories.GetFactory(connectionSettings.ProviderName);
                  _connectionString = connectionSettings.ConnectionString;
              }
              return _factory;
          }
      }

      public string ConnectionString
      {
          get
          {
              return _connectionString;
          }
      }

      public string QuotePrefix
      {
          get
          {
              if (string.IsNullOrEmpty(_quotePrefix))
              {
                  FillQuotes();
              }
              return _quotePrefix;
          }
      }

      public string QuoteSuffix
      {
          get
          {
              if (string.IsNullOrEmpty(_quoteSuffix))
              {
                  FillQuotes();
              }
              return _quoteSuffix;
          }
      }

      //this function gets the proper characters to wrap
      //database, table, and column names.
      private void FillQuotes()
      {
          DbCommandBuilder cb = Factory.CreateCommandBuilder();
          if (!string.IsNullOrEmpty(cb.QuotePrefix))
          {
              _quoteSuffix = cb.QuoteSuffix;
              _quotePrefix = cb.QuotePrefix;
              return;
          }
          using (DbConnection conn = GetConnection())
          {
              using (DbCommand cmd = conn.CreateCommand())
              {
                  //test to see if we can wrap names in square brackets.
                  cmd.CommandText = "SELECT '1' as [default]";
                  try
                  {
                      using (DbDataReader dr = cmd.ExecuteReader())
                      {
                          while (dr.Read())
                          {
                          }
                      }
                      _quotePrefix = "[";
                      _quoteSuffix = "]";
                  }
                  catch
                  {
                      try
                      {
                          //square brackets failed, try double quotes.
                          cmd.CommandText = "SELECT '1' as \"default\"";
                          using (DbDataReader dr = cmd.ExecuteReader())
                          {
                              while (dr.Read())
                              {
                              }
                          }
                          _quotePrefix = _quoteSuffix = "\"";
                      }
                      catch
                      {
                          //no characters appear to work
                      }
                  }
              }
          }
      }

      private DbConnection GetConnection()
      {
          DbConnection conn = Factory.CreateConnection();

          conn.ConnectionString = ConnectionString;
          conn.Open();
          return conn;
      }

      public int ExecuteNonQuery(string sql, IEnumerable<DbParameter> parameters)
      {
          using (DbConnection conn = GetConnection())
          {
              DbCommand cmd = null;
              try
              {
                  cmd = conn.CreateCommand();
                  cmd.CommandText = sql;
                  foreach (DbParameter parameter in parameters)
                  {
                      cmd.Parameters.Add(parameter);
                  }
                  return cmd.ExecuteNonQuery();
              }
              finally
              {
                  if (cmd != null)
                  {
                      cmd.Parameters.Clear();
                      cmd.Dispose();
                  }
                  cmd = null;
              }
          }
      }

      public DbDataReader ExecuteReader(string sql, IEnumerable<DbParameter> parameters)
      {
          DbConnection conn = GetConnection();
          DbCommand cmd = null;
          try
          {
              cmd = conn.CreateCommand();
              cmd.CommandText = sql;
              foreach (DbParameter parameter in parameters)
              {
                  cmd.Parameters.Add(parameter);
              }
              return cmd.ExecuteReader(CommandBehavior.CloseConnection);
          }
          finally
          {
              if (cmd != null)
              {
                  cmd.Parameters.Clear();
                  cmd.Dispose();
              }
              cmd = null;
          }
      }
  }

This class is not complete; therefore, it is not good for use in production code. It lacks the ability to use multiple connection strings, and more importantly, it doesn't support transactions. Probably, the biggest shortcoming is the complete lack of error handling. It does, however, have a couple really nice features; it takes full advantage of connection pooling, and it exposes the Quote characters for the provider. I have made it non-static, since static classes are terrible for web applications.

This example only uses one table: "Users".

Users
Unique ConstraintIDUUID/GUID
Primary Keyuservarchar(255)
passwordbyte[64]

I intentionally made the column names collide with SQL keywords to show the functionality of wrapping column and table names. Therefore, you must wrap the column names correctly.

Using the code

Before we can authenticate a user, we must register them. To register a user, we have to do the following:

  • Get the username
  • Get the password
  • Generate a random salt
  • Create the password hash
  • Store the username, hash, and salt in the database

Surprisingly, this comes down to a very small block of code. You will see two odd things in the following code. I am not using any hardcoded provider types, and I am using RNGCryptoServiceProvider. The .NET Random object provides pseudo-random numbers; this would be fine, except they will repeat every time you create a new object. To solve this problem, Microsoft tells you to either use RNGCryptoServiceProvider or simply create a single static Random object that all the code in your project uses for random numbers.

C#
bool successful = false;
   try
   {
       string insertUserSQL =
           string.Format("INSERT INTO {0}Users{1} ({0}user{1},{0}ID{1},{0}password{1}) VALUES (?,?,?)",
                         DatabaseContext.QuotePrefix, DatabaseContext.QuoteSuffix);
       List<DbParameter> parameters = new List<DbParameter>();
       HashAlgorithm hashAlgorithm = SHA512.Create();
       byte[] b = new byte[32];
       RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
       rng.GetBytes(b);

       //create all 3 parameters, the order is critical since
       //these are positional parameters.

       //user
       DbParameter p = DatabaseContext.Factory.CreateParameter();
       p.DbType = DbType.String;
       p.Value = txtUserName.Text;
       parameters.Add(p);

       //ID and salt
       p = DatabaseContext.Factory.CreateParameter();
       p.DbType = DbType.Binary;
       p.Value = b;
       parameters.Add(p);

       //password
       p = DatabaseContext.Factory.CreateParameter();
       p.DbType = DbType.Binary;
       p.Value = b;
       parameters.Add(p);

       string s = txtPassword.Text;

       List<byte> pass = new List<byte>(Encoding.Unicode.GetBytes(s));
       pass.AddRange(b);
       p.Value = hashAlgorithm.ComputeHash(pass.ToArray());

       DatabaseContext.ExecuteNonQuery(insertUserSQL, parameters);
       successful = true;
   }
   catch (Exception ex)
   {
       Debug.WriteLine(ex.ToString());
   }
   Label3.Text = "Registration " + (successful ? "successful" : "failed");

The above code is as simple as possible. It should include checks for usernames already existing in the database and more.

The next thing is actually responding to a login request. Again, it is a surprisingly simple bit of code. Our list of things to do is:

  • Wait 2 seconds to "tarpit" attackers, thus slowing brute force attacks to a crawl.
  • Get the username and password from the user.
  • Get the correct record from the database.
  • Use the salt from the database to create a hash from the salt and password attempt.
  • Compare the resulting hash to the password hash that is stored in the database.
  • Return "login failed" or "login successful"; we don't want to show "user not found" or "incorrect password" as that would give attackers too much information.

Here is the code to do all of that:

C#
Thread.Sleep(2000); //tarpit
            bool successful = false;
            HashAlgorithm hashAlgorithm = SHA512.Create();
            string retrieveUser =
                string.Format(
                    "SELECT {0}ID{1} FROM {0}Users{1} WHERE {0}user{1}=?",
                    DatabaseContext.QuotePrefix,
            DatabaseContext.QuoteSuffix);
            List<DbParameter> parameters = new List<DbParameter>();

            try
            {
                DbParameter p = DatabaseContext.Factory.CreateParameter();
                p.DbType = DbType.String;
                p.Value = txtUserName.Text;
                parameters.Add(p);
                byte[] computedHash = null;

                using (DbDataReader dr = DatabaseContext.ExecuteReader(retrieveUser, parameters))
                {
                    if (dr.Read())
                    {
                        byte[] salt = (byte[])dr.GetValue(0);
                        List<byte> buffer =
                          new List<byte>(Encoding.Unicode.GetBytes(txtPassword.Text));
                        buffer.AddRange(salt);
                        computedHash = hashAlgorithm.ComputeHash(buffer.ToArray());
                    }
                }
                if (computedHash != null)
                {
                    DbParameter p2 = DatabaseContext.Factory.CreateParameter();
                    p2.DbType = DbType.String;
                    p2.Value = computedHash;
                    parameters.Add(p2);
                    retrieveUser =
                        string.Format(
                            "SELECT COUNT(*) FROM {0}Users{1} WHERE {0}user{1}=? AND {0}password{1}=?",
                            DatabaseContext.QuotePrefix,
                            DatabaseContext.QuoteSuffix);
                    using (DbDataReader dr = DatabaseContext.ExecuteReader(retrieveUser, parameters))
                    {
                        if (dr.Read())
                        {
                            successful = Convert.ToInt32(dr.GetValue(0)) == 1;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }
            Label3.Text = "Login " +
              (successful ? "successful" : "failed"); 

Points of interest

As stated above, an MD5 hash is the same size as a GUID, thus I am using the ID as my salt. This would be a horrible idea if the ID field is either predictable, like a pseudo-random number or worse and autoincrement number.

Because I am using a parameter for the values, I can use any value I like for the username, be it an email address, or something with normally disallowed characters, such as an apostrophe like in "O'Brian".

Remember to use SSL to secure any page that deals with sensitive information, especially passwords since people tend to reuse passwords. Cost is not an excuse. While some sites like Verisign and Thawte charge over $100 (US) to purchase an SSL certificate, you can get cheap certificates from GoDaddy for around $30 (US) or free from StartSSL, and just like Verisign and Thawte, they work in all major browsers as well. Before the CAcert crowd starts responding, I don't recommend CA, because it will require all who use your site to insert the root certificate manually.

There are a few shortcomings to this code. For example, instead of simply using a 2 second tar pit, we could "spin the password", and hash the hash many times, multiplying the complexity of using something like rainbow tables to brute force the password. Also I didn't touch on enforcing password strength, or keeping history, or storing an authentication token or anything like that. Still, this is a really good place to start.

The hash could also be stronger if you included the username.

History

  • 2014-04-01: fixed the buffer size after the re-write, a guid must be 32 bytes
  • 2014-03-31: changed the article to use the ID column as a salt, to prevent data moving attacks. also modified the code slightly.
  • 2010-01-25: Added the "Remember SSL" point of interest.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Pengdows
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralComment Pin
kalsa23-Aug-16 0:28
kalsa23-Aug-16 0:28 
GeneralMy vote of 5 Pin
jgakenhe2-Apr-14 7:17
professionaljgakenhe2-Apr-14 7:17 
GeneralMy vote of 3 Pin
Oleg A.Lukin1-Apr-14 22:10
Oleg A.Lukin1-Apr-14 22:10 
GeneralRe: My vote of 3 Pin
Alaric Dailey2-Apr-14 0:47
Alaric Dailey2-Apr-14 0:47 
SuggestionSmall typo/grammar Pin
_Noctis_1-Apr-14 14:42
professional_Noctis_1-Apr-14 14:42 
GeneralRe: Small typo/grammar Pin
Alaric Dailey2-Apr-14 0:42
Alaric Dailey2-Apr-14 0:42 
GeneralRe: Small typo/grammar Pin
_Noctis_2-Apr-14 0:47
professional_Noctis_2-Apr-14 0:47 
GeneralMy vote of 5 Pin
AlphaDeltaTheta27-Jul-13 16:47
AlphaDeltaTheta27-Jul-13 16:47 
GeneralRe: My vote of 5 Pin
Oleg A.Lukin1-Apr-14 22:13
Oleg A.Lukin1-Apr-14 22:13 
QuestionSTorage of salt and choosing a salt Pin
gurjsing16-Sep-12 23:27
gurjsing16-Sep-12 23:27 
AnswerRe: STorage of salt and choosing a salt Pin
Alaric Dailey16-Oct-12 3:40
Alaric Dailey16-Oct-12 3:40 
AnswerRe: STorage of salt and choosing a salt Pin
jgakenhe2-Apr-14 7:20
professionaljgakenhe2-Apr-14 7:20 
AnswerRe: STorage of salt and choosing a salt Pin
RobTeixeira2-Apr-14 11:36
RobTeixeira2-Apr-14 11:36 
Generalthanks for sharing ... Pin
ditsche4-Feb-10 9:02
ditsche4-Feb-10 9:02 
GeneralRe: thanks for sharing ... Pin
Alaric Dailey4-Feb-10 11:41
Alaric Dailey4-Feb-10 11:41 
QuestionRe: thanks for sharing ... Pin
ditsche5-Feb-10 5:12
ditsche5-Feb-10 5:12 
AnswerRe: thanks for sharing ... Pin
Alaric Dailey5-Feb-10 7:01
Alaric Dailey5-Feb-10 7:01 
GeneralRe: thanks for sharing ... Pin
Alaric Dailey8-Feb-10 3:42
Alaric Dailey8-Feb-10 3:42 
GeneralGood article but the code is too coupled Pin
Puchko Vasili24-Jan-10 9:54
Puchko Vasili24-Jan-10 9:54 
GeneralRe: Good article but the code is too coupled Pin
Alaric Dailey24-Jan-10 11:36
Alaric Dailey24-Jan-10 11:36 
GeneralRe: Good article but the code is too coupled Pin
Puchko Vasili27-Jan-10 8:09
Puchko Vasili27-Jan-10 8:09 
GeneralRe: Good article but the code is too coupled Pin
Alaric Dailey27-Jan-10 13:31
Alaric Dailey27-Jan-10 13:31 
GeneralRe: Good article but the code is too coupled Pin
tiffstormy28-Jan-10 4:46
tiffstormy28-Jan-10 4:46 
GeneralNice article Pin
Richard MacCutchan24-Jan-10 6:28
mveRichard MacCutchan24-Jan-10 6:28 
GeneralRe: Nice article Pin
Alaric Dailey24-Jan-10 7:07
Alaric Dailey24-Jan-10 7:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.