Avoid password reuse with Pwned Passwords and ASP.NET Core 🔒

Let's face it. Password re-use is something that should be avoided. Breach after breach are published and giant lists of users unhashed passwords available for hackers to exploit. In this post, I'll show you how we are avoiding people to re-use pwned passwords on elmah.io in our ASP.NET Core web app.

Avoid password reuse with Pwned Passwords and ASP.NET Core

In this post, I want to introduce you to Pwned Passwords and the PwnedPasswords.Client NuGet package. Pwned Passwords is an excellent service created by Troy Hunt. It's basically a REST API where you provide a user's hashed password and get a reply if that password is already in one or more password breaches. The API is free as long as you clearly attribute it where you consume data. If you like the API you can consider making a donation directly to Troy Hunt through the https://haveibeenpwned.com/ website.

Let's build an example in ASP.NET Core MVC where we utilize Pwned Passwords. You can either call the API through a HttpClient or use the nice little helper PwnedPasswords.Client created by Andrew Lock. In my test application I have an endpoint for users to call when signing up:

[HttpPost]
[Route("signup")]
public async Task<ActionResult> SignUp([FromForm]SignUpViewModel model)
{
    if (!ModelState.IsValid) return BadRequest();

    // ...
    
    return Ok();
}

The browser will send an HTTP POST to this endpoint and provide the posted form as properties within the SignUpViewModel class through data-binding:

public class SignUpViewModel
{
    [Required]
    public string Username { get; set; }

    [Required]
    public string Password { get; set; }
}

The // ... in the controller action represents the code where you would create the user in a local database or however you want to store user data.

The ModelState.IsValid property will verify that Username and Password have values and if not, return a bad request to the browser. After validating the password has a value, we want to extend the code to validate the password isn't pwned. Start by installing the PwnedPasswords.Client NuGet package:

dotnet add package PwnedPasswords.Client

The client is configured through dependency injection which follows how the rest of ASP.NET Core nicely. Include the following code in the ConfigureServices method in the Startup.cs file:

services.AddPwnedPasswordHttpClient(minimumFrequencyToConsiderPwned: 1)
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(2)));

The AddOwnedPasswordHttpClient method will register an IPwnedPasswordsClient instance in your application. The minimumFrequencyToConsiderPwned parameter tells PwnedPasswords.Client when to consider a password as pwned. In this example, I set it to 1 to never allow a password found in any breaches. The rest of the code configure retry when calling the Pwned Passwords API. If you haven't already, you need to install the Polly and Microsoft.Extensions.Http.Polly NuGet packages for this code to compile.

We are ready to start checking inputted passwords. Add a dependency to the constructor of the signup controller:

private readonly IPwnedPasswordsClient pwnedPasswords;

public SignupController(IPwnedPasswordsClient pwnedPasswords)
{
    this.pwnedPasswords = pwnedPasswords;
}

Finally, validate the posted password in the SignUp method:

public async Task<ActionResult> SignUp([FromForm]SignUpViewModel model)
{
    if (!ModelState.IsValid) return BadRequest();
    
    if (await pwnedPasswords.HasPasswordBeenPwned(model.Password)) return BadRequest();
    
    // ...
    
    return Ok();
}

The HasPasswordBeenPwned method will return true if the inputted password is found in one or more breaches. In this case, I return a bad request to the browser. You may want to include an error code, validation message, or similar back to the client in order to show a nice error message to the user.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website