8 Handling passwords

This chapter covers

  • Learning how passwords may be stolen
  • Learning how attackers retrieve encrypted or hashed passwords
  • Exploring why hashing is best for password handling
  • Implementing password hashing with ASP.NET Core
  • Changing default password hashing behavior of ASP.NET Core

In early October 2013, Adobe reported a security incident on their servers. The original blog posts are no longer available (only using a web archive), but independent news sources paint a very clear picture. According to security journalist Brian Krebs (http://mng.bz/o2DZ), attackers were able to access the source code of a few Adobe products. Also, personal customer information was stolen. As the Verge reported (http://mng.bz/nND5), the number of affected accounts was initially estimated at around 3 million, but the actual number turned out to probably be over 150 million. To be fair, it is unclear how many of those accounts were inactive or test accounts. The numbers are still staggering, though.

Among the data extracted were, among other things, passwords. Luckily, they were not stored in plaintext. However, it was still possible to access many of them due to the way the application worked. Let’s look at this case study to learn what went wrong (and to make it better).

8.1 From data leak to password theft

The passwords were encrypted with a symmetric-key block cypher (3DES), using the same key for encrypting and decrypting data (see chapter 7). The encryption was also deterministic: if two users had the same password, the same value was stored in Adobe’s database.

This turned out to be a real problem given the sheer amount of data stolen. If someone has access to the data dump, one way to decipher passwords is to group the encrypted values by the count of their occurrence. The more often an encrypted password occurs, the more popular the password is—not only in the hacked application, but in many other websites, too.

There are several lists of the most commonly used passwords available. The best-known list is probably by NordPass, the developers of the password manager of the same name. Here are the top ten entries from 2020 (https://nordpass.com/most-common-passwords-list/ will contain the most up-to-date list at the time of checking the URL):

  1. 123456

  2. 123456789

  3. picture1

  4. password

  5. 12345678

  6. 111111

  7. 123123

  8. 12345

  9. 1234567890

  10. senha (In case you are wondering, that’s Portuguese for password.)

Very creative, isn’t it? Note that the NordPass page also estimates the time required to crack such a password. Whereas the most commonly used one, 123456, may be cracked in less than a second, the number 62 entry, ohmnamah23, takes 12 days. That password still isn’t very good, but you see the difference.

Choosing a secure password

Obviously, 123456 is not a secure password for a variety of reasons. It’s rather short, only uses digits, and is predictable (and holds the top spot on both the NordPass and the Adobe list). For many years, a common suggestion was to use letters, digits, and special characters to avoid passwords that were easy to guess or part of a dictionary. The downside of this approach is that such passwords were hard to remember, so users routinely wrote them down and were also reusing them (so if a password was stolen once, the attacker might try it at many different services).


The most important aspect of a password is the entropy, a value that depends on the number of guesses a brute-force attack would require to find out the password. The password 123456 has an entropy of almost 20 bits, so it would take at most 220 attempts (a bit more than 1 million) to guess it. Obviously, that number is not very high—for a computer, at least. A password consisting of six lowercase characters, six uppercase characters, and six digits would have an entropy of over 100 bits, requiring a number of guesses that is 33 digits long. So, the longer the password, the better.


NIST issues requirements for “digital identities,” which also include passwords. For a while, the use of special characters was recommended, but that was eventually dropped. The current version, at the time of this writing (http://mng.bz/v6Dp—look for section 5.1.1.2, “Memorized Secret Verifiers”), recommends a minimum password length of 8 characters but also prompts developers to expect up to 64 characters. I’d aim at the latter.


Since password safes—software that stores a number of passwords and uses one passphrase to get access to them—are a commodity nowadays, there is no excuse not to use long, secure passwords. All these password safes offer to autogenerate such passwords. Even web browsers play their part, as the figure here shows (apologies for ruining the party, but that’s not the password I’m actually using).

CH08_F01_Wenz

Firefox suggests a secure password.

If you are not using a password manager, you may use the advice of Randall Munroe’s popular webcomic, XKCD. Just take a few unrelated words and use them as a password: His example is correct horse battery staple; this gives about 44 bits of entropy (see https://xkcd.com/936/ for the details).

Back to the list of Adobe encrypted passwords. Chances are that the most commonly used passwords at the Adobe site are also high up on the NordPass list (or any other comparable list). This facilitates guessing those passwords, even though the encryption itself has not been broken yet (see the following note). But in reality, things were even worse. Adobe also stored a security question that allowed users to remember their passwords. Note that I wrote “remember,” not “retrieve.” Instead of “What was the color of your first car?” users were prompted to provide a passphrase that would remind them of their passwords! These passphrases were stored in plaintext and were also part of the data dump.

note A group of researchers successfully recovered many of the Adobe passwords. The original page is no longer available, but the Internet Archive’s Wayback Machine still has a copy: http://mng.bz/44ER. As you can see, there are many overlaps with the NordPass list.

Here’s a little quiz. The following passphrases were used by various people who all had the same password:

  • let me in

  • knock

  • lmi

  • open sesame

  • let who in?

  • the usual

  • standard

Would you take an educated guess what the associated password is? Spoiler: it’s letmein.

Developer Ben Falconer took this one step further and created a crossword puzzle (https://zed0.co.uk/crossword/) containing some of the top 1,000 passwords from the Adobe leak. Figure 8.1 shows one based on the top 100 passwords.

CH08_F02_Wenz

Figure 8.1 1 down: What could adobex2, adobe twice, and 2xadobe hint at?

The Adobe password leak is certainly not the only high-profile incident where user data has been stolen en masse. The “Have I Been Pwned?” service (https://haveibeenpwned.com/) contains the data from countless leaks, large and small, and tells you which data may have been stolen. I checked one throwaway email address I was using for nonessential services. It was part of 30 breaches (so far)—figure 8.2 shows the first three, including details about the data stolen.

Retrieving passwords from dumps is the effect of the actual problem: data being leaked. Often, this occurs using SQL injection, which we can properly defend our applications against (see chapter 6). There are other attack vectors, too, such as malware, accessing a back door in an application, or an internal attacker. Remember that we are always aiming at defense in depth! If data is being stolen, we need to make sure that the attackers can do as little as possible with it. When talking about passwords specifically, we must never store them, and we need to make it virtually impossible to retrieve those passwords from the stolen pieces of information. This is where hashing comes in.

CH08_F03_Wenz

Figure 8.2 The first three of 30 (!) breaches that contained a certain email address

8.2 Implementing password hashing

Not storing passwords in cleartext goes without saying—if there is a data leak, the password is gone. Symmetric encryption does not really work, either. If the decryption key is stolen along with the encrypted passwords, the latter are gone, too. There is no real good solution if a website needs to access the passwords of the users. If the application can somehow get ahold of the password, an attacker may, too. That’s why you should run if a website asks you to email a “forgotten” password (instead of providing you with a means to reset it).

But how can an application implement a login form if there is no way to retrieve a stored password? A common best practice is to not store the password itself but to store a hash of it. Imagine the hash as being like a fingerprint. If police find a fingerprint at a crime scene, they usually do not know the person it belongs to. However, if they have a list of suspects, they can take their fingerprints and then compare them with the one found. Someone’s fingerprint is unique and does not change significantly. Hashing passwords works in a similar fashion, as figure 8.3 shows.

CH08_F04_Wenz

Figure 8.3 Password hashing in action

When a user registers with a site, their password is not stored. Instead, its fingerprint is generated (as a kind of one-way encryption) and then put into the database. Whenever that user tries to log in using their password, the application calculates the hash of the provided password and compares it with the one from the database. If they match, the application assumes that the password was correct and grants access.

There are many options for hashing algorithms. However, not all of them are suited for usage with passwords. First of all, the hash should have a certain length; otherwise, an attacker might manage to find a hash collision—two passwords having the same hashed value. Imagine that an attacker manages to get ahold of a password hash—for instance, via a leak. They now know the hash they need and just have to find a password that has exactly this hash. With a very insecure hashing algorithm, that password might not even be the one chosen by the user! Let’s start with a hashing algorithm that has been used for a very long time, MD5.

8.2.1 MD5 (and why not to use it)

Spoiler alert: MD5 should be avoided these days. It creates a hash that is 128 bits, or 16 bytes, long. Back in the mid-1990s (!), it became obvious that MD5 might be broken beyond repair, since some research could prove that finding a collision might be feasible. In 2012, a research paper by Marc Stevens demonstrated an approach to construct such a collision using one single block (64 bytes) of input data (http://mng.bz/Qv66). Since then, relying on MD5 hashes to verify the validity of critical data such as passwords is futile.

Before we look at how to better hash algorithms, let’s talk about some other reasons why MD5 should be avoided. We’ll use the password from the aforementioned XKCD comic:

correct horse battery staple

The following simple console program calculates the hash of that password:

using System;
using System.Security.Cryptography;
using System.Text;
 
public class Program
{
    public static void Main()
    {
        using (var md5 = new MD5Cng()) {                 
            var enc = new ASCIIEncoding();
            var hash = md5.ComputeHash(                  
                enc.GetBytes(                            
                    "correct horse battery staple"));    
            var sb = new StringBuilder();
            foreach (var b in hash) {
                sb.Append(b.ToString("x2").ToLower());   
            }
            Console.WriteLine(sb.ToString());            
        }
            
    }
}

Uses the MD5 functionality built into .NET

Computes the MD5 hash of a byte array

Creates the byte array from the input string

Converts the calculated hash into a hex string

Outputs the result

The desired password is converted into a byte array, which is the required input data type for .NET’s MD5 calculation. The result is then converted into a hexadecimal string and looks like this:

9cc2ae8a1ba7a93da39b46fc1019c481

So how do we find a password that has the same MD5 hash? Using brute force, we would not live long enough to await the end result (unless, of course, there are revolutionary advances when it comes to calculation speed). However, there are databases of precalculated hashes available. For everything on the NordPass list or other lists, there is already an available hash. Depending on the hashing algorithm used, a technique called rainbow tables can speed up the password guessing significantly. The technical details are beyond the scope of this book, but here’s a simplified explanation: rainbow tables cache intermediate results when generating hashes to save potentially many CPU cycles.

There are password/hash lists and rainbow tables consisting of many, many gigabytes of data, and there are also websites that look up hashes in their database. I tried a few better-known ones, and surprisingly, they did not find the previous MD5 hash. However, entering the hash into Google yielded the result shown in figure 8.4.

CH08_F05_Wenz

Figure 8.4 The “correct horse battery staple” password hash is common knowledge.

Over two dozen websites already know that hash—this is a sign that you should avoid the password. Some web applications tried to mitigate this kind of attack by using a technique called salting. The application added a piece of information to the password, a “salt,” and then calculated the hash. This salt was known only to the application. Let’s try a salt of Manning, which could lead to the following data prior to hashing:

correct horse battery stapleManning

That leads to the following MD5 hash:

06c8cc9cb28ea9e1224efcb2cc96b8d5

Google now reports zero matches (this will, of course, change once the e-book is indexed).

Salting provides protection to some degree from the aforementioned attacks. However, in our example, we made a few mistakes that torpedo our security efforts:

  • The salt is rather short.

  • The salt is predictable.

  • The salt is constant.

A better approach would be to generate a long, random salt for each password and store the salt along with the hash. There is no direct way to retrieve the password from those two pieces of information. Also, if two users pick the same password, their hashes will be very different.

You might add some extra precautions by also adding pepper to the salt. Yes, that’s really the terms that are being used. “Peppering” adds another security layer in case the database has been leaked (which means that the hashes and the salts are now known to the attacker). The hashes could be signed or encrypted. The application needs to know the appropriate data to validate the signature or to decrypt the hash. So if the application itself is not stolen, the extra defense layer might protect the hashes.

Now that we have established that MD5 is not secure any longer, let’s look at alternatives. Another popular algorithm, SHA1, is also considered unsuitable. Other SHA-based algorithms, such as SHA256, SHA384, or SHA512, are better, but basically, the following four options are viable:

  • Argon2—The winner of a password competition running from 2013 to 2015 (see www.password-hashing.net/ for details).

  • PBKDF2 (Password-Based Key Derivation Function 2)—The recommendation by NIST; also fulfills the FIPS-140 standard (NIST’s Federal Information Processing Standard for cryptography).

  • scrypt—A hashing algorithm that uses more resources than many of the alternatives to make it more costly for attackers to brute-force a hash.

  • bcrypt—An older password hashing algorithm dating back to 1999, but considered secure. There are some limitations, though, including a maximum length of 72 bytes for the data to be hashed.

.NET comes with support for PBKDF2; there are third-party implementations for the other algorithms, which we will talk about in a bit. But first, on to PBKDF2.

8.2.2 PBKDF2

The IETF republished PBKDF2 as RFC 2898 (see www.ietf.org/rfc/rfc2898.txt), so that’s why the .NET class responsible for that algorithm is aptly, but probably surprisingly, called Rfc2898DeriveBytes and resides in the System.Security .Cryptography namespace. The class constructor expects three parameters:

  • The password (or value) to hash.

  • The size (in bytes) of the salt (it’s also possible to provide a custom salt).

  • The number of iterations the algorithm should use. The more iterations, the more secure the hash, but the longer hashing takes. This value is optional, but we will set it, and you will see why in a bit.

The following listing shows a username/password form that mimics the two essential aspects of using a password hash: generating the hash upon registration and validating the hash upon login.

Listing 8.1 The combined registration/login form as a Razor Page

@page
@model HashingModel
 
<div class="text-center">
    <h1 class="display-4">Password Hashing</h1>
    <div class="mt-5 mb-5">
        <form method="post" action="">
            <div class="form-group">
                <label class="control-label" for="UserName">User name</label>
                <input type="text" id="UserName" name="UserName" 
                class="form-control" value="@Model.UserName" />          
            </div>
            <div class="form-group">
                <label class="control-label" for="Password">Password</label>
                <input type="password" id="Password" name="Password" 
                class="form-control" value="@Model.Password" />          
            </div>
            <div class="form-group">
                <label class="control-label" for="HashToVerify">Hash to
                verify</label>
                <input type="text" id="HashToVerify" name="HashToVerify" 
                class="form-control" value="@Model.HashToVerify" />      
            </div>
            <div class="form-group">
                <label class="control-label" for="SaltToVerify">Salt to
                verify</label>
                <input type="text" id="SaltToVerify" name="SaltToVerify" 
                class="form-control" value="@Model.SaltToVerify" />      
            </div>            
            <div class="form-group">
                <input type="submit" asp-page-handler="Register"
                value="Register" class="btn btn-primary" />              
                <input type="submit" asp-page-handler="Login" value="Login" 
                class="btn btn-primary" />                               
            </div>
        </form>
    </div>
    <div class="mb-3">
        @Model?.Message
    </div>
</div>

Shows the username field

Shows the password field

Shows the hash field (for mimicking the login)

Shows the salt field (for mimicking the login)

Shows the Registration button

Shows the Login button

The form fields are filled with the values from the model, including the password field. That’s not best practice, of course, but it helps us test the hash creation and verification without any extra copy-and-paste efforts. The associated page model class is shown in the next listing.

Listing 8.2 The page model class for the combined registration/login form

using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
 
namespace AspNetCoreSecurity.RazorSamples.Pages
{
    public class HashingModel : PageModel
    {
        public string Message { get; set; } = string.Empty;
 
        [BindProperty]
        public string UserName { get; set; } = string.Empty;
        [BindProperty]
        public string Password { get; set; } = string.Empty;
        [BindProperty]
        public string HashToVerify { get; set; } = string.Empty;
        [BindProperty]
        public string SaltToVerify { get; set; } = string.Empty;
 
        public void OnPostRegister()    
        {
            // TODO
        }
 
        public void OnPostLogin()       
        {
            // TODO
        }
    }
}

The Handler method for the Register button

The Handler method for the Login button

We will fill in the blanks—the code that runs after clicking the Register or Login buttons—individually for all the algorithms we will cover. First, the PBKDF2 hash creation:

public void OnPostRegister()
{
    var rfc2898 = new Rfc2898DeriveBytes(                       
        this.Password,                                          
        32,                                                     
        310_000);                                               
 
    var hash = Convert.ToBase64String(rfc2898.GetBytes(20));    
    var salt = Convert.ToBase64String(rfc2898.Salt);            
 
    this.HashToVerify = hash;
    this.SaltToVerify = salt;
    this.Message = "Hash created";
}

Instantiates the Rfc2898DeriveBytes class

Pulls out the hash (and Base64-encodes it)

Pulls out the salt (and Base64-encodes it)

The Rfc2898DeriveBytes class creates the hash that is available by calling GetBytes(20), 20 being the default length in bytes of the hash. The automatically generated salt is more conveniently available by accessing the Salt property. Both values are then Base64-encoded and written in the page model properties so that they show up in the form fields.

Figure 8.5 shows one possible outcome of this code. Yours will most certainly look different since the hash is so random that a collision is highly unlikely.

CH08_F06_Wenz

Figure 8.5 One possible set of salt/hash pairs for the most popular password, 123456

Note If you feel so inclined, you may also generate your own secure salt by calling new RNGCryptoServiceProvider().GetBytes().

Since the hash and salt input fields are now conveniently prepopulated, we can implement the password verification method:

public void OnPostLogin()
{
    var salt = Convert.FromBase64String(
    this.SaltToVerify);                                
    var rfc2898 = new Rfc2898DeriveBytes(                
        this.Password,                                   
        salt,                                            
        310000);                                         
    var hash = Convert.ToBase64String(
    rfc2898.GetBytes(20));                             
    var isValid = hash == this.HashToVerify;             
 
    if (isValid)
    {
        Message = "Login successful";
    } else
    {
        Message = "Login failed";
    }
}

Converts the salt from the form to a string

Calculates the hash using the same salt as before

Converts the generated hash to a Base64-encodded string

Compares the stored hash with the generated one

Using the same password with the same salt and the same number of iterations will generate the same hash. So, if both the old and the new hash match, the password must be the same and the login was successful. In a real-world application, both the hash and the salt (and, if you intend to change that eventually, the number of iterations) will be stored in the database and then used later to verify the password. At no point did the application persist the password itself.

The OWASP maintains a password storage cheat sheet at http://mng.bz/XZy9, which they continuously update according to advances in password-cracking techniques. Among other things, they recommend the number of iterations to use for PBKDF2. That’s precisely where the value of 310,000 came from. Make sure that you revisit that page and possibly update the number of iterations accordingly. The OWASP cheat sheet also covers other relevant algorithms, so let’s look at their .NET implementations.

8.2.3 Argon2

Argon2 is—at least for now—the best option for password hashing. The algorithm is modern and efficient, but there is no built-in support in .NET (yet). The most common library implementing Argon2 is libsodium (see https://doc.libsodium.org/ for more details). There are a few .NET ports and wrappers for that library, with libsodium-core from https://github.com/tabrath/libsodium-core being the best maintained version at the moment. (This is not to be confused with the original from https://github.com/adamcaudill/libsodium-net. This version does not seem to be maintained any longer and is not compliant to .NET Standard, so we are using a fork.)

In order to use the library, the Sodium.Core package needs to be added to the project, either by using the command line

dotnet add package Sodium.Core

or by using the NuGet Package Manager console:

Install-Package Sodium.Core

Or, even easier, by referring to the NuGet package manager GUI in Visual Studio (figure 8.6).

CH08_F07_Wenz

Figure 8.6 Adding Sodium.Core in Visual Studio’s NuGet package manager

Here, you can easily install the package in the Visual Studio solution. Then a simple call to the ArgonHashString() method creates the hash:

using Sodium;
 
...
 
public void OnPostRegister()
{
    var hash = PasswordHash.ArgonHashString(    
        this.Password,
        PasswordHash.StrengthArgon.Interactive)
        .TrimEnd('');                         
    var salt = string.Empty;
 
    this.HashToVerify = hash;
    this.SaltToVerify = salt;
    this.Message = "Hash created";
}

Hash the string using Argon2.

Removes excessive null bytes

Note that the code removes null bytes at the end of the hash. Otherwise, the validation may fail later. There are four predefined strength levels for Argon2; we use the default. Figure 8.7 shows the possible output for a call to this code.

CH08_F08_Wenz

Figure 8.7 Hashing using Argon2

The hash contains all the information about the algorithm—which one was used (Argon2, or Argon2id, to be exact), the parameters being used (e.g., t is the minimum number of iterations, and m is the minimum memory size), and the salt. In this case, there is no need to store any extra values since the hash is basically self-descriptive. Verifying a hash is also not much more than a simple method call:

public void OnPostLogin()
{
    var isValid = PasswordHash.ArgonHashStringVerify(    
        this.HashToVerify, this.Password);               
 
    if (isValid)
    {
        Message = "Login successful";
    } else
    {
        Message = "Login failed";
    }
}

Verifies the hash

The ArgonHashStringVerify() method is “smart” enough to accept the complete hash and the password to verify against it; all the metadata about the algorithm, settings, and salt is extracted from the former.

8.2.4 scrypt

The best bet for scrypt support under .NET is the Scrypt.NET package from https://github.com/viniciuschiele/Scrypt, which can be installed similarly to Sodium .Core: command line, NuGet package manager console, or NuGet package manager UI. Afterward, hashing a password requires just minimal coding (shown in bold):

using Scrypt;
 
public void OnPostRegister()
{
    var scryptEncoder = new ScryptEncoder();
    var hash = scryptEncoder.Encode(this.Password);
    var salt = string.Empty;
 
    this.HashToVerify = hash;
    this.SaltToVerify = salt;
    this.Message = "Hash created";
}

Here is a typical resulting hash, once again using the password 123456 as an input:

$s2$16384$8$1$gduLm6gW+tVEC3V68FVNFSqprYi+rylX6tgJ2FqoE+E=$RhN0FWmgf5vXqgfQCoeIiG6nZyXXQp8CkZsBuIb1VfM=

Once again, all the relevant information about the hashing is part of the hash:

  • s2 stands for the scrypt algorithm.

  • 16384 is the number of iterations.

  • 8 is the block size.

  • 1 is the number of threads.

The OWASP password storage cheat sheet recommends higher values, so we should increase the default values. There are a few different suggestions in the document, but one is to increase the number of threads to 4, and another one is to increase the iterations to 65536. This may be achieved in the class constructor:

var scryptEncoder = new ScryptEncoder(
    iterationCount: 65536,
    blockSize: 8,
    threadCount: 4);

It is also possible to provide a custom salt generator (the argument is adequately called saltGenerator), but usually this is not required.

Verifying a password provided upon login against a stored hash is rather trivial as well:

public void OnPostLogin()
{
    var scryptEncoder = new ScryptEncoder();
    var isValid = scryptEncoder.Compare(
        this.Password, this.HashToVerify);
 
    if (isValid)
    {
        Message = "Login successful";
    } else
    {
        Message = "Login failed";
    }
}

This time, there are no arguments required for the ScriptEncoder class, since all the hashing configuration options may be pulled from the hash itself.

8.2.5 bcrypt

Finally, let’s have a look at an “oldie but goldie”—bcrypt was first unveiled in 1999 and has stood the test of time. This is pretty amazing given all the advances in computing power. For instance, SHA1 is from 1995, and its use has basically been discouraged everywhere since 2017. There is a NuGet package called BCrypt.Net-Next that brings bcrypt support to .NET (if you want a signed package, use BCrypt.Net-Next.StrongName). Its source code is available from https://github.com/BcryptNet/bcrypt.net.

After installation of the package, hashing a password is essentially a one-liner. Note that the fully qualified method call would be BCrypt.Net.BCrypt.HashPassword(), so make sure to put the using within your current namespace, not outside of it:

...
 
namespace AspNetCoreSecurity.RazorSamples.Pages
{
    using BCrypt.Net;
 
    public class HashingModel : PageModel
    {
 
    ...
 
        public void OnPostRegister()
        {
            var hash = BCrypt.HashPassword(this.Password);
            var salt = string.Empty;
 
            this.HashToVerify = hash;
            this.SaltToVerify = salt;
            this.Message = "Hash created";
        }
 
    ...
    }
}

This code then generates a bcrypt hash with the default settings, which can look like this:

$2a$11$mxPcFdFcwKzn4Mv.llBaV.sOoGUaWCK.WdZZaEYQqP2wDwjMBvC.W

By now, you should know the drill: 2a is the identifier of the bcrypt hashing algorithm, and 11 is the cost factor (it corresponds to 211=2,048 iterations). OWASP recommends a minimum of 10 (1,024 iterations), so we are good. If you feel the need to increase the value, you can do so as follows:

var hash = BCrypt.HashPassword(
    this.Password,
    workFactor: 12);

The hash verification is done by the BCrypt.Verify() method:

public void OnPostLogin()
{
    var isValid = BCrypt.Verify(this.Password, this.HashToVerify);
 
    if (isValid)
    {
        Message = "Login successful";
    } else
    {
        Message = "Login failed";
    }
}

And that’s it. Once you know how, securely creating a password hash and verifying it later is just a few lines of code. If you roll your own user management and login form, hashing is the best way to go. However, if you look at the ASP.NET Core templates, you will notice that they already use password hashing out of the box.

8.3 Analyzing ASP.NET Core templates

When creating an ASP.NET Core application based on the default template, one of the questions asked is whether you want to use authentication (figure 8.8).

CH08_F09_Wenz

Figure 8.8 ASP.NET Core project creation in Visual Studio

note When using another IDE, and/or using dotnet new to create a project based on the web template, use the -au option to configure the desired authentication type.

When choosing anything but None, the project will allow users to register and to log in. The URLs of these two pages end with /Account/Register and /Account/Login. Surprisingly, there are no associated Razor Pages in the project. The reason is that Microsoft is hiding the UI in a separate package. To take a look, go to Visual Studio’s Project Explorer, right-click on the application, and select Add/New Scaffolded Item. Then you can pick the individual pages you would like to be added to the project (figure 8.9).

CH08_F10_Wenz

Figure 8.9 Adding account-related pages to the project

Note If you get an error message, try again—sometimes Visual Studio lags behind in the packages required for scaffolding. Also, you may want to manually install the Microsoft.VisualStudio.Web.CodeGeneration.Utils NuGet package to the project prior to scaffolding.

That’s a lot of pages! We are only interested in AccountRegister so far, but if you are curious, feel free to pick as many as you like. In the Data Context Class drop-down menu, pick the sole entry. Then the Areas/Identity/Pages/Account folder in the project will be filled with many new Razor Pages. We are especially interested in Register .cshtml, the registration page, and its associated model class, Register.cshtml.cs. The latter file contains the OnPostAsync() method, which is executed if the registration form is submitted. The crucial lines of code are these two:

var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);

A new user is created, but the password is not part of the model. Instead, the CreateAsync() method of the user manager receives the credentials as an additional argument. The implementation of this user manager is part of .NET, so looking at the source code is the best option here to understand what’s going on. It is available at http://mng.bz/yvDp and looks like this (only showing the relevant parts, and the code is obviously subject to change):

public virtual async Task<IdentityResult> CreateAsync(TUser user, string password)
{
    var passwordStore = GetPasswordStore();
    var result = await UpdatePasswordHash(passwordStore, user, password);
    if (!result.Succeeded)
    {
        return result;
    }
    return await CreateAsync(user);
}

So before the user is created at the end of the method (by calling CreateAsync(), and just providing the user, sans the password), the password hash is updated. The method user, UpdatePasswordHash(), is implemented in the PasswordHasher class, with the source code available at http://mng.bz/M5KQ. If you glance over the source code, you can see that there are two versions of password hashing implemented: a v2, which uses PBKDF2 with 1,000 iterations, and a v3, which uses 10,000 iterations (there are other subtle differences). It’s good to know that ASP.NET Core is using a proven hashing algorithm. However, the iteration values are (currently) lower than what OWASP suggests.

The OWASP recommendation depends on the internal hashing algorithm used by the PBKDF2 implementation. If SHA256 is used (as v3 does), 310,000 iterations should be good. For SHA1 (as v2 uses), an iteration count of 720,000 is the suggested setting.

Luckily, a simple addition to Program.cs can change the behavior of the PBKDF2 hashing accordingly:

builder.Services.Configure<PasswordHasherOptions>(
    options => options.IterationCount = 310_000);

The password hasher may be swapped out against any other that implements the IPasswordHasher interface, which looks as follows:

public interface IPasswordHasher<TUser> where TUser : class
{
    string HashPassword(TUser user, string password);
    PasswordVerificationResult VerifyHashedPassword(TUser user, 
    string hashedPassword, string providedPassword);
}

That’s pretty straightforward, so it is relatively easy to use other hashing algorithms, such as the alternatives just mentioned. But it’s getting better: .NET security expert Scott Brady has created convenient NuGet packages for Argon2, bcrypt, and scrypt. With them, you can use a different hashing algorithm with minimal effort. Refer to http://mng.bz/aJzj, where you will find details about how to use the packages.

Note We will thoroughly cover ASP.NET Core Identity—which is the base of the user manager, among many other things—in chapter 12.

Summary

Let’s review what we have learned so far:

  • An application does not need to know the passwords of its users; it only needs to be able to verify whether a password is correct.

  • A hashing algorithm is a kind of one-way encryption and cannot be reversed.

  • Hashing passwords with a secure algorithm like PBKDF2 allows password verification but prevents attackers from easily retrieving the password.

  • ASP.NET Core supports PBKDF2 by default, but third-party libraries support alternatives like Argon2, bcrypt, and scrypt.

  • The default settings of ASP.NET’s password hashing need to be overridden for better security.

  • The ASP.NET Core project templates use PBKDF2 password hashing by default but are also extensible to use other algorithms.

After all the coding, it’s time to look at various configuration options that make our ASP.NET Core applications even more robust, starting with HTTP headers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.150.80