World is now on Opti ID! Learn more

Daniel Ovaska
Mar 13, 2018
  916
(0 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

Please login to comment.
Latest blogs
Make Global Assets Site- and Language-Aware at Indexing Time

I had a support case the other day with a question around search on global assets on a multisite. This is the result of that investigation. This co...

dada | Jun 26, 2025

The remote server returned an error: (400) Bad Request – when configuring Azure Storage for an older Optimizely CMS site

How to fix a strange issue that occurred when I moved editor-uploaded files for some old Optimizely CMS 11 solutions to Azure Storage.

Tomas Hensrud Gulla | Jun 26, 2025 |

Enable Opal AI for your Optimizely products

Learn how to enable Opal AI, and meet your infinite workforce.

Tomas Hensrud Gulla | Jun 25, 2025 |

Deploying to Optimizely Frontend Hosting: A Practical Guide

Optimizely Frontend Hosting is a cloud-based solution for deploying headless frontend applications - currently supporting only Next.js projects. It...

Szymon Uryga | Jun 25, 2025

World on Opti ID

We're excited to announce that world.optimizely.com is now integrated with Opti ID! What does this mean for you? New Users:  You can now log in wit...

Patrick Lam | Jun 22, 2025

Avoid Scandinavian Letters in File Names in Optimizely CMS

Discover how Scandinavian letters in file names can break media in Optimizely CMS—and learn a simple code fix to automatically sanitize uploads for...

Henning Sjørbotten | Jun 19, 2025 |