Views: 982
Number of votes: 4
Average rating:

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

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.

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.

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.