1
\$\begingroup\$

I have a password validation class that takes values from our database and depending on their values, checks if the password is valid. If it is not, I built an error message containing everything that is wrong with the entered password.

I want it to be easy to change when I add a validation attribute in the database.

Imports System.ComponentModel.DataAnnotations
Imports System.Globalization
''' <summary>
''' A custom ValidationAttribute for Internal Login password.
''' </summary>
Friend Class CustomPasswordValidatorAttribute
   Inherits ValidationAttribute

   Private Property PasswordAttrs As PasswordsAttributes
   Private Property InvalidPassReasonsMessage As New StringBuilder
   Private Property Password As String

   Public Sub New()
      MyBase.New()
      PasswordAttrs = GetPasswordsAttributes()
   End Sub

   Protected Overrides Function IsValid(value As Object, validationContext As ValidationContext) As ValidationResult
      SetCultureWithCookie()

      Password = If(value Is Nothing, String.Empty, value.ToString)
      InvalidPassReasonsMessage.Clear()
      IfConditionMetAddReason(IsInvalidMinLength(), String.Format(Resources.Account.PassMinLengthErr, PasswordAttrs.MinLength))
      IfConditionMetAddReason(IsInvalidMaxLength(), String.Format(Resources.Account.PassMaxLengthErr, PasswordAttrs.MaxLength))
      IfConditionMetAddReason(IsInvalidNeedSpecialCharacter(), Resources.Account.PassNeedNonLetterOrDigitErr)
      IfConditionMetAddReason(IsInvalidNeedDigit(), Resources.Account.PassNeedDigitErr)
      IfConditionMetAddReason(IsInvalidNeedLowercase(), Resources.Account.PassNeedLowerErr)
      IfConditionMetAddReason(IsInvalidNeedUppercase(), Resources.Account.PassNeedUpperErr)

      Return GetPasswordValidationResult(InvalidPassReasonsMessage)
   End Function

   Private Sub SetCultureWithCookie()
      Dim location = HttpContext.Current.Request.QueryString("location")
      If String.IsNullOrEmpty(location) Then location = "default"
      Dim webSettings As Collections.Generic.Dictionary(Of String, String) = ICE.Utility.IceApplication.instance.Location(location).WebSettings
      If Utility.GlobalUtility.LanguageCookie(location, webSettings) = Constants.GlobalConstant.Language.French Then
         CultureInfo.CurrentUICulture = New CultureInfo("fr")
         CultureInfo.CurrentCulture = New CultureInfo("fr")
      Else
         CultureInfo.CurrentUICulture = New CultureInfo("en")
         CultureInfo.CurrentCulture = New CultureInfo("en")
      End If
   End Sub

   Private Sub IfConditionMetAddReason(condition As Boolean, msg As String)
      If condition Then
         InvalidPassReasonsMessage.Append(msg)
      End If
   End Sub

   Private Function GetPasswordValidationResult(reason As StringBuilder) As ValidationResult
      If reason.Length = 0 Then
         Return ValidationResult.Success
      Else
         reason.Insert(0, Resources.Account.PassErr)
         Return New ValidationResult(reason.ToString)
      End If
   End Function

   Private Function IsInvalidMinLength() As Boolean
      Return String.IsNullOrEmpty(Password) OrElse Password.Length < PasswordAttrs.MinLength
   End Function
   Private Function IsInvalidMaxLength() As Boolean
      Return Password.Length > PasswordAttrs.MaxLength
   End Function

   Private Function IsInvalidNeedSpecialCharacter() As Boolean
      Return PasswordAttrs.NeedSpecialCharacter AndAlso Not ContainsSpecialChar(Password)
   End Function

   Private Function IsInvalidNeedDigit() As Boolean
      Return PasswordAttrs.NeedDigit AndAlso Not ContainsDigit(Password)
   End Function

   Private Function IsInvalidNeedLowercase() As Boolean
      Return PasswordAttrs.NeedLowercase AndAlso Not ContainsLowercase(Password)
   End Function

   Private Function IsInvalidNeedUppercase() As Boolean
      Return PasswordAttrs.NeedUppercase AndAlso Not ContainsUppercase(Password)
   End Function

   Private Function ContainsSpecialChar(s As String) As Boolean
      Return s.Any(Function(c) (Not Char.IsLetterOrDigit(c) AndAlso Not Char.IsWhiteSpace(c)))
   End Function

   Private Function ContainsDigit(s As String) As Boolean
      Return s.Any(Function(c) (Char.IsDigit(c)))
   End Function

   Private Function ContainsLowercase(s As String) As Boolean
      Return s.Any(Function(c) (Char.IsLower(c)))
   End Function

   Private Function ContainsUppercase(s As String) As Boolean
      Return s.Any(Function(c) (Char.IsUpper(c)))
   End Function

   Private Function GetPasswordsAttributes() As PasswordsAttributes
      Dim adoTable As New System.Data.DataTable
      ICE.ContextLess.FillDataTable(adoTable,
                  "SELECT *
                  FROM   LOGINPARAMETERS_T")

      If adoTable.Rows.Count = 1 Then
         Return New PasswordsAttributes(adoTable.Rows(0))
      End If
      Return Nothing
   End Function

   Private Class PasswordsAttributes
      Public Property MinLength As Integer
      Public Property MaxLength As Integer
      Public Property NeedSpecialCharacter As Boolean
      Public Property NeedDigit As Boolean
      Public Property NeedLowercase As Boolean
      Public Property NeedUppercase As Boolean

      Public Sub New()
      End Sub

      Public Sub New(row As DataRow)
         If Not IsDBNull(row("PWD_MIN_LEN")) Then Me.MinLength = CInt(row("PWD_MIN_LEN"))
         If Not IsDBNull(row("PWD_MAX_LEN")) Then Me.MaxLength = CInt(row("PWD_MAX_LEN"))
         If Not IsDBNull(row("PWD_REQ_SP_CHAR_IND")) Then Me.NeedSpecialCharacter = CBool(row("PWD_REQ_SP_CHAR_IND"))
         If Not IsDBNull(row("PWD_REQ_DIG_IND")) Then Me.NeedDigit = CBool(row("PWD_REQ_DIG_IND"))
         If Not IsDBNull(row("PWD_REQ_LOCS_CHAR_IND")) Then Me.NeedLowercase = CBool(row("PWD_REQ_LOCS_CHAR_IND"))
         If Not IsDBNull(row("PWD_REQ_UPCS_CHAR_IND")) Then Me.NeedUppercase = CBool(row("PWD_REQ_UPCS_CHAR_IND"))
      End Sub
   End Class
End Class

How can this be improved? Thank you.

\$\endgroup\$
  • \$\begingroup\$ Just to confirm, this is a database that is storing the passwords in the clear? \$\endgroup\$ – AJD Dec 11 '19 at 5:34
  • \$\begingroup\$ @AJD not at all. The password in clear comes from the parameter in IsValid. And it is stored encrypted but not it this part of the code. This is only the validator. \$\endgroup\$ – Word Rearranger Dec 11 '19 at 13:48
  • \$\begingroup\$ Move away from the archaic, currently-non-standard complexity rules for passwords. Reference the NIST current standards at pages.nist.gov/800-63-3/sp800-63b.html Specifically, look at section 5.1.1.1. They use "memorized secret" for password. \$\endgroup\$ – HardCode Jan 22 at 19:34
  • \$\begingroup\$ @HardCode I understand your point bu clients want what they want and we figured it is better to have more than less. By default only the min length and max length will be set and the other ones won't be used. \$\endgroup\$ – Word Rearranger Jan 22 at 19:41
  • \$\begingroup\$ @Edit fair enough, customers pay the bills. It looks like you're building out what FluentValidation does. Have you looked into it before? \$\endgroup\$ – HardCode Jan 22 at 19:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.