Try our conversational search powered by Generative AI!

Daniel Ovaska
Mar 13, 2018
  2833
(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
From Procrastination to Proficiency: Navigating Your Journey to Web Experimentation Certification

Hey there, Optimizely enthusiasts!   Join me in celebrating a milestone – I'm officially a certified web experimentation expert! It's an exhilarati...

Silvio Pacitto | May 17, 2024

GPT-4o Now Available for Optimizely via the AI-Assistant plugin!

I am excited to announce that GPT-4o is now available for Optimizely users through the Epicweb AI-Assistant integration. This means you can leverag...

Luc Gosso (MVP) | May 17, 2024 | Syndicated blog

The downside of being too fast

Today when I was tracking down some changes, I came across this commit comment Who wrote this? Me, almost 5 years ago. I did have a chuckle in my...

Quan Mai | May 17, 2024 | Syndicated blog

Optimizely Forms: Safeguarding Your Data

With the rise of cyber threats and privacy concerns, safeguarding sensitive information has become a top priority for businesses across all...

K Khan | May 16, 2024

The Experimentation Process

This blog is part of the series -   Unlocking the Power of Experimentation: A Marketer's Insight. Welcome back, to another insightful journey into...

Holly Quilter | May 16, 2024

Azure AI Language – Sentiment Analysis in Optimizely CMS

In the following article, I showcase how sentiment analysis, which is part of the Azure AI Language service, can be used to detect the sentiment of...

Anil Patel | May 15, 2024 | Syndicated blog