Docs > User Areas > Configuration >

Password Policies

When a user password is changed, Cofoundry will validate the password against a set of criteria. The default password policy includes the following criteria:

  • Must be at least 10 characters.
  • Must be 300 characters or less.
  • Must have at least 5 unique characters.
  • Must not be the same as the users current password.
  • Must not be the same as the users email or username.
  • Must not be a sequence of numbers or characters e.g. "abcdefgh" or "87654321".

Cofoundry offers various ways of customizing a password policy, from applying basic settings globally to creating bespoke validators that can be applied to specific user areas.

Global password settings

The easiest way to modify the default policy is to add configuration settings to your app.config file. Modifying these settings will change the default policy for all user areas, including the Cofoundry admin panel user area. The following basic settings can be modified:

  • Cofoundry:Identity:Password:MinLength: The minimum length of a password. Defaults to 10 and anything less is not recommended. Must be between 6 and 2048 characters.
  • Cofoundry:Identity:Password:MaxLength: The maximum length of a password. Defaults to 300 characters and must be between 6 and 2048 characters.
  • Cofoundry:Identity:Password:MinUniqueCharacters: The number of unique characters required in a password. This is to prevent passwords like "aabbccdd". Defaults to 5 unique characters.

Example:

{
  "Cofoundry": {
    "Identity:Password": {
        "MinLength": 10,
        "MaxLength": 300,
        "MinUniqueCharacters": 5
    }
  }
}

Customizing a user area policy via IUserAreaDefinition

If you need to modify the basic password policy settings for a specific user area, you can do this in the ConfigureOptions(UserAreaOptions) interface method in your definition class. In this example we increase the minimum number of unique characters required to 6:

using Cofoundry.Domain;

public class MemberUserArea : IUserAreaDefinition
{
    public const string Code = "MEM";

    public string UserAreaCode => Code;

    public string Name => "Member";

    public bool AllowPasswordSignIn => true;

    // other properties removed for brevity

    public void ConfigureOptions(UserAreaOptions options)
    {
        options.Password.MinUniqueCharacters = 6;
    }
}

Customizing a user area policy via IPasswordPolicyConfiguration

For more control over the password policy for an individual user area you can implement IPasswordPolicyConfiguration<TUserAreaDefinition>, which provides complete control over the password policy builder. Your implementation will be registered automatically by the Cofoundry DI system.

In the following example, we'll upgrade the password policy of the built-in CofoundryAdminUserArea by increasing the minimum length, building on top of the default configuration:

using Cofoundry.Domain;

public class AdminPasswordPolicyConfiguration : IPasswordPolicyConfiguration<CofoundryAdminUserArea>
{
    public void Configure(IPasswordPolicyBuilder builder)
    {
        builder.UseDefaults(c => c.MinLength = 12);
    }
}

So far we're only altering simple properties and relying on the defaults, but let's take it a step further and replace the UseDefaults() method to illustrate how we can add validators to the policy manually:

public class AdminPasswordPolicyConfiguration : IPasswordPolicyConfiguration<CofoundryAdminUserArea>
{
    public void Configure(IPasswordPolicyBuilder builder)
    {
        builder
            .SetDescription($"Passwords must be between 12 and 300 characters.")
            .ValidateMinLength(12)
            .ValidateMaxLength(300)
            .ValidateMinUniqueCharacters(5)
            .ValidateNotCurrentPassword()
            .ValidateNotPersonalData()
            .ValidateNotSequential()
            ;
    }
}

Changing the description

In the above example we added a description using builder.SetDescription(string). The description is an optional property that can be used to succinctly describe the policy, and it is used in the admin panel to help guide users when creating a new password. If you're altering the password policy for the Cofoundry admin user area then you may want to also update the description, however it's not required for other custom user areas.

If do want to use the dynamically generated description yourself, it can be retrieved through IAdvancedContentRepository:

var policyDescription = await _advancedContentRepository
    .UserAreas()
    .PasswordPolicies()
    .GetByCode(MemberUserArea.Code)
    .AsDescription()
    .ExecuteAsync();

The policy description also contains a collection of criteria statements from each of the validators, which is useful is you need a more thorough description of the policy.

Configuring multiple user areas

To apply your IPasswordPolicyConfiguration to multiple user areas, simply implement the interface for each definition type:

public class ExamplePasswordPolicyConfiguration 
    : IPasswordPolicyConfiguration<CofoundryAdminUserArea>
    , IPasswordPolicyConfiguration<MemberUserArea>
{
    public void Configure(IPasswordPolicyBuilder builder)
    {
        // Add config here
    }
}

Creating custom password validators

A basic password validator

Validators can be either synchronous or asynchronous, but let's start with a simple example of a synchronous validator that requires a password to contain a digit.

By default Cofoundry doesn't enforce any "character-composition" requirements, as this goes against current guidance (see OWASP Authentication Cheat Sheet), however if the requirement is mandated by a client we can still enforce it by implementing a custom validator:

public class RequireDigitNewPasswordValidator : INewPasswordValidator
{
    public string Criteria => "Must contain a digit.";

    public ValidationError Validate(INewPasswordValidationContext context)
    {
        ValidationError result = null;

        if (!context.Password.Any(c => Char.IsDigit(c)))
        {
            result = new ValidationError("Password must contain a digit", context.PropertyName);
        }

        return result;
    }
}

The Validate(INewPasswordValidationContext) method should return a ValidationError if validation was unsuccessful, otherwise null should be returned to indicate success.

We can now add the validator to our IPasswordPolicyConfiguration implementation:

using Cofoundry.Core.Validation;
using Cofoundry.Domain;

public class MemberPasswordPolicyConfiguration  : IPasswordPolicyConfiguration<MemberUserArea>
{
    public void Configure(IPasswordPolicyBuilder builder)
    {
        builder
            .UseDefaults()
            .AddValidator<RequireDigitNewPasswordValidator>();
    }
}

An async password validator

In some cases you may need to run async code to validate a password e.g. to query a database or read a file. In the following example we query a fictional repository of forbidden passwords, and return an error if the password matches one of the values.

This example also highlights that validators supports DI. Our DI system automatically scans for validators and registers them with the DI container.

using Cofoundry.Core.Validation;
using Cofoundry.Domain;

public class NotInBlocklistNewPasswordValidator : IAsyncNewPasswordValidator
{
    private readonly BadPasswordRepository _badPasswordRepository;

    public NotInBlocklistNewPasswordValidator(BadPasswordRepository badPasswordRepository)
    {
        _badPasswordRepository = badPasswordRepository;
    }

    public string Criteria => $"Must not appear in the block-list.";

    public async Task<ValidationError> ValidateAsync(INewPasswordValidationContext context)
    {
        ValidationError result = null;

        var blocklist = await _badPasswordRepository.GetAll();
            
        if (blocklist.Any(b => context.Password.Equals(b, StringComparison.OrdinalIgnoreCase)))
        {
            result = new ValidationError("Password is invalid because it appears in the block-list", context.PropertyName);
        }

        return result;
    }
}

Note that synchronous validators always run first, and async validators will only be invoked if no other errors are found.

Configuring a password validator

If you want to make your validator re-usable with different user areas or websites with different configurations you can add an INewPasswordValidatorWithConfig<TOptions> implementation to your validator.

using Cofoundry.Core.Validation;
using Cofoundry.Domain;

public class RequireDigitNewPasswordValidator 
    : INewPasswordValidator
    , INewPasswordValidatorWithConfig<int>
{
    public string Criteria => $"Must contain at least {NumberOfDigitsRequired} digits.";

    public int NumberOfDigitsRequired { get; private set; }

    public void Configure(int numberOfDigitsRequired)
    {
        NumberOfDigitsRequired = numberOfDigitsRequired;
    }

    public ValidationError Validate(INewPasswordValidationContext context)
    {
        ValidationError result = null;

        var numberOfDigits = context.Password.Count(c => Char.IsDigit(c));
        if (numberOfDigits < NumberOfDigitsRequired)
        {
            result = new ValidationError($"Password must contain at least {NumberOfDigitsRequired} digits", context.PropertyName);
        }

        return result;
    }
}

We can now configure the validator in our IPasswordPolicyConfiguration implementation:

using Cofoundry.Core.Validation;
using Cofoundry.Domain;

public class MemberPasswordPolicyConfiguration  : IPasswordPolicyConfiguration<MemberUserArea>
{
    public void Configure(IPasswordPolicyBuilder builder)
    {
        builder
            .UseDefaults()
            .AddValidatorWithConfig<RequireDigitNewPasswordValidator, int>(6);
    }
}

If you're making a reusable validator for different projects, you may want to add an extension method to make configuration a bit more intuitive:

using Cofoundry.Domain;

public static class IPasswordPolicyBuilderExtensions
{
    public static IPasswordPolicyBuilder ValidateMinDigits(this IPasswordPolicyBuilder builder, int numberOfDigitsRequired)
    {
        return builder.AddValidatorWithConfig<RequireDigitNewPasswordValidator, int>(6);
    }
}

Our configuration now looks like this:

public class MemberPasswordPolicyConfiguration  : IPasswordPolicyConfiguration<MemberUserArea>
{
    public void Configure(IPasswordPolicyBuilder builder)
    {
        builder
            .UseDefaults()
            .ValidateMinDigits(6);
    }
}