cc-d / flask-simple-captcha

lightweight captcha. doesn't require server side sessions packages
MIT License
29 stars 12 forks source link

flask-simple-captcha

CURRENT VERSION: v5.5.4

v5.0.0+ added an encryption mechanism to the stored text in the jwts. Previous versions are insecure!

v5.5.4 removed the upper limit on werkzeug/pillow dependency versions

flask-simple-captcha is a CAPTCHA generator class for generating and validating CAPTCHAs. It allows for easy integration into Flask applications.

See the encryption / decryption breakdown below for more information on the verification mechanism.

Features

Prerequisites

Installation

Import this package directly into your Flask project and make sure to install all dependencies.

How to Use

Configuration

DEFAULT_CONFIG = {
    'SECRET_CAPTCHA_KEY': 'LONGKEY',  # use for JWT encoding/decoding

    # CAPTCHA GENERATION SETTINGS
    'EXPIRE_SECONDS': 60 * 10,  # takes precedence over EXPIRE_MINUTES
    'CAPTCHA_IMG_FORMAT': 'JPEG',  # 'PNG' or 'JPEG' (JPEG is 3X faster)

    # CAPTCHA TEXT SETTINGS
    'CAPTCHA_LENGTH': 6,  # Length of the generated CAPTCHA text
    'CAPTCHA_DIGITS': False,  # Should digits be added to the character pool?
    'EXCLUDE_VISUALLY_SIMILAR': True,  # Exclude visually similar characters
    'BACKGROUND_COLOR': (0, 0, 0),  # RGB(A?) background color (default black)
    'TEXT_COLOR': (255, 255, 255),  # RGB(A?) text color (default white)

    # Optional settings
    #'ONLY_UPPERCASE': True, # Only use uppercase characters
    #'CHARACTER_POOL': 'AaBb',  # Use a custom character pool
}

Initialization

Add this code snippet at the top of your application:

from flask_simple_captcha import CAPTCHA
YOUR_CONFIG = {
    'SECRET_CAPTCHA_KEY': 'LONG_KEY',
    'CAPTCHA_LENGTH': 6,
    'CAPTCHA_DIGITS': False,
    'EXPIRE_SECONDS': 600,
}
SIMPLE_CAPTCHA = CAPTCHA(config=YOUR_CONFIG)
app = SIMPLE_CAPTCHA.init_app(app)

Protecting a Route

To add CAPTCHA protection to a route, you can use the following code:

@app.route('/example', methods=['GET','POST'])
def example():
    if request.method == 'GET':
        new_captcha_dict = SIMPLE_CAPTCHA.create()
        return render_template('your_template.html', captcha=new_captcha_dict)
    if request.method == 'POST':
        c_hash = request.form.get('captcha-hash')
        c_text = request.form.get('captcha-text')
        if SIMPLE_CAPTCHA.verify(c_text, c_hash):
            return 'success'
        else:
            return 'failed captcha'

In your HTML template, you need to wrap the CAPTCHA inputs within a form element. The package will only generate the CAPTCHA inputs but not the surrounding form or the submit button.

<!-- your_template.html -->
<form action="/example" method="post">
  {{ captcha_html(captcha)|safe }}
  <input type="submit" value="Submit" />
</form>

Example Captcha Images

Here is an example of what the generated CAPTCHA images look like, this is a screen shot from the /images route of the debug server.

Example CAPTCHA Image

link to image url if the above does not load

Encryption and Decryption Breakdown

Uses a combination of JWTs and Werkzeug's password hashing to encrypt and decrypt CAPTCHA text.

Encryption

  1. Salting the Text: The CAPTCHA text is salted by appending the secret key at the beginning.
    salted_text = secret_key + text
  2. Hashing: Werkzeug's generate_password_hash function is then used to hash the salted CAPTCHA text.
    hashed_text = generate_password_hash(salted_text)
  3. Creating JWT Token: A JWT token is generated using the hashed CAPTCHA text and an optional expiration time.
    payload = {
       'hashed_text': hashed_text,
       'exp': datetime.utcnow() + timedelta(seconds=expire_seconds),
    }
    return jwt.encode(payload, secret_key, algorithm='HS256')

Decryption

  1. Decode JWT Token: The JWT token is decoded using the secret key. If the token is invalid or expired, the decryption process will fail.
    decoded = jwt.decode(token, secret_key, algorithms=['HS256'])
  2. Extract Hashed Text: The hashed CAPTCHA text is extracted from the decoded JWT payload.
    hashed_text = decoded['hashed_text']
  3. Verifying the Hash: Werkzeug's check_password_hash function is used to verify that the hashed CAPTCHA text matches the original salted CAPTCHA text.
    salted_original_text = secret_key + original_text
    if check_password_hash(hashed_text, salted_original_text):
       return original_text

Development

Setting Up Your Development Environment Without VS Code

  1. Create a Virtual Environment:

    • Navigate to the project directory where you've cloned the repository and create a virtual environment named venv within the project directory:

      python -m venv venv/
  2. Activate the Virtual Environment:

    • Activate the virtual environment to isolate the project dependencies:
      • On macOS/Linux:
        source venv/bin/activate
      • On Windows (using Command Prompt):
        .\venv\Scripts\activate
      • On Windows (using PowerShell):
        .\venv\Scripts\Activate.ps1
  3. Install Dependencies:

    Install the required dependencies for development:

    pip install -r requirements_dev.txt

    Install the local flask-simple-captcha package:

    pip install .

Running Tests

ENSURE YOU HAVE A VENV NAMED venv IN THE PROJECT DIRECTORY AND THAT IT IS ACTIVATED AND BOTH THE DEPENDENCIES AND THE LOCAL FLASK-SIMPLE-CAPTCHA PACKAGE ARE INSTALLED IN THE VENV

As of the time of me writing this README (2023-11-15), pytest reports 97% test coverage of the logic in the flask_simple_captcha package. Should be kept as close to 100% as possible.

Run Tests Without VS Code

Running Tests With VS Code

Simply hit command + shift + p and type "Select And Start Debugging" and select Python: Run tests. You will want to make sure your venv is installed and activated.

Example Test Output

... previous output omitted for brevity ...

tests.py::TestCaptchaUtils::test_jwtencrypt PASSED
tests.py::TestCaptchaUtils::test_no_hashed_text PASSED

---------- coverage: platform darwin, python 3.8.18-final-0 ----------
Name                                         Stmts   Miss  Cover   Missing
----------------------------------
flask_simple_captcha/__init__.py                 3      0   100%
flask_simple_captcha/captcha_generation.py      78      0   100%
flask_simple_captcha/config.py                  10      0   100%
flask_simple_captcha/img.py                     56      0   100%
flask_simple_captcha/text.py                    25      0   100%
flask_simple_captcha/utils.py                   51      0   100%
----------------------------
TOTAL                                          223      0   100%

==================================== 41 passed in 5.53s

Debug Server

Start the debug server without VS Code

  1. Set Environment Variables:
    • Before running the debug Flask server, set the required environment variables:
    • On macOS/Linux:
      export FLASK_APP=debug_flask_server
      export FLASK_DEBUG=1
    • On Windows (using Command Prompt):
      set FLASK_APP=debug_flask_server
      set FLASK_DEBUG=1
    • On Windows (using PowerShell):
      $env:FLASK_APP="debug_flask_server"
      $env:FLASK_DEBUG="1"
  2. Start the debug Flask server:
    • Run the following command to start the debug Flask server:
      flask run --no-debugger
    • This will start the debug Flask server with the automatic reloader. See the navigation section below on how to access the debug server.

Start the debug server with VS Code

Accessing the Debug Server

Navigate to localhost:5000 in your browser to view the debug server. You can also navigate to localhost:5000/images to view 50 CAPTCHA images at once, or localhost:5000/images/<$NUMBER_OF_IMAGES> to view an arbritrary amount of generated captchas.

Code Examples

For usage and integration examples, including how to use flask-simple-captcha with WTForms and Flask-Security, check out the Code Examples document.

Contributing

Feel free to open a PR. The project has undergone a recent overhaul to improve the code quality.

If you make changes in the logic, please follow the steps laid out in this document for testing and debugging. Make sure the coverage % stays >= 100% and that you verify manually at least once that things look okay by submitting a real CAPTCHA in the debug server.

The pyproject.toml has the required configuration for black and isort. There is also a vscode settings file equivalent to the pyproject.toml file in .vscode/settings.json.

License

MIT

Contact: ccarterdev@gmail.com