Copyright | (c) Hiroto Shioi 2020; Felix Paulusma 2020 |
---|---|
License | BSD-style (see LICENSE file) |
Maintainer | cdep.illabout@gmail.com |
Stability | experimental |
Portability | POSIX |
Safe Haskell | Safe-Inferred |
Language | Haskell2010 |
Password Validation
It is common for passwords to have a set of requirements. The most obvious requirement being a minimum length, but another common requirement is for the password to at least include a certain amount of characters of a certain category, like uppercase and lowercase alphabetic characters, numbers and/or other special characters. Though, nowadays, this last type of requirement is discouraged by security experts.
This module provides an API which enables you to set up your own
PasswordPolicy
to validate the format of Password
s.
Recommendations by the NIST
For policy recommendations and more, look to the following publication by the National Institute of Standards and Technology (especially the addendum): https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf
A short summary:
- Enforcing inclusion of specific character types (like special characters, numbers, lowercase and uppercase letters) actually makes passwords less secure.
- The length of a password is the most important factor, so let users make their passwords as lengthy as they want, within reason. (keep in mind some algorithms have length limitations, like bcrypt's 72 character limit)
- Do allow spaces so users can use sentences for passwords.
- Showing the "strength" of user's passwords is advised. A good algorithm to use is zxcvbn.
- The best way to mitigate online attacks is to limit the rate of login attempts.
Password Policies
The most important part is to have a valid and robust PasswordPolicy
.
A defaultPasswordPolicy_
is provided to quickly set up a NIST recommended
validation of passwords, but you can also adjust it, or just create your
own.
Just remember that a PasswordPolicy
must be validated first to make
sure it is actually a ValidPasswordPolicy
. Otherwise, you'd never be
able to validate any given Password
s.
Example usage
So let's say we're fine with the default policy, which requires the password to be between 8-64 characters, and doesn't enforce any specific character category usage, then our function would look like the following:
myValidateFunc ::Password
-> Bool myValidateFunc =isValidPassword
defaultPasswordPolicy_
Custom policies
But, for example, if you'd like to enforce that a Password
includes
at least one special character, and be at least 12 characters long,
you'll have to make your own PasswordPolicy
.
customPolicy ::PasswordPolicy
customPolicy =defaultPasswordPolicy
{ minimumLength = 12 , specialChars = 1 }
This custom policy will then have to be validated first, so it can be
used to validate Password
s further on.
Template Haskell
The easiest way to validate a custom PasswordPolicy
is by using a
Template Haskell splice.
Just turn on the {-# LANGUAGE TemplateHaskell #-}
pragma, pass your
policy to validatePasswordPolicyTH
, surround it by $(...)
and if
it compiles it will be a ValidPasswordPolicy
.
{-# LANGUAGE TemplateHaskell #-} customValidPolicy ::ValidPasswordPolicy
customValidPolicy = $(validatePasswordPolicyTH
customPolicy)
NB: any custom CharSetPredicate
will be ignored by validatePasswordPolicyTH
and replaced with the defaultCharSetPredicate
.
So if you want to use your own CharSetPredicate
, you won't be able
to validate your policy using validatePasswordPolicyTH
. Most users,
however, will find defaultCharSetPredicate
to be sufficient.
At runtime
Another way of validating your custom policy is validatePasswordPolicy
.
In an application, this might be implemented in the following way.
main :: IO () main = case (validatePasswordPolicy
customPolicy) of Left reasons -> error $ show reasons Right validPolicy -> app `runReaderT` validPolicy customValidateFunc ::Password
-> ReaderTValidPasswordPolicy
IO Bool customValidateFunc pwd = do policy <- ask return $isValidPassword
policy pwd
Let's get dangerous
Or, if you like living on the edge, you could also just match on Right
.
I hope you're certain your policy is valid, though. So please have at least
a unit test to verify that passing your PasswordPolicy
to
validatePasswordPolicy
actually returns a Right
.
Right validPolicy =validatePasswordPolicy
customPolicy customValidateFunc ::Password
-> Bool customValidateFunc =isValidPassword
validPolicy
Synopsis
- validatePassword :: ValidPasswordPolicy -> Password -> ValidationResult
- isValidPassword :: ValidPasswordPolicy -> Password -> Bool
- data ValidationResult
- validatePasswordPolicy :: PasswordPolicy -> Either [InvalidPolicyReason] ValidPasswordPolicy
- validatePasswordPolicyTH :: PasswordPolicy -> Q Exp
- data PasswordPolicy = PasswordPolicy {
- minimumLength :: !Int
- maximumLength :: !Int
- uppercaseChars :: !Int
- lowercaseChars :: !Int
- specialChars :: !Int
- digitChars :: !Int
- charSetPredicate :: CharSetPredicate
- data ValidPasswordPolicy
- fromValidPasswordPolicy :: ValidPasswordPolicy -> PasswordPolicy
- defaultPasswordPolicy :: PasswordPolicy
- defaultPasswordPolicy_ :: ValidPasswordPolicy
- newtype CharSetPredicate = CharSetPredicate {
- getCharSetPredicate :: Char -> Bool
- defaultCharSetPredicate :: CharSetPredicate
- data InvalidReason
- data InvalidPolicyReason
- data CharacterCategory
- type MinimumLength = Int
- type MaximumLength = Int
- type ProvidedLength = Int
- type MinimumAmount = Int
- type ProvidedAmount = Int
- defaultCharSet :: String
- validateCharSetPredicate :: PasswordPolicy -> [InvalidPolicyReason]
- categoryToPredicate :: CharacterCategory -> Char -> Bool
- isSpecial :: Char -> Bool
- allButCSP :: PasswordPolicy -> [Int]
Validating passwords
The main function of this module is probably isValidPassword
,
as it is simple and straightforward.
Though if you'd want to know why a Password
failed to validate,
because you'd maybe like to communicate those InvalidReason
s
back to the user, validatePassword
is here to help you out.
validatePassword :: ValidPasswordPolicy -> Password -> ValidationResult Source #
Checks if a given Password
adheres to the provided ValidPasswordPolicy
.
In case of an invalid password, returns the reasons why it wasn't valid.
>>>
let pass = mkPassword "This_Is_Valid_Password1234"
>>>
validatePassword defaultPasswordPolicy_ pass
ValidPassword
Since: 2.1.0.0
isValidPassword :: ValidPasswordPolicy -> Password -> Bool Source #
This function is equivalent to:
validatePassword
policy password ==ValidPassword
>>>
let pass = mkPassword "This_Is_Valid_PassWord1234"
>>>
isValidPassword defaultPasswordPolicy_ pass
True
Since: 2.1.0.0
data ValidationResult Source #
Result of validating a Password
.
Since: 2.1.0.0
Instances
Show ValidationResult Source # | |
Defined in Data.Password.Validate showsPrec :: Int -> ValidationResult -> ShowS # show :: ValidationResult -> String # showList :: [ValidationResult] -> ShowS # | |
Eq ValidationResult Source # | |
Defined in Data.Password.Validate (==) :: ValidationResult -> ValidationResult -> Bool # (/=) :: ValidationResult -> ValidationResult -> Bool # |
Password Policy
A PasswordPolicy
has to be validated before it can be used to validate a
Password
.
This is done using validatePasswordPolicy
or validatePasswordPolicyTH
.
Next to the obvious lower and upper bounds for the length of a Password
,
a PasswordPolicy
can dictate how many lowercase letters, uppercase letters,
digits and/or special characters are minimally required to be used in the
Password
to be considered a valid Password
.
An observant user might have also seen that a PasswordPolicy
includes a
CharSetPredicate
. Very few users will want to change this from the
defaultCharSetPredicate
, since this includes all non-control ASCII characters.
If, for some reason, you'd like to accept more characters (e.g. é, ø, か, 事)
or maybe you want to only allow alpha-numeric characters, charSetPredicate
is
the place to do so.
validatePasswordPolicy :: PasswordPolicy -> Either [InvalidPolicyReason] ValidPasswordPolicy Source #
Verifies that a PasswordPolicy
is valid and converts it into a ValidPasswordPolicy
.
>>>
validatePasswordPolicy defaultPasswordPolicy
Right (...)
Since: 2.1.0.0
validatePasswordPolicyTH :: PasswordPolicy -> Q Exp Source #
Template Haskell validation function for PasswordPolicy
s.
{-# LANGUAGE TemplateHaskell #-} myPolicy ::PasswordPolicy
myPolicy =defaultPasswordPolicy
{ specialChars = 1 } myValidPolicy ::ValidPasswordPolicy
myValidPolicy = $(validatePasswordPolicyTH
myPolicy)
For technical reasons, the charSetPredicate
field is ignored and the
defaultCharSetPredicate
is used. If, for any reason, you do need to use a
custom CharSetPredicate
, please use validatePasswordPolicy
and either handle
the failure case at runtime and/or use a unit test to make sure your policy is valid.
Since: 2.1.0.0
data PasswordPolicy Source #
Set of policies used to validate a Password
.
When defining your own PasswordPolicy
, please keep in mind that:
- The value of
maximumLength
must be bigger than 0 - The value of
maximumLength
must be bigger thanminimumLength
- If any other field has a negative value (e.g.
lowercaseChars
), it will be defaulted to 0 - The total sum of all character category values (i.e. all fields ending in
-Chars
) must not be larger than the value ofmaximumLength
. - The provided
CharSetPredicate
needs to allow at least one of the characters in the categories which require more than 0 characters. (e.g. iflowercaseChars
is > 0, thecharSetPredicate
must allow at least one of the characters in['a'..'z']
)
or else the validation functions will return one or more InvalidPolicyReason
s.
If you're unsure of what to do, please use the default: defaultPasswordPolicy_
Since: 2.1.0.0
PasswordPolicy | |
|
Instances
Show PasswordPolicy Source # | |
Defined in Data.Password.Validate showsPrec :: Int -> PasswordPolicy -> ShowS # show :: PasswordPolicy -> String # showList :: [PasswordPolicy] -> ShowS # | |
Eq PasswordPolicy Source # | N.B. This will not check equality on the |
Defined in Data.Password.Validate (==) :: PasswordPolicy -> PasswordPolicy -> Bool # (/=) :: PasswordPolicy -> PasswordPolicy -> Bool # | |
Ord PasswordPolicy Source # | N.B. This will not check order on the |
Defined in Data.Password.Validate compare :: PasswordPolicy -> PasswordPolicy -> Ordering # (<) :: PasswordPolicy -> PasswordPolicy -> Bool # (<=) :: PasswordPolicy -> PasswordPolicy -> Bool # (>) :: PasswordPolicy -> PasswordPolicy -> Bool # (>=) :: PasswordPolicy -> PasswordPolicy -> Bool # max :: PasswordPolicy -> PasswordPolicy -> PasswordPolicy # min :: PasswordPolicy -> PasswordPolicy -> PasswordPolicy # |
data ValidPasswordPolicy Source #
A PasswordPolicy
that has been checked to be valid
Since: 2.1.0.0
Instances
Show ValidPasswordPolicy Source # | |
Defined in Data.Password.Validate showsPrec :: Int -> ValidPasswordPolicy -> ShowS # show :: ValidPasswordPolicy -> String # showList :: [ValidPasswordPolicy] -> ShowS # | |
Eq ValidPasswordPolicy Source # | |
Defined in Data.Password.Validate (==) :: ValidPasswordPolicy -> ValidPasswordPolicy -> Bool # (/=) :: ValidPasswordPolicy -> ValidPasswordPolicy -> Bool # | |
Ord ValidPasswordPolicy Source # | |
Defined in Data.Password.Validate compare :: ValidPasswordPolicy -> ValidPasswordPolicy -> Ordering # (<) :: ValidPasswordPolicy -> ValidPasswordPolicy -> Bool # (<=) :: ValidPasswordPolicy -> ValidPasswordPolicy -> Bool # (>) :: ValidPasswordPolicy -> ValidPasswordPolicy -> Bool # (>=) :: ValidPasswordPolicy -> ValidPasswordPolicy -> Bool # max :: ValidPasswordPolicy -> ValidPasswordPolicy -> ValidPasswordPolicy # min :: ValidPasswordPolicy -> ValidPasswordPolicy -> ValidPasswordPolicy # |
fromValidPasswordPolicy :: ValidPasswordPolicy -> PasswordPolicy Source #
In case you'd want to retrieve the PasswordPolicy
from the ValidPasswordPolicy
Since: 2.1.0.0
defaultPasswordPolicy :: PasswordPolicy Source #
Default value for the PasswordPolicy
.
Enforces that a password must be between 8-64 characters long, though can easily be adjusted by using record update syntax:
myPolicy = defaultPasswordPolicy{ minimumLength = 12 }
Do note that this being a default policy doesn't make it a good
enough policy in every situation. The most important field, minimumLength
,
has 8 characters as the default, because it is the bare minimum for some
sense of security. The longer the password, the more difficult it will be
to guess or brute-force, so a minimum of 12 or 16 would be advised in
a production setting.
This policy on it's own is guaranteed to be valid. Any changes made to
it might result in validatePasswordPolicy
returning one or more
InvalidPolicyReason
s.
>>>
defaultPasswordPolicy
PasswordPolicy {minimumLength = 8, maximumLength = 64, uppercaseChars = 0, lowercaseChars = 0, specialChars = 0, digitChars = 0, charSetPredicate = <FUNCTION>}
Since: 2.1.0.0
defaultPasswordPolicy_ :: ValidPasswordPolicy Source #
Unchangeable defaultPasswordPolicy
, but guaranteed to be valid.
Since: 2.1.0.0
newtype CharSetPredicate Source #
Predicate which defines the characters that can be used for a password.
Since: 2.1.0.0
defaultCharSetPredicate :: CharSetPredicate Source #
The default character set consists of uppercase and lowercase letters, numbers,
and special characters from the ASCII
character set.
(i.e. everything from the ASCII
set except the control characters)
Since: 2.1.0.0
data InvalidReason Source #
Possible reasons for a Password
to be invalid.
Since: 2.1.0.0
PasswordTooShort !MinimumLength !ProvidedLength | Length of |
PasswordTooLong !MaximumLength !ProvidedLength | Length of |
NotEnoughReqChars !CharacterCategory !MinimumAmount !ProvidedAmount |
|
InvalidCharacters !Text |
|
Instances
Show InvalidReason Source # | |
Defined in Data.Password.Validate showsPrec :: Int -> InvalidReason -> ShowS # show :: InvalidReason -> String # showList :: [InvalidReason] -> ShowS # | |
Eq InvalidReason Source # | |
Defined in Data.Password.Validate (==) :: InvalidReason -> InvalidReason -> Bool # (/=) :: InvalidReason -> InvalidReason -> Bool # | |
Ord InvalidReason Source # | |
Defined in Data.Password.Validate compare :: InvalidReason -> InvalidReason -> Ordering # (<) :: InvalidReason -> InvalidReason -> Bool # (<=) :: InvalidReason -> InvalidReason -> Bool # (>) :: InvalidReason -> InvalidReason -> Bool # (>=) :: InvalidReason -> InvalidReason -> Bool # max :: InvalidReason -> InvalidReason -> InvalidReason # min :: InvalidReason -> InvalidReason -> InvalidReason # |
data InvalidPolicyReason Source #
Possible reasons for a PasswordPolicy
to be invalid
Since: 2.1.0.0
InvalidLength !MinimumLength !MaximumLength | Value of InvalidLength minimumLength maximumLength |
MaxLengthBelowZero !MaximumLength | Value of MaxLengthBelowZero maximumLength |
CategoryAmountsAboveMaxLength !MaximumLength !Int | The total of the character category amount requirements are
higher than the maximum length of the password. (i.e. the CategoryAmountsAboveMaxLength maximumLength totalRequiredChars |
InvalidCharSetPredicate !CharacterCategory !MinimumAmount |
|
Instances
Show InvalidPolicyReason Source # | |
Defined in Data.Password.Validate showsPrec :: Int -> InvalidPolicyReason -> ShowS # show :: InvalidPolicyReason -> String # showList :: [InvalidPolicyReason] -> ShowS # | |
Eq InvalidPolicyReason Source # | |
Defined in Data.Password.Validate (==) :: InvalidPolicyReason -> InvalidPolicyReason -> Bool # (/=) :: InvalidPolicyReason -> InvalidPolicyReason -> Bool # | |
Ord InvalidPolicyReason Source # | |
Defined in Data.Password.Validate compare :: InvalidPolicyReason -> InvalidPolicyReason -> Ordering # (<) :: InvalidPolicyReason -> InvalidPolicyReason -> Bool # (<=) :: InvalidPolicyReason -> InvalidPolicyReason -> Bool # (>) :: InvalidPolicyReason -> InvalidPolicyReason -> Bool # (>=) :: InvalidPolicyReason -> InvalidPolicyReason -> Bool # max :: InvalidPolicyReason -> InvalidPolicyReason -> InvalidPolicyReason # min :: InvalidPolicyReason -> InvalidPolicyReason -> InvalidPolicyReason # |
data CharacterCategory Source #
Character categories
Since: 2.1.0.0
Instances
Show CharacterCategory Source # | |
Defined in Data.Password.Validate showsPrec :: Int -> CharacterCategory -> ShowS # show :: CharacterCategory -> String # showList :: [CharacterCategory] -> ShowS # | |
Eq CharacterCategory Source # | |
Defined in Data.Password.Validate (==) :: CharacterCategory -> CharacterCategory -> Bool # (/=) :: CharacterCategory -> CharacterCategory -> Bool # | |
Ord CharacterCategory Source # | |
Defined in Data.Password.Validate compare :: CharacterCategory -> CharacterCategory -> Ordering # (<) :: CharacterCategory -> CharacterCategory -> Bool # (<=) :: CharacterCategory -> CharacterCategory -> Bool # (>) :: CharacterCategory -> CharacterCategory -> Bool # (>=) :: CharacterCategory -> CharacterCategory -> Bool # max :: CharacterCategory -> CharacterCategory -> CharacterCategory # min :: CharacterCategory -> CharacterCategory -> CharacterCategory # |
type MinimumLength = Int Source #
type MaximumLength = Int Source #
type ProvidedLength = Int Source #
type MinimumAmount = Int Source #
type ProvidedAmount = Int Source #
For internal use
These are used in the test suite. You should not need these.
These are basically internal functions and as such have NO guarantee (NONE) to be consistent between releases.
defaultCharSet :: String Source #
Default character set
Should be all non-control characters in the ASCII character set.
validateCharSetPredicate :: PasswordPolicy -> [InvalidPolicyReason] Source #
Validate CharSetPredicate
to return True
on at least one of the characters
that is required.
For instance, if PasswordPolicy
states that the password requires at least
one uppercase letter, then CharSetPredicate
should return True on at least
one uppercase letter.
categoryToPredicate :: CharacterCategory -> Char -> Bool Source #
Convert a CharacterCategory
into its associated predicate function
isSpecial :: Char -> Bool Source #
Check if given Char
is a special character.
(i.e. any non-alphanumeric non-control ASCII character)
allButCSP :: PasswordPolicy -> [Int] Source #
All Int
fields of the PasswordPolicy
in a row