Skip to content

Password Validation

Assignment

Build a Python application with:

  • Build a password validation function that checks an input password against the following 5 rules. Continue to prompt the user for a password until it meets the validation rules. If not, notify the user to re-input with a clear message.
Rule description Requirement Example
Minimum password length 8 characters Ensures basic resistance to brute-force attacks
Must include at least 1 special symbols 1 character Symbols like !, @, #, $, %, etc. can increase complexity
Must contain both uppercase and lowercase letters Yes Example: "SecurePass" instead of "securepass"
Must include at least one numeric digit Yes Example: "Passw0rd" includes the digit "0"
Avoid common words or predictable patterns Yes Avoid "123456", "password", or keyboard patterns like "qwerty"
  • Calculated time to execute guess and manipulation into 10k cases.

  • (Advance - Optional) Using GitHub to publish the program into a repository through a pull request (PR)

Solution

The engine required to build function(s) to validate string input and returned the structured output indicating whether the input meets the validation rules.

With that, the engine is interactive between the application and user based on the following flow:

sequenceDiagram
  participant user as User
  participant application as Application Interface
  participant engine as Engine

  %% Flow
  user->>application: input password (type string)
  application->>engine: push argument
  engine->>engine: Validation process
  engine->>application: return a dictionary containing the validation result of each rule
  application->>user: Table formated of the dictionary

Using TypeDict to structured output format of the engine with following metadata

Field Type Description Sample
rule_id int Unique identifier for each rule 1
rule_description str Description of each rule Minimum password length
status bool Status of the validation result True
context str Context of the validation result Password length is 8 characters

For the detail of the validation result, so that this can be accessed throguh the application

ValidationResult
  |__ ValidationResultOnRule

Using the NordPass list of most commonly used passwords as a basis, the engine should not allow any of the common rules to pass the list.

For example, if the validation is successful,

🎏 Please input your password to validate: 9asdjnhasdj1238asdK2$
┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
┃ [No.] ┃ Rule description                                        ┃ Status  ┃
┑━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
β”‚ 1     β”‚ Password length is required to be at least 8 characters β”‚ True 🟒 β”‚
β”‚ 2     β”‚ Password must include at least 1 special symbol         β”‚ True 🟒 β”‚
β”‚ 3     β”‚ Must contain both uppercase and lowercase letters       β”‚ True 🟒 β”‚
β”‚ 4     β”‚ Must include at least 1 digit                           β”‚ True 🟒 β”‚
β”‚ 5     β”‚ Avoid common words or predictable patterns              β”‚ True 🟒 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
>>> Your password is secure

And if the password fails in any of the rules

🎏 Please input your password to validate: notsatifypass
┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ [No.] ┃ Rule description                                        ┃ Status   ┃
┑━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
β”‚ 1     β”‚ Password length is required to be at least 8 characters β”‚ True 🟒  β”‚
β”‚ 2     β”‚ Password must include at least 1 special symbol         β”‚ False πŸ”΄ β”‚
β”‚ 3     β”‚ Must contain both uppercase and lowercase letters       β”‚ True 🟒  β”‚
β”‚ 4     β”‚ Must include at least 1 digit                           β”‚ False πŸ”΄ β”‚
β”‚ 5     β”‚ Avoid common words or predictable patterns              β”‚ True 🟒  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
>>> Your password is not strong enough.
😰 Please re-input again: Failagain
┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ [No.] ┃ Rule description                                        ┃ Status   ┃
┑━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
β”‚ 1     β”‚ Password length is required to be at least 8 characters β”‚ True 🟒  β”‚
β”‚ 2     β”‚ Password must include at least 1 special symbol         β”‚ False πŸ”΄ β”‚
β”‚ 3     β”‚ Must contain both uppercase and lowercase letters       β”‚ True 🟒  β”‚
β”‚ 4     β”‚ Must include at least 1 digit                           β”‚ False πŸ”΄ β”‚
β”‚ 5     β”‚ Avoid common words or predictable patterns              β”‚ True 🟒  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
>>> Your password is not strong enough.
😰 Please re-input again:                # ------------ Will loop until success

Code:

Start to build an engine function to define

engine.py
#!/bin/python3

# Global
import re
from typing import TypedDict, Callable, Sequence
import string
import itertools


class ValidationResultOnRule(TypedDict):
    rule_id: str
    rule_description: str
    status: bool = False
    contexts: dict


class ValidationResult(TypedDict):
    status: bool = False
    failures: list[int]
    details: dict[int, ValidationResultOnRule]


class ValidationRule(object):

    def __init__(self, rule_id: str, rule_description: str, func: Callable[[str], bool], context_generator: dict[str, Callable[[str], dict]] | None = None):
        self.rule_id = rule_id
        self.rule_description = rule_description
        self.func = func
        self.context_generator = context_generator

    def validate(self, text: str) -> ValidationResultOnRule:

        validation_result = self.func(text)
        contexts = {}
        if self.context_generator is not None:
            for key, context_func in self.context_generator.items():
                metadata = context_func(text)
                contexts[key] = metadata

        result = ValidationResultOnRule(
            rule_id=self.rule_id,
            rule_description=self.rule_description,
            status=validation_result,
            contexts=contexts
        )

        return result


def validate_rules(text: str, rules: dict[int, ValidationRule]) -> ValidationResult:
    """
    Validate a string against a list of validation rules.

    Args
    ----
    text (str): The string to validate.
    rules (list[ValidationRule]): A list of validation rules to check against.

    Returns
    -------
    A ValidationResult object, which contains the results of the validation for each rule.
        The object contains a dictionary with the index of the rule as the key and a ValidationResultOnRule object as the value.
        The ValidationResultOnRule object contains the status of the validation, a description of the rule, and a dictionary of additional context metadata.

    Usage
    -----
    >>> validate_rules(
        text="randomStringForValid9999@#$",
        rules={
            1: ValidationRule(rule_id="length", rule_description="Minimum length required", func=lambda s: len(s) >= 3, context_generator={"length": len}),
        }
    )
    """

    # Pair
    rule_results: ValidationResultOnRule  = {}
    for index, rule in rules.items():
        output = rule.validate(text=text)
        rule_results[index] = output

    # Chain
    result = ValidationResult(
        status=all([r["status"] for _, r in rule_results.items()]),
        failures=[k for k, _ in itertools.filterfalse(lambda s: s[1]["status"], rule_results.items())],
        details=rule_results
    )

    return result


def validate_contain_at_least_n_special_symbol(text: str, n: int) -> bool:
    return len(re.findall(rf"[{re.escape(string.punctuation)}]", text)) >= n



def validate_contain_both_uppsercase_and_lowercase(text: str) -> bool:
    return (
        len(re.findall(rf"[{re.escape(string.ascii_uppercase)}]", text))
        + len(re.findall(rf"[{re.escape(string.ascii_lowercase)}]", text))
    ) >= 2


def validate_contain_at_least_n_digit(text: str, n: int) -> bool:
    return len(re.findall(rf"[{re.escape(string.digits)}]", text)) >= n


def validate_not_match_predictable_patterns(text: str, bucket: Sequence[str]) -> bool:
    for value in bucket:
        if value == text:
            return False
    return True


# List of common password by top providers
# Included: Nordpass, Keeper, Global (Set string of all providers)
# For references, see: <https://en.wikipedia.org/wiki/List_of_the_most_common_passwords>

MOST_COMMON_PASSWORDS_NORDPASS = (
    "123456",
    "123456789",
    "1234567890",
    "password",
    "qwerty123",
    "qwerty1",
    "111111",
    "12345",
    "secret",
    "123123",
    "1234567890",
    "000000",
    "abc123",
    "password1",
    "iloveyou",
    "11111111",
    "dragon",
    "monkey",
)

MOST_COMMON_PASSWORDS_KEEPER = (
    "123456",
    "123456789",
    "qwerty",
    "12345678",
    "111111",
    "1234567890",
    "1234567",
    "password",
    "123123",
    "987654321",
    "qwertyuiop",
    "mynoob",
    "123321",
    "666666",
    "18atcskd2w",
    "7777777",
    "1q2w3e4r",
    "654321",
    "555555",
    "3rjs1la7qe",
    "google",
    "1q2w3e4r5t",
    "123qwe",
    "zxcvbnm",
    "1q2w3e",
)

MOST_COMMON_PASSWORDS: set[str] = set(MOST_COMMON_PASSWORDS_NORDPASS + MOST_COMMON_PASSWORDS_KEEPER)

Then construct application

application.py
#!/bin/python3

# Global
import sys
import os

# Path Append
sys.path.append(os.path.abspath(os.curdir))

# External
from rich.prompt import Prompt
from rich.console import Console
from rich.table import Table


# Internal
from lab.password_validation.engine import (
    ValidationRule,
    validate_contain_at_least_n_special_symbol,
    validate_contain_both_uppsercase_and_lowercase,
    validate_contain_at_least_n_digit,
    validate_not_match_predictable_patterns,
    MOST_COMMON_PASSWORDS_NORDPASS,
    validate_rules,
)

# Construct passowrd validation rules
PASSWORD_VALIDATION_RULES = {
    1: ValidationRule(
        rule_id="MINIMUM_PASSWORD_LENGTH",
        rule_description="Password length is required to be at least 8 characters",
        func=lambda s: len(s) >= 8,
        context_generator={
            "length": len
        }
    ),
    2: ValidationRule(
        rule_id="INCLUDE_AT_LEAST_ONE_SPECIAL_SYMBOL",
        rule_description="Password must include at least 1 special symbol",
        func=lambda s: validate_contain_at_least_n_special_symbol(s, n=1),
    ),
    3: ValidationRule(
        rule_id="INCLUDE_BOTH_UPPERCASE_AND_LOWERCASE_LETTERS",
        rule_description="Must contain both uppercase and lowercase letters",
        func=validate_contain_both_uppsercase_and_lowercase,
    ),
    4: ValidationRule(
        rule_id="INCLUDE_AT_LEAST_ONE_DIGIT",
        rule_description="Must include at least 1 digit",
        func=lambda s: validate_contain_at_least_n_digit(s, n=1),
    ),
    5: ValidationRule(
        rule_id="AVOID_COMMON_PATTERNS",
        rule_description="Avoid common words or predictable patterns",
        func=lambda s: validate_not_match_predictable_patterns(s, bucket=MOST_COMMON_PASSWORDS_NORDPASS),
    ),
}


if __name__ == "__main__":

    # Handle
    console = Console(emoji=True)

    # Get from user
    # Inspired by: https://github.com/Textualize/rich/blob/master/rich/prompt.py
    input_sentence = Prompt.ask(":flags: Please input your password to validate")

    while True:

        # Validate
        validation_result = validate_rules(text=input_sentence, rules=PASSWORD_VALIDATION_RULES)

        # Add the table output
        table = Table()
        table.add_column("[No.]")
        table.add_column("Rule description")
        table.add_column("Status")

        for ind, detail in sorted(validation_result["details"].items()):
            _mark = ":green_circle:" if detail["status"] else ":red_circle:"
            table.add_row(f"{ind}", detail["rule_description"], f"{detail['status']} {_mark}")

        # Handle
        console.print(table)

        if validation_result["status"] is True:
            console.print("[green]>>> Your password is secure")
            break

        else:
            console.print("[red]>>> Your password is not strong enough.")
            input_sentence = Prompt.ask(":anxious_face_with_sweat: Please re-input again")

Next, execute the application by running the following command in your terminal:

python lab/password_validation/application.py

For the performance of the validate_rules engine:

python lab/password_validation/benchmark.py
# [Result] [Total] Execute 10000 executions take 0.546714 seconds
# [Result] [Avg.] Each execution take 0.000055 seconds

by following snippet:

benchmark.py
#!/bin/python3

# Global
import sys
import os
import timeit
import textwrap

# Path Append
sys.path.append(os.path.abspath(os.curdir))

# Internal
stmt = textwrap.dedent("""\
from lab.password_validation.engine import validate_rules
from lab.password_validation.application import PASSWORD_VALIDATION_RULES
validate_rules(text="StrongPasswordSince2026@123456789", rules=PASSWORD_VALIDATION_RULES)
""")

if __name__ == "__main__":

    # Benchmark for validate_rules with 5 rules
    # The result is the total time for total of number execution
    number_execution = 10_000
    benchmark = timeit.timeit(stmt=stmt, number=number_execution)
    print(f"[Result] [Total] Execute {number_execution} executions take {benchmark:3f} seconds")
    print(f"[Result] [Avg.] Each execution take {benchmark / number_execution:3f} seconds")

Futher Reading