loading...

Daily Challenge #224 - Password Validator

thepracticaldev profile image dev.to staff ・1 min read

Write a quick password validator to make sure that the people that visit your site use appropriate passwords.

Respond with "VALID" if the string meets the requirements or "INVALID" if it does not.

Requirements:
More than 3 characters but less than 20.
Must contain only alphanumeric characters.
Must contain letters and numbers.

Examples
'Username123!' => INVALID
'123' => INVALID
'Username123' => VALID

Tests
'Username'
'IsThisPasswordTooLong'
'DEVCommunity'

Good luck!


This challenge comes from jhoffner on CodeWars. Thank you to CodeWars, who has licensed redistribution of this challenge under the 2-Clause BSD License!

Want to propose a challenge idea for a future post? Email yo+challenge@dev.to with your suggestions!

Posted on by:

thepracticaldev profile

dev.to staff

@thepracticaldev

The hardworking team behind dev.to ❤️

Discussion

markdown guide
 

Please, please, please... do not set maximum lengths on passwords. [1]

It's 2020, you should be hashing [2] your passwords (so the output is constant length no matter what the input). Password strength is based on two things: entropy (randomness) and length. Since people are usually bad at entropy, let them enter long passwords or passphrases if they want to. Also it sucks for people with password managers to have to generate custom rules. Accept any character, even spaces (you're hashing the password, so you can even accept ';-- for that matter), for passphrases.

[1] If you really want to set one to avoid 1GiB passwords, put it at 256 characters, so that password managers can go wild.
[2] Yes, I mean hashing + salting, don't roll it yourself and use bcrypt.

I know this is a training exercise, but cargo cult security is real.

 

Nice idea but it's not always your choice.

Case in point, you're advocating using bcrypt when bcrypt itself can only support 72 characters maximum, anything larger is truncated.

 

Maybe archaic systems, and also, shorter lengths are easier to make test cases that are easy for inspection. Most programming challenges I know are usually short and sweet (unless we're dealing with huge data deliberately).

 

Python solution

passwordValidator = lambda password : "VALID" if((3 < len(password) < 20) and password.isalnum() and any(map(str.isdigit, password)) and any(map(str.isalpha, password))) else "INVALID"

Here is the detailed version of above,

def passwordValidator(password : str) -> str:
    if(3 < len(password) < 20):
        if(password.isalnum()):
            if(any(map(str.isdigit, password)) and any(map(str.isalpha, password))):
                return "VALID"
    return "INVALID"

Output,

print(passwordValidator('Username123!')) # output -> INVALID
print(passwordValidator('123')) # output -> INVALID
print(passwordValidator('Username123')) # output -> VALID
print(passwordValidator('Username')) # output -> INVALID
print(passwordValidator('IsThisPasswordTooLong')) # output -> INVALID
print(passwordValidator('DEVCommunity')) # output -> INVALID
 

A little more readable example that is explicit about the lazy evaluation (chaining and)

def validatePassword(password: str) -> str:
    if (3 < len(password) < 20
            and password.isalnum()
            and any(map(str.isdigit, password))
            and any(map(str.isalpha, password))):
        return 'VALID'
    return 'INVALID'

assert validatePassword('Username123!') == 'INVALID'
assert validatePassword('123') == 'INVALID'
assert validatePassword('Username123') == 'VALID'

assert validatePassword('Username') == 'INVALID'
assert validatePassword('IsThisPasswordTooLong') == 'INVALID'
assert validatePassword('DEVCommunity') == 'INVALID'
 

F#, may have had a bit too much fun with this challenge and built a mini validation framework that aggregates errors:

module DailyChallenge

open System

type Password =
    | Valid
    | Invalid of string list

let length min max (s : string) =
    if s.Length >= min && s.Length <= max then
        Ok()
    else
        Error
            (sprintf "Password must be between %d and %d characters" min max)

let alphaNumOnly (s : string) =
    if Seq.forall (fun c -> Char.IsLetter(c) || Char.IsNumber(c)) s
    then Ok()
    else Error "Password can only contain alphanumeric characters"

let lettersAndNumbers (s : string) =
    match Seq.tryFind Char.IsLetter s, Seq.tryFind Char.IsNumber s with
    | Some _, Some _ -> Ok()
    | _ -> Error "Password needs to contain letters and numbers"

let runValidations validations (s : string) =
    (Ok(), validations)
    ||> List.fold (fun res f ->
            match res, (f s) with
            | Ok _, Ok _ -> Ok()
            | Ok _, Error e -> Error [ e ]
            | Error es, Ok _ -> Error es
            | Error es, Error e -> Error(e :: es))

let validate (s : string) =
    runValidations
        [ length 3 20
          alphaNumOnly
          lettersAndNumbers ] s
    |> function
    | Ok() -> Valid
    | Error es -> Invalid es

Tests:

module DailyChallengeTests

open FsUnit.Xunit
open Xunit
open DailyChallenge

[<Fact>]
let ``invalid character``() =
    validate "Username123!"
    |> should equal
           (Invalid [ "Password can only contain alphanumeric characters" ])

[<Fact>]
let ``no letters``() =
    validate "123"
    |> should equal
           (Invalid [ "Password needs to contain letters and numbers" ])

[<Fact>]
let ``too short``() =
    validate "a1"
    |> should equal
           (Invalid [ "Password must be between 3 and 20 characters" ])

[<Fact>]
let ``too long``() =
    validate "a1a2a3a4a5a6a7a8a9a10"
    |> should equal
           (Invalid [ "Password must be between 3 and 20 characters" ])

[<Fact>]
let ``multiple errors``() =
    validate "a!"
    |> should equal
           (Invalid
               [ "Password needs to contain letters and numbers"
                 "Password can only contain alphanumeric characters"
                 "Password must be between 3 and 20 characters" ])

let valid() = validate "password123" |> should equal Valid
 

Here's a much simpler version with partial active patterns:

module DailyChallenge

open System

type Password =
    | Valid
    | Invalid

let private boolToOption b =
    if b then Some() else None

let (|Length|_|) min max (s : string) =
    (s.Length >= min && s.Length <= max) |> boolToOption

let (|AlphaNumOnly|_|) (s : string) =
    Seq.forall (fun c -> Char.IsLetter(c) || Char.IsNumber(c)) s |> boolToOption

let (|LettersAndNumbers|_|) (s : string) =
    boolToOption
        (Option.isSome (Seq.tryFind Char.IsLetter s)
         && Option.isSome (Seq.tryFind Char.IsNumber s))

let validate (s : string) =
    match s with
    | Length 3 20 & AlphaNumOnly & LettersAndNumbers -> Valid
    | _ -> Invalid

Tests:

module DailyChallengeTests

open FsUnit.Xunit
open Xunit
open DailyChallenge

[<Fact>]
let ``invalid character``() = validate "Username123!" |> should equal Invalid

[<Fact>]
let ``no letters``() = validate "123" |> should equal Invalid

[<Fact>]
let ``too short``() = validate "a1" |> should equal Invalid

[<Fact>]
let ``too long``() = validate "a1a2a3a4a5a6a7a8a9a10" |> should equal Invalid

[<Fact>]
let ``multiple errors``() = validate "a!" |> should equal Invalid

let valid() = validate "password123" |> should equal Valid
 

A lazy JS implementation:

function validatePassword(password) {
  const pattern = /^[a-z0-9]{4,19}$/i;
  const test = pattern.test(password) && /[a-z]/i.test(password) && /[0-9]/.test(password);

  return test ? 'VALID' : 'INVALID';
}

function testValidatePassword() {
  console.assert(validatePassword('Username123!') === 'INVALID');
  console.assert(validatePassword('123') === 'INVALID');
  console.assert(validatePassword('Username123') === 'VALID');

  console.assert(validatePassword('Username') === 'INVALID');
  console.assert(validatePassword('IsThisPasswordTooLong') === 'INVALID');
  console.assert(validatePassword('DEVCommunity') === 'INVALID');
}
 

My swift solution :

func passwordValidator(password: String) -> String {
    ((password.reduce(true) { $0 == true && ($1.isLetter || $1.isNumber) }) &&
        3..<20 ~= password.count &&
        (password.contains { $0.isLetter }) &&
        (password.contains { $0.isNumber })) ? "VALID" : "INVALID"
}

passwordValidator(password:"Username123!") // INVALID
passwordValidator(password:"123") // INVALID
passwordValidator(password:"Username123") // VALID
 

Python

var = input("Enter the password: ")
aflag = 0
nflag = 0
sflag = 0

if len(var) < 4 or len(var) > 19:
        print("INNVALID")
else:
        for i in var:
                if i.isalpha():
                        aflag += 1
                elif i.isnumeric():
                        nflag += 1
                else:
                        sflag += 1
                        break

        if aflag == 0 or nflag == 0 or sflag != 0:
                print("INVALID")
        else:
                print("VALID")
 

Rust

fn validize(pwd: &str) -> bool {
    if !(3..=20).contains(&pwd.len()) {
        return false;
    }
    let mut alph = false;
    let mut num = false;
    for c in pwd.chars() {
        match c {
            '0'..='9' => num = true,
            'a'..='z' | 'A'..='Z' => alph = true,
            _ => return false,
        }
    }
    alph && num
}

// usage
fn main() {
    let t = |pwd| println!("{} => {}", pwd, validize(pwd));
    t("Username");
    t("IsThisPasswordTooLong");
    t("DEVCommunity");
    t("Username123!");
    t("123");
    t("Username123");
}

Imperative, because two predicates of any and one of all would be three iterations, and that is simply not acceptable.
The lambda is only a lambda to save lines on the internet. Don't do such things.
Look at it go.

DEVCommunity would be invalid because it's missing the numeric part ;)

 

A quick JS function:

const PASS_REGEX = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{4,20}$/g;
function validPass(password){
  return password.search(PASS_REGEX) < 0 ? 'INVALID' : 'VALID';
}
 

I think you missed rule 3. The password needs both alpha and numeric.