Daniel Ovaska
Mar 13, 2018
  2864
(4 votes)

Using Troy Hunts Pwned Passwords API

Troy Hunt built a great API to check if a password has been compromised (pwned). 

Let's check out how to use it to make sure that your users don't use unsecure passwords!

Query the API

The first part is how to query the api. A simple repository with a single "GetOwnedCount" method can then look like:

public class OwnedPasswordRepository : IOwnedPasswordRepository
{
     static HttpClient client = new HttpClient();
     public string BaseUrl { get; set; } = "https://api.pwnedpasswords.com/range/";
     public int GetOwnedCount(string password)
     {
         var hashedPassword = Hash(password);
         var searchResultsString = client.GetStringAsync(BaseUrl + hashedPassword.Substring(0, 5)).Result;
         var resultsArray = searchResultsString.Split(new[] { "\r\n" }, System.StringSplitOptions.RemoveEmptyEntries);
         var key = hashedPassword.Substring(5);
         foreach (var resultString in resultsArray)
         {
             var values = resultString.Split(':');
             if (key == values[0])
             {
                 var ownedPasswords = Int32.Parse(values[1]);
                 return ownedPasswords;
             }
         }
         return 0;
     }
     public static string Hash(string input)
     {
         using (var sha1 = new SHA1Managed())
         {
             var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(input));
             var sb = new StringBuilder(hash.Length * 2);

             foreach (byte b in hash)
             {
                 sb.Append(b.ToString("X2"));
             }
             return sb.ToString();
         }
     }
}

Block users without secure passwords

For identity this can be done by implementing a new passwordvalidator class. Let's inherit the existing and spice it up:

public class OwnedPasswordValidator: PasswordValidator
{
    private readonly LocalizationService localizationService;
    private readonly IOwnedPasswordRepository _ownedPasswordRepository;
    public OwnedPasswordValidator(IOwnedPasswordRepository ownedPasswordRepository) :base()
    {
        _ownedPasswordRepository = ownedPasswordRepository;
        localizationService = ServiceLocator.Current.GetInstance<LocalizationService>();
    }
    private ILogger _log = LogManager.Instance.GetLogger(typeof(OwnedPasswordValidator).ToString());
    public string BaseUrl { get; set; } = "https://api.pwnedpasswords.com/range/";
    public const string DefaultErrorMessage = "Your password occurs in hacked databases {0} times. Try another password!";
    public int MaxAllowedOwnedPasswords { get; set; } = 0;
    public const string OwnedPasswordErrorKey = "/OwnedPasswordError";
    static HttpClient client = new HttpClient();
    public override Task<IdentityResult> ValidateAsync(string password)
    {
        IdentityResult resultToReturn = IdentityResult.Success;
        var baseResult = base.ValidateAsync(password).Result;
        if(baseResult.Succeeded)
        {
            try
            {
                var ownedPasswordsCount = _ownedPasswordRepository.GetOwnedCount(password);
                if (ownedPasswordsCount > MaxAllowedOwnedPasswords)
                {
                    resultToReturn = IdentityResult.Failed(string.Format(localizationService.GetString(OwnedPasswordErrorKey, DefaultErrorMessage), ownedPasswordsCount));
                }
            }
            catch(Exception ex)
            {
                _log.Error("Failed to call owned passwords service.",ex);
            }
        }
        else
        {
            resultToReturn = baseResult;
        }
        return Task.FromResult(resultToReturn);
    }
}

Ok, so far so good. We have our own password validator class. But how to force Episerver identity based site to use it? Easiest is to take control of the registration in owin startup. Let's create some IAppBuilder extensions for initialization. 

/// <summary>
/// Some helper methods to use with Episerver identity based sites. 
/// You can simply use it in your owin Startup.cs
/// 
/// app.AddCustomCmsAspNetIdentity<ApplicationUser>();
/// </summary>
public static class IdentityExtensions
{
    public static IAppBuilder AddCustomCmsAspNetIdentity<TUser>(this IAppBuilder app) where TUser : IdentityUser, IUIUser, new()
    {
        return app.AddCustomCmsAspNetIdentity<TUser>(new ApplicationOptions());
    }
    public static IAppBuilder AddCustomCmsAspNetIdentity<TUser>(this IAppBuilder app, ApplicationOptions applicationOptions) where TUser : IdentityUser, IUIUser, new()
    {
        applicationOptions.DataProtectionProvider = app.GetDataProtectionProvider();
        app.CreatePerOwinContext<ApplicationOptions>((Func<ApplicationOptions>)(() => applicationOptions));
        app.CreatePerOwinContext<ApplicationDbContext<TUser>>(new Func<IdentityFactoryOptions<ApplicationDbContext<TUser>>, IOwinContext, ApplicationDbContext<TUser>>(ApplicationDbContext<TUser>.Create));
        app.CreatePerOwinContext<ApplicationRoleManager<TUser>>(new Func<IdentityFactoryOptions<ApplicationRoleManager<TUser>>, IOwinContext, ApplicationRoleManager<TUser>>(ApplicationRoleManager<TUser>.Create));
        app.CreatePerOwinContext<ApplicationUserManager<TUser>>(new Func<IdentityFactoryOptions<ApplicationUserManager<TUser>>, IOwinContext, ApplicationUserManager<TUser>>(ApplicationUserManagerInitializer<TUser>.Create));
        app.CreatePerOwinContext<ApplicationSignInManager<TUser>>(new Func<IdentityFactoryOptions<ApplicationSignInManager<TUser>>, IOwinContext, ApplicationSignInManager<TUser>>(ApplicationSignInManager<TUser>.Create));
        app.CreatePerOwinContext<UIUserProvider>(new Func<IdentityFactoryOptions<UIUserProvider>, IOwinContext, UIUserProvider>(ApplicationUserProvider<TUser>.Create));
        app.CreatePerOwinContext<UIRoleProvider>(new Func<IdentityFactoryOptions<UIRoleProvider>, IOwinContext, UIRoleProvider>(ApplicationRoleProvider<TUser>.Create));
        app.CreatePerOwinContext<UIUserManager>(new Func<IdentityFactoryOptions<UIUserManager>, IOwinContext, UIUserManager>(ApplicationUIUserManager<TUser>.Create));
        app.CreatePerOwinContext<UISignInManager>(new Func<IdentityFactoryOptions<UISignInManager>, IOwinContext, UISignInManager>(ApplicationUISignInManager<TUser>.Create));
        ConnectionStringNameResolver.ConnectionStringNameFromOptions = applicationOptions.ConnectionStringName;
        return app;
    }
}

Ok, this looks tricky but to be honest it's really exactly what Episerver does below the hood except for one line:

 app.CreatePerOwinContext<ApplicationUserManager<TUser>>(new Func<IdentityFactoryOptions<ApplicationUserManager<TUser>>, IOwinContext, ApplicationUserManager<TUser>>(ApplicationUserManagerInitializer<TUser>.Create));

If you are observant you can see we have added a custom Create method. Below the hood that Create() method does this:

public static class ApplicationUserManagerInitializer <TUser> where TUser : IdentityUser, IUIUser, new()
{
    public static ApplicationUserManager<TUser> Create(IdentityFactoryOptions<ApplicationUserManager<TUser>> options, IOwinContext context)
    {
        var userManager = ApplicationUserManager<TUser>.Create(options,  context);
           
        userManager.PasswordValidator = new OwnedPasswordValidator(new OwnedPasswordRepository())
        {
            RequiredLength =  6,
            RequireNonLetterOrDigit = true,
            RequireDigit = true,
            RequireLowercase = true,
            RequireUppercase = true,
            MaxAllowedOwnedPasswords = 0
        };
        return userManager;
    }
}

So you can see above that we switch out the PasswordValidator to the our own. Only one step left now. We need to initialize this in Startup.cs with this line to use our new custom password validator:

//Comment out this:
//app.AddCmsAspNetIdentity<ApplicationUser>();
app.AddCustomCmsAspNetIdentity<ApplicationUser>();

Test drive

There you go! Take if for a test spin and check it out by trying to create a user with hacked password like: P@ssw0rd

Nuget package is available for Episerver 11 with id BinaryTrue.OwnedPassword. 

If you want to copy paste the code instead, head over to the github page.

Image Hacked2.PNG

Mar 13, 2018

Comments

Mar 13, 2018 01:53 PM

Nice solution. One comment though, if you now want to please Troy, please remove these lines:

RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,

These rules will only force users to create passwords they already know and uses, i.e. unsecure. What you can do instead is to require them to create longer passwords/passphrases.

Mar 13, 2018 02:24 PM

I agree. I just copied the default settings that Episerver uses. I will make them optional in the package with only Troy's solution as default.

Mar 13, 2018 09:47 PM

Added new appSettings for the password strength with version 1.1 of nuget package.









Default is basically off for everything except check vs Troy Hunts pwned database.

Nuget package is now uploaded with id:

BinaryTrue.OwnedPassword

Please login to comment.
Latest blogs
Opti ID overview

Opti ID allows you to log in once and switch between Optimizely products using Okta, Entra ID, or a local account. You can also manage all your use...

K Khan | Jul 26, 2024

Getting Started with Optimizely SaaS using Next.js Starter App - Extend a component - Part 3

This is the final part of our Optimizely SaaS CMS proof-of-concept (POC) blog series. In this post, we'll dive into extending a component within th...

Raghavendra Murthy | Jul 23, 2024 | Syndicated blog

Optimizely Graph – Faceting with Geta Categories

Overview As Optimizely Graph (and Content Cloud SaaS) makes its global debut, it is known that there are going to be some bugs and quirks. One of t...

Eric Markson | Jul 22, 2024 | Syndicated blog

Integration Bynder (DAM) with Optimizely

Bynder is a comprehensive digital asset management (DAM) platform that enables businesses to efficiently manage, store, organize, and share their...

Sanjay Kumar | Jul 22, 2024

Frontend Hosting for SaaS CMS Solutions

Introduction Now that CMS SaaS Core has gone into general availability, it is a good time to start discussing where to host the head. SaaS Core is...

Minesh Shah (Netcel) | Jul 20, 2024

Optimizely London Dev Meetup 11th July 2024

On 11th July 2024 in London Niteco and Netcel along with Optimizely ran the London Developer meetup. There was an great agenda of talks that we put...

Scott Reed | Jul 19, 2024