Compare commits
	
		
			34 Commits
		
	
	
		
			v0.3.0
			...
			5b255b6504
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5b255b6504 | |||
| 919b8d0dc3 | |||
| 604ed0be27 | |||
| 9e0a06bd4c | |||
| e861cae2e0 | |||
| 264ba158ee | |||
| d1fd0c3693 | |||
| bc15a578cb | |||
| cedff68247 | |||
| 769ca7dddd | |||
| 33eb81f368 | |||
| 5faf51c49b | |||
| d5a8bb3acd | |||
| 27d27127f6 | |||
| 5ebdea6d0a | |||
| ea31bb9579 | |||
| 4f30756dc5 | |||
| cdc057f851 | |||
| 574ecade05 | |||
| 84b9c5f62e | |||
| 4990de085c | |||
| 51e51ae4e2 | |||
| 2de770aed0 | |||
| 9ab413d583 | |||
| aeb93a60e5 | |||
| a07118ef9c | |||
| 514e9255aa | |||
| 79abdc9cde | |||
| 038e7a8352 | |||
| 0387abb4f6 | |||
| 10e8add9e6 | |||
| c004e28c37 | |||
| 46f05a1022 | |||
| b9384150b7 | 
							
								
								
									
										40
									
								
								.readthedocs.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.readthedocs.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
# The Flask HTTP Digest Authentication Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/4/5
 | 
			
		||||
 | 
			
		||||
#  Copyright (c) 2023 imacat.
 | 
			
		||||
#
 | 
			
		||||
#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
#  you may not use this file except in compliance with the License.
 | 
			
		||||
#  You may obtain a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#  Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
#  See the License for the specific language governing permissions and
 | 
			
		||||
#  limitations under the License.
 | 
			
		||||
 | 
			
		||||
# .readthedocs.yaml
 | 
			
		||||
# Read the Docs configuration file
 | 
			
		||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
 | 
			
		||||
 | 
			
		||||
# Required
 | 
			
		||||
version: 2
 | 
			
		||||
 | 
			
		||||
# Set the version of Python and other tools you might need
 | 
			
		||||
build:
 | 
			
		||||
  os: ubuntu-22.04
 | 
			
		||||
  tools:
 | 
			
		||||
    python: "3.8"
 | 
			
		||||
 | 
			
		||||
# Build documentation in the docs/ directory with Sphinx
 | 
			
		||||
 | 
			
		||||
# If using Sphinx, optionally build your docs in additional formats such as PDF
 | 
			
		||||
formats: all
 | 
			
		||||
 | 
			
		||||
# Optionally declare the Python requirements required to build your docs
 | 
			
		||||
python:
 | 
			
		||||
   install:
 | 
			
		||||
   - method: pip
 | 
			
		||||
     path: .
 | 
			
		||||
@@ -15,8 +15,7 @@
 | 
			
		||||
#  See the License for the specific language governing permissions and
 | 
			
		||||
#  limitations under the License.
 | 
			
		||||
 | 
			
		||||
include docs/*
 | 
			
		||||
include docs/source/*
 | 
			
		||||
include docs/source/_static/*
 | 
			
		||||
include docs/source/_templates/*
 | 
			
		||||
include tests/*
 | 
			
		||||
recursive-include docs *
 | 
			
		||||
recursive-exclude docs/build *
 | 
			
		||||
recursive-include tests *
 | 
			
		||||
recursive-exclude tests *.pyc
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										355
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										355
									
								
								README.rst
									
									
									
									
									
								
							@@ -6,14 +6,12 @@ Flask HTTP Digest Authentication
 | 
			
		||||
Description
 | 
			
		||||
===========
 | 
			
		||||
 | 
			
		||||
*Flask-Digest-Auth* is an `HTTP Digest Authentication`_ implementation
 | 
			
		||||
*Flask-DigestAuth* is an `HTTP Digest Authentication`_ implementation
 | 
			
		||||
for Flask_ applications.  It authenticates the user for the protected
 | 
			
		||||
views.
 | 
			
		||||
 | 
			
		||||
HTTP Digest Authentication is specified in `RFC 2617`_.
 | 
			
		||||
 | 
			
		||||
Refer to the full `Flask-Digest-Auth readthedocs documentation`_.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Why HTTP Digest Authentication?
 | 
			
		||||
-------------------------------
 | 
			
		||||
@@ -30,7 +28,7 @@ own challenge-response log in form, but then you are reinventing the
 | 
			
		||||
wheels.  If a pretty log in form is not critical to your project, HTTP
 | 
			
		||||
Digest Authentication should be a good choice.
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth works with Flask-Login_.  Log in protection can be
 | 
			
		||||
Flask-DigestAuth works with Flask-Login_.  Log in protection can be
 | 
			
		||||
separated with the authentication mechanism.  You can create protected
 | 
			
		||||
Flask modules without knowing the actual authentication mechanisms.
 | 
			
		||||
 | 
			
		||||
@@ -38,358 +36,36 @@ Flask modules without knowing the actual authentication mechanisms.
 | 
			
		||||
Installation
 | 
			
		||||
============
 | 
			
		||||
 | 
			
		||||
You can install Flask-Digest-Auth with ``pip``:
 | 
			
		||||
You can install Flask-DigestAuth with ``pip``:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    pip install Flask-Digest-Auth
 | 
			
		||||
    pip install Flask-DigestAuth
 | 
			
		||||
 | 
			
		||||
You may also install the latest source from the
 | 
			
		||||
`Flask-Digest-Auth GitHub repository`_.
 | 
			
		||||
`Flask-DigestAuth GitHub repository`_.
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    pip install git+https://github.com/imacat/flask-digest-auth.git
 | 
			
		||||
    pip install git+https://github.com/imacat/flask-digestauth.git
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Setting the Password
 | 
			
		||||
====================
 | 
			
		||||
Documentation
 | 
			
		||||
=============
 | 
			
		||||
 | 
			
		||||
The password hash of the HTTP Digest Authentication is composed of the
 | 
			
		||||
realm, the username, and the password.  Example for setting the
 | 
			
		||||
password:
 | 
			
		||||
Refer to the `documentation on Read the Docs`_.
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    from flask_digest_auth import make_password_hash
 | 
			
		||||
Change Log
 | 
			
		||||
==========
 | 
			
		||||
 | 
			
		||||
    user.password = make_password_hash(realm, username, password)
 | 
			
		||||
 | 
			
		||||
The username is part of the hash.  If the user changes their username,
 | 
			
		||||
you need to ask their password, to generate and store the new password
 | 
			
		||||
hash.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth Alone
 | 
			
		||||
=======================
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth can authenticate the users alone.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Simple Applications with Flask-Digest-Auth Alone
 | 
			
		||||
------------------------------------------------
 | 
			
		||||
 | 
			
		||||
In your ``my_app.py``:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    from flask import Flask, request, redirect
 | 
			
		||||
    from flask_digest_auth import DigestAuth
 | 
			
		||||
 | 
			
		||||
    app: flask = Flask(__name__)
 | 
			
		||||
    ... (Configure the Flask application) ...
 | 
			
		||||
 | 
			
		||||
    auth: DigestAuth = DigestAuth(realm="Admin")
 | 
			
		||||
    auth.init_app(app)
 | 
			
		||||
 | 
			
		||||
    @auth.register_get_password
 | 
			
		||||
    def get_password_hash(username: str) -> t.Optional[str]:
 | 
			
		||||
        ... (Load the password hash) ...
 | 
			
		||||
 | 
			
		||||
    @auth.register_get_user
 | 
			
		||||
    def get_user(username: str) -> t.Optional[t.Any]:
 | 
			
		||||
        ... (Load the user) ...
 | 
			
		||||
 | 
			
		||||
    @app.get("/admin")
 | 
			
		||||
    @auth.login_required
 | 
			
		||||
    def admin():
 | 
			
		||||
        return f"Hello, {g.user.username}!"
 | 
			
		||||
 | 
			
		||||
    @app.post("/logout")
 | 
			
		||||
    @auth.login_required
 | 
			
		||||
    def logout():
 | 
			
		||||
        auth.logout()
 | 
			
		||||
        return redirect(request.form.get("next"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Larger Applications with ``create_app()`` with Flask-Digest-Auth Alone
 | 
			
		||||
----------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
In your ``my_app/__init__.py``:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    from flask import Flask
 | 
			
		||||
    from flask_digest_auth import DigestAuth
 | 
			
		||||
 | 
			
		||||
    auth: DigestAuth = DigestAuth()
 | 
			
		||||
 | 
			
		||||
    def create_app(test_config = None) -> Flask:
 | 
			
		||||
        app: flask = Flask(__name__)
 | 
			
		||||
        ... (Configure the Flask application) ...
 | 
			
		||||
 | 
			
		||||
        auth.realm = app.config["REALM"]
 | 
			
		||||
        auth.init_app(app)
 | 
			
		||||
 | 
			
		||||
        @auth.register_get_password
 | 
			
		||||
        def get_password_hash(username: str) -> t.Optional[str]:
 | 
			
		||||
            ... (Load the password hash) ...
 | 
			
		||||
 | 
			
		||||
        @auth.register_get_user
 | 
			
		||||
        def get_user(username: str) -> t.Optional[t.Any]:
 | 
			
		||||
            ... (Load the user) ...
 | 
			
		||||
 | 
			
		||||
        return app
 | 
			
		||||
 | 
			
		||||
In your ``my_app/views.py``:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    from my_app import auth
 | 
			
		||||
    from flask import Flask, Blueprint, request, redirect
 | 
			
		||||
 | 
			
		||||
    bp = Blueprint("admin", __name__, url_prefix="/admin")
 | 
			
		||||
 | 
			
		||||
    @bp.get("/admin")
 | 
			
		||||
    @auth.login_required
 | 
			
		||||
    def admin():
 | 
			
		||||
        return f"Hello, {g.user.username}!"
 | 
			
		||||
 | 
			
		||||
    @app.post("/logout")
 | 
			
		||||
    @auth.login_required
 | 
			
		||||
    def logout():
 | 
			
		||||
        auth.logout()
 | 
			
		||||
        return redirect(request.form.get("next"))
 | 
			
		||||
 | 
			
		||||
    def init_app(app: Flask) -> None:
 | 
			
		||||
        app.register_blueprint(bp)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Flask-Login Integration
 | 
			
		||||
=======================
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth works with Flask-Login_.  You can write a Flask
 | 
			
		||||
module that requires log in, without specifying how to log in.  The
 | 
			
		||||
application can use either HTTP Digest Authentication, or the log in
 | 
			
		||||
forms, as needed.
 | 
			
		||||
 | 
			
		||||
To use Flask-Login with Flask-Digest-Auth,
 | 
			
		||||
``login_manager.init_app(app)`` must be called before
 | 
			
		||||
``auth.init_app(app)``.
 | 
			
		||||
 | 
			
		||||
The currently logged-in user can be retrieved at
 | 
			
		||||
``flask_login.current_user``, if any.
 | 
			
		||||
 | 
			
		||||
The views only depend on Flask-Login, but not the Flask-Digest-Auth.
 | 
			
		||||
You can change the actual authentication mechanism without changing
 | 
			
		||||
the views.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Simple Applications with Flask-Login Integration
 | 
			
		||||
------------------------------------------------
 | 
			
		||||
 | 
			
		||||
In your ``my_app.py``:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    import flask_login
 | 
			
		||||
    from flask import Flask, request, redirect
 | 
			
		||||
    from flask_digest_auth import DigestAuth
 | 
			
		||||
 | 
			
		||||
    app: flask = Flask(__name__)
 | 
			
		||||
    ... (Configure the Flask application) ...
 | 
			
		||||
 | 
			
		||||
    login_manager: flask_login.LoginManager = flask_login.LoginManager()
 | 
			
		||||
    login_manager.init_app(app)
 | 
			
		||||
 | 
			
		||||
    @login_manager.user_loader
 | 
			
		||||
    def load_user(user_id: str) -> t.Optional[User]:
 | 
			
		||||
        ... (Load the user with the username) ...
 | 
			
		||||
 | 
			
		||||
    auth: DigestAuth = DigestAuth(realm="Admin")
 | 
			
		||||
    auth.init_app(app)
 | 
			
		||||
 | 
			
		||||
    @auth.register_get_password
 | 
			
		||||
    def get_password_hash(username: str) -> t.Optional[str]:
 | 
			
		||||
        ... (Load the password hash) ...
 | 
			
		||||
 | 
			
		||||
    @app.get("/admin")
 | 
			
		||||
    @flask_login.login_required
 | 
			
		||||
    def admin():
 | 
			
		||||
        return f"Hello, {flask_login.current_user.get_id()}!"
 | 
			
		||||
 | 
			
		||||
    @app.post("/logout")
 | 
			
		||||
    @flask_login.login_required
 | 
			
		||||
    def logout():
 | 
			
		||||
        auth.logout()
 | 
			
		||||
        # Do not call flask_login.logout_user()
 | 
			
		||||
        return redirect(request.form.get("next"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Larger Applications with ``create_app()`` with Flask-Login Integration
 | 
			
		||||
----------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
In your ``my_app/__init__.py``:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    from flask import Flask
 | 
			
		||||
    from flask_digest_auth import DigestAuth
 | 
			
		||||
    from flask_login import LoginManager
 | 
			
		||||
 | 
			
		||||
    auth: DigestAuth = DigestAuth()
 | 
			
		||||
 | 
			
		||||
    def create_app(test_config = None) -> Flask:
 | 
			
		||||
        app: flask = Flask(__name__)
 | 
			
		||||
        ... (Configure the Flask application) ...
 | 
			
		||||
 | 
			
		||||
        login_manager: LoginManager = LoginManager()
 | 
			
		||||
        login_manager.init_app(app)
 | 
			
		||||
 | 
			
		||||
        @login_manager.user_loader
 | 
			
		||||
        def load_user(user_id: str) -> t.Optional[User]:
 | 
			
		||||
            ... (Load the user with the username) ...
 | 
			
		||||
 | 
			
		||||
        auth.realm = app.config["REALM"]
 | 
			
		||||
        auth.init_app(app)
 | 
			
		||||
 | 
			
		||||
        @auth.register_get_password
 | 
			
		||||
        def get_password_hash(username: str) -> t.Optional[str]:
 | 
			
		||||
            ... (Load the password hash) ...
 | 
			
		||||
 | 
			
		||||
        return app
 | 
			
		||||
 | 
			
		||||
In your ``my_app/views.py``:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    import flask_login
 | 
			
		||||
    from flask import Flask, Blueprint, request, redirect
 | 
			
		||||
    from my_app import auth
 | 
			
		||||
 | 
			
		||||
    bp = Blueprint("admin", __name__, url_prefix="/admin")
 | 
			
		||||
 | 
			
		||||
    @bp.get("/admin")
 | 
			
		||||
    @flask_login.login_required
 | 
			
		||||
    def admin():
 | 
			
		||||
        return f"Hello, {flask_login.current_user.get_id()}!"
 | 
			
		||||
 | 
			
		||||
    @app.post("/logout")
 | 
			
		||||
    @flask_login.login_required
 | 
			
		||||
    def logout():
 | 
			
		||||
        auth.logout()
 | 
			
		||||
        # Do not call flask_login.logout_user()
 | 
			
		||||
        return redirect(request.form.get("next"))
 | 
			
		||||
 | 
			
		||||
    def init_app(app: Flask) -> None:
 | 
			
		||||
        app.register_blueprint(bp)
 | 
			
		||||
 | 
			
		||||
The views only depend on Flask-Login, but not the actual
 | 
			
		||||
authentication mechanism.  You can change the actual authentication
 | 
			
		||||
mechanism without changing the views.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Session Integration
 | 
			
		||||
===================
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth features session integration.  The user log in
 | 
			
		||||
is remembered in the session.  The authentication information is not
 | 
			
		||||
requested again.  This is different to the practice of the HTTP Digest
 | 
			
		||||
Authentication, but is convenient for the log in accounting.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Log In Bookkeeping
 | 
			
		||||
==================
 | 
			
		||||
 | 
			
		||||
You can register a callback to run when the user logs in, for ex.,
 | 
			
		||||
logging the log in event, adding the log in counter, etc.
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    @auth.register_on_login
 | 
			
		||||
    def on_login(user: User) -> None:
 | 
			
		||||
        user.visits = user.visits + 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Log Out
 | 
			
		||||
=======
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth supports log out.  The user will be prompted for the
 | 
			
		||||
new username and password.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Test Client
 | 
			
		||||
===========
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth comes with a test client that supports HTTP digest
 | 
			
		||||
authentication.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
A unittest Test Case
 | 
			
		||||
--------------------
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    from flask import Flask
 | 
			
		||||
    from flask_digest_auth import Client
 | 
			
		||||
    from flask_testing import TestCase
 | 
			
		||||
    from my_app import create_app
 | 
			
		||||
 | 
			
		||||
    class MyTestCase(TestCase):
 | 
			
		||||
 | 
			
		||||
        def create_app(self):
 | 
			
		||||
            app: Flask = create_app({
 | 
			
		||||
                "SECRET_KEY": token_urlsafe(32),
 | 
			
		||||
                "TESTING": True
 | 
			
		||||
            })
 | 
			
		||||
            app.test_client_class = Client
 | 
			
		||||
            return app
 | 
			
		||||
 | 
			
		||||
        def test_admin(self):
 | 
			
		||||
            response = self.client.get("/admin")
 | 
			
		||||
            self.assertEqual(response.status_code, 401)
 | 
			
		||||
            response = self.client.get(
 | 
			
		||||
                "/admin", digest_auth=("my_name", "my_pass"))
 | 
			
		||||
            self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
A pytest Test
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    import pytest
 | 
			
		||||
    from flask import Flask
 | 
			
		||||
    from flask_digest_auth import Client
 | 
			
		||||
    from my_app import create_app
 | 
			
		||||
 | 
			
		||||
    @pytest.fixture()
 | 
			
		||||
    def app():
 | 
			
		||||
        app: Flask = create_app({
 | 
			
		||||
            "SECRET_KEY": token_urlsafe(32),
 | 
			
		||||
            "TESTING": True
 | 
			
		||||
        })
 | 
			
		||||
        app.test_client_class = Client
 | 
			
		||||
        yield app
 | 
			
		||||
 | 
			
		||||
    @pytest.fixture()
 | 
			
		||||
    def client(app):
 | 
			
		||||
        return app.test_client()
 | 
			
		||||
 | 
			
		||||
    def test_admin(app: Flask, client: Client):
 | 
			
		||||
        with app.app_context():
 | 
			
		||||
            response = client.get("/admin")
 | 
			
		||||
            assert response.status_code == 401
 | 
			
		||||
            response = client.get(
 | 
			
		||||
                "/admin", digest_auth=("my_name", "my_pass"))
 | 
			
		||||
            assert response.status_code == 200
 | 
			
		||||
Refer to the `change log`_.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Copyright
 | 
			
		||||
=========
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2022 imacat.
 | 
			
		||||
 Copyright (c) 2022-2023 imacat.
 | 
			
		||||
 | 
			
		||||
 Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 you may not use this file except in compliance with the License.
 | 
			
		||||
@@ -414,6 +90,7 @@ Authors
 | 
			
		||||
.. _HTTP Digest Authentication: https://en.wikipedia.org/wiki/Digest_access_authentication
 | 
			
		||||
.. _RFC 2617: https://www.rfc-editor.org/rfc/rfc2617
 | 
			
		||||
.. _Flask: https://flask.palletsprojects.com
 | 
			
		||||
.. _Flask-Digest-Auth GitHub repository: https://github.com/imacat/flask-digest-auth
 | 
			
		||||
.. _Flask-Digest-Auth readthedocs documentation: https://flask-digest-auth.readthedocs.io
 | 
			
		||||
.. _Flask-DigestAuth GitHub repository: https://github.com/imacat/flask-digestauth
 | 
			
		||||
.. _Flask-Login: https://flask-login.readthedocs.io
 | 
			
		||||
.. _documentation on Read the Docs: https://flask-digestauth.readthedocs.io
 | 
			
		||||
.. _change log: https://flask-digestauth.readthedocs.io/en/latest/changelog.html
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,6 @@ if "%SPHINXBUILD%" == "" (
 | 
			
		||||
set SOURCEDIR=source
 | 
			
		||||
set BUILDDIR=build
 | 
			
		||||
 | 
			
		||||
if "%1" == "" goto help
 | 
			
		||||
 | 
			
		||||
%SPHINXBUILD% >NUL 2>NUL
 | 
			
		||||
if errorlevel 9009 (
 | 
			
		||||
	echo.
 | 
			
		||||
@@ -21,10 +19,12 @@ if errorlevel 9009 (
 | 
			
		||||
	echo.may add the Sphinx directory to PATH.
 | 
			
		||||
	echo.
 | 
			
		||||
	echo.If you don't have Sphinx installed, grab it from
 | 
			
		||||
	echo.http://sphinx-doc.org/
 | 
			
		||||
	echo.https://www.sphinx-doc.org/
 | 
			
		||||
	exit /b 1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if "%1" == "" goto help
 | 
			
		||||
 | 
			
		||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
 | 
			
		||||
goto end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
flask
 | 
			
		||||
							
								
								
									
										122
									
								
								docs/source/changelog.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								docs/source/changelog.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,122 @@
 | 
			
		||||
Change Log
 | 
			
		||||
==========
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.6.0
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2023/4/26
 | 
			
		||||
 | 
			
		||||
* Updated the minimal Python version to 3.8.
 | 
			
		||||
* Switched from ``setup.cfg`` to ``pyproject.toml``.
 | 
			
		||||
* Added the change log.
 | 
			
		||||
* Simplified ``README.rst``.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.5.0
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2023/1/6
 | 
			
		||||
 | 
			
		||||
* Added the ``DIGEST_AUTH_REALM`` configuration variable as the
 | 
			
		||||
  recommended way to set the authentication realm.
 | 
			
		||||
* Changed the default realm from an empty string to
 | 
			
		||||
  ``Login Required``.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.4.0
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2023/1/4
 | 
			
		||||
 | 
			
		||||
* Changed the package name from ``flask-digest-auth`` to
 | 
			
		||||
  ``Flask-DigestAuth``, according to the Flask recommended extension
 | 
			
		||||
  guidelines
 | 
			
		||||
  https://flask.palletsprojects.com/en/latest/extensiondev/ .
 | 
			
		||||
* Replaced ``app.digest_auth`` with ``app.extensions["digest-auth"]``
 | 
			
		||||
  to store the ``DigestAuth`` instance.
 | 
			
		||||
* Replaced ``auth.app`` with ``current_app``, to prevent circular
 | 
			
		||||
  imports.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.3.1
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2022/12/29
 | 
			
		||||
 | 
			
		||||
Fixed the missing authentication state with disabled users.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.3.0
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2022/12/7
 | 
			
		||||
 | 
			
		||||
Changed the visibility of several methods and properties of the
 | 
			
		||||
DigestAuth class that should be private to private.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.2.4
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2022/12/6
 | 
			
		||||
 | 
			
		||||
Fixed the pytest example in the documentation.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.2.3
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2022/12/6
 | 
			
		||||
 | 
			
		||||
Fixed the dependencies for the documentation hosted on Read the Docs.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.2.2
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2022/12/6
 | 
			
		||||
 | 
			
		||||
Added the Sphinx documentation, and hosted the documentation on
 | 
			
		||||
Read the Docs.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.2.1
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2022/12/6
 | 
			
		||||
 | 
			
		||||
Various fixes, with the help from SonarQube.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.2.0
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2022/11/27
 | 
			
		||||
 | 
			
		||||
* Added log out support.  User can log out.
 | 
			
		||||
* Added on-login event handler.  You can do some accounting when the
 | 
			
		||||
  user logs in.
 | 
			
		||||
 | 
			
		||||
This release is written in Sydney and on the international flight,
 | 
			
		||||
and released in Taipei.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.1.1
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2022/11/24
 | 
			
		||||
 | 
			
		||||
Changed the minimal Python version to 3.7.
 | 
			
		||||
 | 
			
		||||
Released at Sydney, Australia on vacation.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Version 0.1.0
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Released 2022/11/24
 | 
			
		||||
 | 
			
		||||
The initial release.
 | 
			
		||||
 | 
			
		||||
Released at Sydney, Australia on vacation.
 | 
			
		||||
@@ -1,59 +1,33 @@
 | 
			
		||||
# Configuration file for the Sphinx documentation builder.
 | 
			
		||||
#
 | 
			
		||||
# This file only contains a selection of the most common options. For a full
 | 
			
		||||
# list see the documentation:
 | 
			
		||||
# For the full list of built-in configuration values, see the documentation:
 | 
			
		||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
 | 
			
		||||
import os
 | 
			
		||||
# -- Path setup --------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
# If extensions (or modules to document with autodoc) are in another directory,
 | 
			
		||||
# add these directories to sys.path here. If the directory is relative to the
 | 
			
		||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
 | 
			
		||||
#
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
sys.path.insert(0, os.path.abspath('../../src/'))
 | 
			
		||||
 | 
			
		||||
import flask_digest_auth
 | 
			
		||||
 | 
			
		||||
# -- Project information -----------------------------------------------------
 | 
			
		||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
 | 
			
		||||
 | 
			
		||||
project = 'Flask-Digest-Auth'
 | 
			
		||||
copyright = '2022, imacat'
 | 
			
		||||
project = 'Flask-DigestAuth'
 | 
			
		||||
copyright = '2022-2023, imacat'
 | 
			
		||||
author = 'imacat'
 | 
			
		||||
 | 
			
		||||
# The full version, including alpha/beta/rc tags
 | 
			
		||||
release = '0.3.0'
 | 
			
		||||
 | 
			
		||||
release = flask_digest_auth.VERSION
 | 
			
		||||
 | 
			
		||||
# -- General configuration ---------------------------------------------------
 | 
			
		||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 | 
			
		||||
 | 
			
		||||
# Add any Sphinx extension module names here, as strings. They can be
 | 
			
		||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 | 
			
		||||
# ones.
 | 
			
		||||
extensions = [
 | 
			
		||||
    "sphinx.ext.autodoc"
 | 
			
		||||
]
 | 
			
		||||
extensions = ["sphinx.ext.autodoc"]
 | 
			
		||||
 | 
			
		||||
# Add any paths that contain templates here, relative to this directory.
 | 
			
		||||
templates_path = ['_templates']
 | 
			
		||||
 | 
			
		||||
# List of patterns, relative to source directory, that match files and
 | 
			
		||||
# directories to ignore when looking for source files.
 | 
			
		||||
# This pattern also affects html_static_path and html_extra_path.
 | 
			
		||||
exclude_patterns = []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# -- Options for HTML output -------------------------------------------------
 | 
			
		||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
 | 
			
		||||
 | 
			
		||||
# The theme to use for HTML and HTML Help pages.  See the documentation for
 | 
			
		||||
# a list of builtin themes.
 | 
			
		||||
#
 | 
			
		||||
html_theme = 'sphinx_rtd_theme'
 | 
			
		||||
 | 
			
		||||
# Add any paths that contain custom static files (such as style sheets) here,
 | 
			
		||||
# relative to this directory. They are copied after the builtin static files,
 | 
			
		||||
# so a file named "default.css" will overwrite the builtin "default.css".
 | 
			
		||||
html_static_path = ['_static']
 | 
			
		||||
 | 
			
		||||
# For readthedocs.io to work properly.
 | 
			
		||||
master_doc = 'index'
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,8 @@ Examples
 | 
			
		||||
 | 
			
		||||
.. _example-alone-simple:
 | 
			
		||||
 | 
			
		||||
Simple Applications with Flask-Digest-Auth Alone
 | 
			
		||||
------------------------------------------------
 | 
			
		||||
Simple Applications with Flask-DigestAuth Alone
 | 
			
		||||
-----------------------------------------------
 | 
			
		||||
 | 
			
		||||
In your ``my_app.py``:
 | 
			
		||||
 | 
			
		||||
@@ -17,7 +17,7 @@ In your ``my_app.py``:
 | 
			
		||||
    app: flask = Flask(__name__)
 | 
			
		||||
    ... (Configure the Flask application) ...
 | 
			
		||||
 | 
			
		||||
    auth: DigestAuth = DigestAuth(realm="Admin")
 | 
			
		||||
    auth: DigestAuth = DigestAuth()
 | 
			
		||||
    auth.init_app(app)
 | 
			
		||||
 | 
			
		||||
    @auth.register_get_password
 | 
			
		||||
@@ -42,8 +42,8 @@ In your ``my_app.py``:
 | 
			
		||||
 | 
			
		||||
.. _example-alone-large:
 | 
			
		||||
 | 
			
		||||
Larger Applications with ``create_app()`` with Flask-Digest-Auth Alone
 | 
			
		||||
----------------------------------------------------------------------
 | 
			
		||||
Larger Applications with ``create_app()`` with Flask-DigestAuth Alone
 | 
			
		||||
---------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
In your ``my_app/__init__.py``:
 | 
			
		||||
 | 
			
		||||
@@ -58,7 +58,6 @@ In your ``my_app/__init__.py``:
 | 
			
		||||
        app: flask = Flask(__name__)
 | 
			
		||||
        ... (Configure the Flask application) ...
 | 
			
		||||
 | 
			
		||||
        auth.realm = app.config["REALM"]
 | 
			
		||||
        auth.init_app(app)
 | 
			
		||||
 | 
			
		||||
        @auth.register_get_password
 | 
			
		||||
@@ -118,7 +117,7 @@ In your ``my_app.py``:
 | 
			
		||||
    def load_user(user_id: str) -> t.Optional[User]:
 | 
			
		||||
        ... (Load the user with the username) ...
 | 
			
		||||
 | 
			
		||||
    auth: DigestAuth = DigestAuth(realm="Admin")
 | 
			
		||||
    auth: DigestAuth = DigestAuth()
 | 
			
		||||
    auth.init_app(app)
 | 
			
		||||
 | 
			
		||||
    @auth.register_get_password
 | 
			
		||||
@@ -164,7 +163,6 @@ In your ``my_app/__init__.py``:
 | 
			
		||||
        def load_user(user_id: str) -> t.Optional[User]:
 | 
			
		||||
            ... (Load the user with the username) ...
 | 
			
		||||
 | 
			
		||||
        auth.realm = app.config["REALM"]
 | 
			
		||||
        auth.init_app(app)
 | 
			
		||||
 | 
			
		||||
        @auth.register_get_password
 | 
			
		||||
@@ -219,8 +217,9 @@ A unittest Test Case
 | 
			
		||||
 | 
			
		||||
        def create_app(self):
 | 
			
		||||
            app: Flask = create_app({
 | 
			
		||||
                "TESTING": True,
 | 
			
		||||
                "SECRET_KEY": token_urlsafe(32),
 | 
			
		||||
                "TESTING": True
 | 
			
		||||
                "DIGEST_AUTH_REALM": "admin",
 | 
			
		||||
            })
 | 
			
		||||
            app.test_client_class = Client
 | 
			
		||||
            return app
 | 
			
		||||
@@ -229,7 +228,7 @@ A unittest Test Case
 | 
			
		||||
            response = self.client.get("/admin")
 | 
			
		||||
            self.assertEqual(response.status_code, 401)
 | 
			
		||||
            response = self.client.get(
 | 
			
		||||
                "/admin", digest_auth=("my_name", "my_pass"))
 | 
			
		||||
                "/admin", digest_auth=(USERNAME, PASSWORD))
 | 
			
		||||
            self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -249,8 +248,9 @@ A pytest Test
 | 
			
		||||
    @pytest.fixture()
 | 
			
		||||
    def app():
 | 
			
		||||
        app: Flask = create_app({
 | 
			
		||||
            "TESTING": True,
 | 
			
		||||
            "SECRET_KEY": token_urlsafe(32),
 | 
			
		||||
            "TESTING": True
 | 
			
		||||
            "DIGEST_AUTH_REALM": "admin",
 | 
			
		||||
        })
 | 
			
		||||
        app.test_client_class = Client
 | 
			
		||||
        yield app
 | 
			
		||||
@@ -264,5 +264,5 @@ A pytest Test
 | 
			
		||||
            response = client.get("/admin")
 | 
			
		||||
            assert response.status_code == 401
 | 
			
		||||
            response = client.get(
 | 
			
		||||
                "/admin", digest_auth=("my_name", "my_pass"))
 | 
			
		||||
                "/admin", digest_auth=(USERNAME, PASSWORD))
 | 
			
		||||
            assert response.status_code == 200
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,37 @@
 | 
			
		||||
flask\_digest\_auth package
 | 
			
		||||
===========================
 | 
			
		||||
 | 
			
		||||
The ``DigestAuth`` Class
 | 
			
		||||
------------------------
 | 
			
		||||
.. autoclass:: flask_digest_auth.DigestAuth
 | 
			
		||||
    :members:
 | 
			
		||||
    :undoc-members:
 | 
			
		||||
    :show-inheritance:
 | 
			
		||||
Submodules
 | 
			
		||||
----------
 | 
			
		||||
 | 
			
		||||
The ``make_password_hash`` Function
 | 
			
		||||
-----------------------------------
 | 
			
		||||
.. autofunction:: flask_digest_auth.make_password_hash
 | 
			
		||||
flask\_digest\_auth.algo module
 | 
			
		||||
-------------------------------
 | 
			
		||||
 | 
			
		||||
The ``calc_response`` Function
 | 
			
		||||
------------------------------
 | 
			
		||||
.. autofunction:: flask_digest_auth.calc_response
 | 
			
		||||
.. automodule:: flask_digest_auth.algo
 | 
			
		||||
   :members:
 | 
			
		||||
   :undoc-members:
 | 
			
		||||
   :show-inheritance:
 | 
			
		||||
 | 
			
		||||
The ``Client`` Test Class
 | 
			
		||||
-------------------------
 | 
			
		||||
.. autoclass:: flask_digest_auth.Client
 | 
			
		||||
    :members:
 | 
			
		||||
    :undoc-members:
 | 
			
		||||
    :show-inheritance:
 | 
			
		||||
flask\_digest\_auth.auth module
 | 
			
		||||
-------------------------------
 | 
			
		||||
 | 
			
		||||
.. automodule:: flask_digest_auth.auth
 | 
			
		||||
   :members:
 | 
			
		||||
   :undoc-members:
 | 
			
		||||
   :show-inheritance:
 | 
			
		||||
 | 
			
		||||
flask\_digest\_auth.test module
 | 
			
		||||
-------------------------------
 | 
			
		||||
 | 
			
		||||
.. automodule:: flask_digest_auth.test
 | 
			
		||||
   :members:
 | 
			
		||||
   :undoc-members:
 | 
			
		||||
   :show-inheritance:
 | 
			
		||||
 | 
			
		||||
Module contents
 | 
			
		||||
---------------
 | 
			
		||||
 | 
			
		||||
.. automodule:: flask_digest_auth
 | 
			
		||||
   :members:
 | 
			
		||||
   :undoc-members:
 | 
			
		||||
   :show-inheritance:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
.. flask-digest-auth documentation master file, created by
 | 
			
		||||
   sphinx-quickstart on Tue Dec  6 15:15:08 2022.
 | 
			
		||||
.. Flask-DigestAuth documentation master file, created by
 | 
			
		||||
   sphinx-quickstart on Wed Dec  7 09:40:48 2022.
 | 
			
		||||
   You can adapt this file completely to your liking, but it should at least
 | 
			
		||||
   contain the root `toctree` directive.
 | 
			
		||||
 | 
			
		||||
Welcome to Flask-Digest-Auth's documentation!
 | 
			
		||||
=============================================
 | 
			
		||||
Welcome to Flask-DigestAuth's documentation!
 | 
			
		||||
============================================
 | 
			
		||||
 | 
			
		||||
*Flask-Digest-Auth* is an `HTTP Digest Authentication`_ implementation
 | 
			
		||||
*Flask-DigestAuth* is an `HTTP Digest Authentication`_ implementation
 | 
			
		||||
for Flask_ applications.  It authenticates the user for the protected
 | 
			
		||||
views.
 | 
			
		||||
 | 
			
		||||
@@ -19,6 +19,7 @@ HTTP Digest Authentication is specified in `RFC 2617`_.
 | 
			
		||||
   intro
 | 
			
		||||
   flask_digest_auth
 | 
			
		||||
   examples
 | 
			
		||||
   changelog
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -26,6 +27,7 @@ Indices and tables
 | 
			
		||||
==================
 | 
			
		||||
 | 
			
		||||
* :ref:`genindex`
 | 
			
		||||
* :ref:`modindex`
 | 
			
		||||
* :ref:`search`
 | 
			
		||||
 | 
			
		||||
.. _HTTP Digest Authentication: https://en.wikipedia.org/wiki/Digest_access_authentication
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ Introduction
 | 
			
		||||
============
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*Flask-Digest-Auth* is an `HTTP Digest Authentication`_ implementation
 | 
			
		||||
*Flask-DigestAuth* is an `HTTP Digest Authentication`_ implementation
 | 
			
		||||
for Flask_ applications.  It authenticates the user for the protected
 | 
			
		||||
views.
 | 
			
		||||
 | 
			
		||||
@@ -24,7 +24,7 @@ own challenge-response log in form, but then you are reinventing the
 | 
			
		||||
wheels.  If a pretty log in form is not critical to your project, HTTP
 | 
			
		||||
Digest Authentication should be a good choice.
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth works with Flask-Login_.  Log in protection can be
 | 
			
		||||
Flask-DigestAuth works with Flask-Login_.  Log in protection can be
 | 
			
		||||
separated with the authentication mechanism.  You can create protected
 | 
			
		||||
Flask modules without knowing the actual authentication mechanisms.
 | 
			
		||||
 | 
			
		||||
@@ -32,18 +32,25 @@ Flask modules without knowing the actual authentication mechanisms.
 | 
			
		||||
Installation
 | 
			
		||||
------------
 | 
			
		||||
 | 
			
		||||
You can install Flask-Digest-Auth with ``pip``:
 | 
			
		||||
You can install Flask-DigestAuth with ``pip``:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    pip install Flask-Digest-Auth
 | 
			
		||||
    pip install Flask-DigestAuth
 | 
			
		||||
 | 
			
		||||
You may also install the latest source from the
 | 
			
		||||
`Flask-Digest-Auth GitHub repository`_.
 | 
			
		||||
`Flask-DigestAuth GitHub repository`_.
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    pip install git+https://github.com/imacat/flask-digest-auth.git
 | 
			
		||||
    pip install git+https://github.com/imacat/flask-digestauth.git
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Configuration
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Flask-DigestAuth takes the configuration ``DIGEST_AUTH_REALM`` as the
 | 
			
		||||
realm.  The default realm is ``Login Required``.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Setting the Password
 | 
			
		||||
@@ -63,13 +70,13 @@ The username is part of the hash.  If the user changes their username,
 | 
			
		||||
you need to ask their password, to generate and store the new password
 | 
			
		||||
hash.
 | 
			
		||||
 | 
			
		||||
See :meth:`flask_digest_auth.make_password_hash`.
 | 
			
		||||
See :func:`flask_digest_auth.algo.make_password_hash`.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth Alone
 | 
			
		||||
-----------------------
 | 
			
		||||
Flask-DigestAuth Alone
 | 
			
		||||
----------------------
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth can authenticate the users alone.
 | 
			
		||||
Flask-DigestAuth can authenticate the users alone.
 | 
			
		||||
 | 
			
		||||
See :ref:`example-alone-simple` and :ref:`example-alone-large`.
 | 
			
		||||
 | 
			
		||||
@@ -77,12 +84,12 @@ See :ref:`example-alone-simple` and :ref:`example-alone-large`.
 | 
			
		||||
Flask-Login Integration
 | 
			
		||||
-----------------------
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth works with Flask-Login_.  You can write a Flask
 | 
			
		||||
Flask-DigestAuth works with Flask-Login_.  You can write a Flask
 | 
			
		||||
module that requires log in, without specifying how to log in.  The
 | 
			
		||||
application can use either HTTP Digest Authentication, or the log in
 | 
			
		||||
forms, as needed.
 | 
			
		||||
 | 
			
		||||
To use Flask-Login with Flask-Digest-Auth,
 | 
			
		||||
To use Flask-Login with Flask-DigestAuth,
 | 
			
		||||
``login_manager.init_app(app)`` must be called before
 | 
			
		||||
``auth.init_app(app)``.
 | 
			
		||||
 | 
			
		||||
@@ -92,7 +99,7 @@ The currently logged-in user can be retrieved at
 | 
			
		||||
See :ref:`example-flask-login-simple` and
 | 
			
		||||
:ref:`example-flask-login-large`.
 | 
			
		||||
 | 
			
		||||
The views only depend on Flask-Login, but not the Flask-Digest-Auth.
 | 
			
		||||
The views only depend on Flask-Login, but not the Flask-DigestAuth.
 | 
			
		||||
You can change the actual authentication mechanism without changing
 | 
			
		||||
the views.
 | 
			
		||||
 | 
			
		||||
@@ -100,7 +107,7 @@ the views.
 | 
			
		||||
Session Integration
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth features session integration.  The user log in
 | 
			
		||||
Flask-DigestAuth features session integration.  The user log in
 | 
			
		||||
is remembered in the session.  The authentication information is not
 | 
			
		||||
requested again.  This is different to the practice of the HTTP Digest
 | 
			
		||||
Authentication, but is convenient for the log in accounting.
 | 
			
		||||
@@ -118,25 +125,25 @@ logging the log in event, adding the log in counter, etc.
 | 
			
		||||
    def on_login(user: User) -> None:
 | 
			
		||||
        user.visits = user.visits + 1
 | 
			
		||||
 | 
			
		||||
See :meth:`flask_digest_auth.DigestAuth.register_on_login`.
 | 
			
		||||
See :meth:`flask_digest_auth.auth.DigestAuth.register_on_login`.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Log Out
 | 
			
		||||
-------
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth supports log out.  The user will be prompted for the
 | 
			
		||||
Flask-DigestAuth supports log out.  The user will be prompted for the
 | 
			
		||||
new username and password.
 | 
			
		||||
 | 
			
		||||
See :meth:`flask_digest_auth.DigestAuth.logout`.
 | 
			
		||||
See :meth:`flask_digest_auth.auth.DigestAuth.logout`.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Test Client
 | 
			
		||||
-----------
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth comes with a test client that supports HTTP digest
 | 
			
		||||
Flask-DigestAuth comes with a test client that supports HTTP digest
 | 
			
		||||
authentication.
 | 
			
		||||
 | 
			
		||||
See :class:`flask_digest_auth.Client`.
 | 
			
		||||
See :class:`flask_digest_auth.test.Client`.
 | 
			
		||||
 | 
			
		||||
Also see :ref:`example-unittest` and :ref:`example-pytest`.
 | 
			
		||||
 | 
			
		||||
@@ -145,4 +152,4 @@ Also see :ref:`example-unittest` and :ref:`example-pytest`.
 | 
			
		||||
.. _RFC 2617: https://www.rfc-editor.org/rfc/rfc2617
 | 
			
		||||
.. _Flask: https://flask.palletsprojects.com
 | 
			
		||||
.. _Flask-Login: https://flask-login.readthedocs.io
 | 
			
		||||
.. _Flask-Digest-Auth GitHub repository: https://github.com/imacat/flask-digest-auth
 | 
			
		||||
.. _Flask-DigestAuth GitHub repository: https://github.com/imacat/flask-digestauth
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								docs/source/modules.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								docs/source/modules.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
src
 | 
			
		||||
===
 | 
			
		||||
 | 
			
		||||
.. toctree::
 | 
			
		||||
   :maxdepth: 4
 | 
			
		||||
 | 
			
		||||
   flask_digest_auth
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# The Flask HTTP Digest Authentication Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/11/23
 | 
			
		||||
 | 
			
		||||
#  Copyright (c) 2022 imacat.
 | 
			
		||||
#  Copyright (c) 2022-2023 imacat.
 | 
			
		||||
#
 | 
			
		||||
#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
#  you may not use this file except in compliance with the License.
 | 
			
		||||
@@ -15,6 +15,43 @@
 | 
			
		||||
#  See the License for the specific language governing permissions and
 | 
			
		||||
#  limitations under the License.
 | 
			
		||||
 | 
			
		||||
[project]
 | 
			
		||||
name = "Flask-DigestAuth"
 | 
			
		||||
dynamic = ["version"]
 | 
			
		||||
description = "The Flask HTTP Digest Authentication project."
 | 
			
		||||
readme = "README.rst"
 | 
			
		||||
requires-python = ">=3.8"
 | 
			
		||||
authors = [
 | 
			
		||||
    {name = "imacat", email = "imacat@mail.imacat.idv.tw"},
 | 
			
		||||
]
 | 
			
		||||
keywords = ["flask", "digest-authentication"]
 | 
			
		||||
classifiers = [
 | 
			
		||||
    "Programming Language :: Python :: 3",
 | 
			
		||||
    "License :: OSI Approved :: Apache Software License",
 | 
			
		||||
    "Operating System :: OS Independent",
 | 
			
		||||
    "Framework :: Flask",
 | 
			
		||||
    "Topic :: System :: Systems Administration :: Authentication/Directory",
 | 
			
		||||
    "Intended Audience :: Developers",
 | 
			
		||||
]
 | 
			
		||||
dependencies = [
 | 
			
		||||
    "flask",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[project.optional-dependencies]
 | 
			
		||||
test = [
 | 
			
		||||
    "unittest",
 | 
			
		||||
    "flask-testing",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[project.urls]
 | 
			
		||||
"Documentation" = "https://flask-digestauth.readthedocs.io"
 | 
			
		||||
"Change Log" = "https://mia-accounting.readthedocs.io/en/latest/changelog.html"
 | 
			
		||||
"Repository" = "https://github.com/imacat/flask-digestauth"
 | 
			
		||||
"Bug Tracker" = "https://github.com/imacat/flask-digestauth/issues"
 | 
			
		||||
 | 
			
		||||
[build-system]
 | 
			
		||||
requires = ["setuptools>=42"]
 | 
			
		||||
build-backend = "setuptools.build_meta"
 | 
			
		||||
 | 
			
		||||
[tool.setuptools.dynamic]
 | 
			
		||||
version = {attr = "flask_digest_auth.VERSION"}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								setup.cfg
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								setup.cfg
									
									
									
									
									
								
							@@ -1,53 +0,0 @@
 | 
			
		||||
# The Flask HTTP Digest Authentication Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/11/23
 | 
			
		||||
 | 
			
		||||
#  Copyright (c) 2022 imacat.
 | 
			
		||||
#
 | 
			
		||||
#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
#  you may not use this file except in compliance with the License.
 | 
			
		||||
#  You may obtain a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#  Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#  distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
#  See the License for the specific language governing permissions and
 | 
			
		||||
#  limitations under the License.
 | 
			
		||||
 | 
			
		||||
[metadata]
 | 
			
		||||
name = flask-digest-auth
 | 
			
		||||
version = 0.3.0
 | 
			
		||||
author = imacat
 | 
			
		||||
author_email = imacat@mail.imacat.idv.tw
 | 
			
		||||
description = The Flask HTTP Digest Authentication project.
 | 
			
		||||
long_description = file: README.rst
 | 
			
		||||
long_description_content_type = text/x-rst
 | 
			
		||||
url = https://github.com/imacat/flask-digest-auth
 | 
			
		||||
project_urls =
 | 
			
		||||
    Bug Tracker = https://github.com/imacat/flask-digest-auth/issues
 | 
			
		||||
classifiers =
 | 
			
		||||
    Programming Language :: Python :: 3
 | 
			
		||||
    License :: OSI Approved :: Apache Software License
 | 
			
		||||
    Operating System :: OS Independent
 | 
			
		||||
    Framework :: Flask
 | 
			
		||||
    Topic :: System :: Systems Administration :: Authentication/Directory
 | 
			
		||||
    Intended Audience :: Developers
 | 
			
		||||
 | 
			
		||||
[options]
 | 
			
		||||
package_dir =
 | 
			
		||||
    = src
 | 
			
		||||
packages = find:
 | 
			
		||||
python_requires = >=3.7
 | 
			
		||||
install_requires =
 | 
			
		||||
  flask
 | 
			
		||||
tests_require =
 | 
			
		||||
  unittest
 | 
			
		||||
  flask-testing
 | 
			
		||||
 | 
			
		||||
[options.packages.find]
 | 
			
		||||
where = src
 | 
			
		||||
 | 
			
		||||
[options.extras_require]
 | 
			
		||||
flask_login =
 | 
			
		||||
    flask-login
 | 
			
		||||
@@ -21,3 +21,6 @@
 | 
			
		||||
from flask_digest_auth.algo import make_password_hash, calc_response
 | 
			
		||||
from flask_digest_auth.auth import DigestAuth
 | 
			
		||||
from flask_digest_auth.test import Client
 | 
			
		||||
 | 
			
		||||
VERSION: str = "0.6.0"
 | 
			
		||||
"""The package version."""
 | 
			
		||||
 
 | 
			
		||||
@@ -20,8 +20,8 @@
 | 
			
		||||
"""
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import typing as t
 | 
			
		||||
from hashlib import md5
 | 
			
		||||
from typing import Optional, Literal
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def make_password_hash(realm: str, username: str, password: str) -> str:
 | 
			
		||||
@@ -44,10 +44,10 @@ def make_password_hash(realm: str, username: str, password: str) -> str:
 | 
			
		||||
 | 
			
		||||
def calc_response(
 | 
			
		||||
        method: str, uri: str, password_hash: str,
 | 
			
		||||
        nonce: str, qop: t.Optional[t.Literal["auth", "auth-int"]] = None,
 | 
			
		||||
        algorithm: t.Optional[t.Literal["MD5", "MD5-sess"]] = "MD5-sess",
 | 
			
		||||
        cnonce: t.Optional[str] = None, nc: t.Optional[str] = None,
 | 
			
		||||
        body: t.Optional[bytes] = None) -> str:
 | 
			
		||||
        nonce: str, qop: Optional[Literal["auth", "auth-int"]] = None,
 | 
			
		||||
        algorithm: Optional[Literal["MD5", "MD5-sess"]] = "MD5-sess",
 | 
			
		||||
        cnonce: Optional[str] = None, nc: Optional[str] = None,
 | 
			
		||||
        body: Optional[bytes] = None) -> str:
 | 
			
		||||
    """Calculates the response value of the HTTP digest authentication.
 | 
			
		||||
 | 
			
		||||
    :param method: The request method.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# The Flask HTTP Digest Authentication Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/10/22
 | 
			
		||||
 | 
			
		||||
#  Copyright (c) 2022 imacat.
 | 
			
		||||
#  Copyright (c) 2022-2023 imacat.
 | 
			
		||||
#
 | 
			
		||||
#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
#  you may not use this file except in compliance with the License.
 | 
			
		||||
@@ -16,17 +16,19 @@
 | 
			
		||||
#  limitations under the License.
 | 
			
		||||
 | 
			
		||||
"""The HTTP Digest Authentication.
 | 
			
		||||
See RFC 2617 HTTP Authentication: Basic and Digest Access Authentication
 | 
			
		||||
See `RFC 2617`_ HTTP Authentication: Basic and Digest Access Authentication
 | 
			
		||||
 | 
			
		||||
.. _RFC 2617: https://www.rfc-editor.org/rfc/rfc2617
 | 
			
		||||
"""
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
import typing as t
 | 
			
		||||
from functools import wraps
 | 
			
		||||
from secrets import token_urlsafe, randbits
 | 
			
		||||
from typing import Any, Optional, Literal, Callable, List
 | 
			
		||||
 | 
			
		||||
from flask import g, request, Response, session, abort, Flask, Request
 | 
			
		||||
from flask import g, request, Response, session, abort, Flask, Request, \
 | 
			
		||||
    current_app
 | 
			
		||||
from itsdangerous import URLSafeTimedSerializer, BadData
 | 
			
		||||
from werkzeug.datastructures import Authorization
 | 
			
		||||
 | 
			
		||||
@@ -36,36 +38,37 @@ from flask_digest_auth.algo import calc_response
 | 
			
		||||
class DigestAuth:
 | 
			
		||||
    """The HTTP digest authentication."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, realm: t.Optional[str] = None):
 | 
			
		||||
    def __init__(self, realm: Optional[str] = None):
 | 
			
		||||
        """Constructs the HTTP digest authentication.
 | 
			
		||||
 | 
			
		||||
        :param realm: The realm.
 | 
			
		||||
        """
 | 
			
		||||
        self.__serializer: URLSafeTimedSerializer \
 | 
			
		||||
            = URLSafeTimedSerializer(token_urlsafe(32))
 | 
			
		||||
        self.realm: str = "" if realm is None else realm
 | 
			
		||||
        """The realm.  Default is an empty string."""
 | 
			
		||||
        self.algorithm: t.Optional[t.Literal["MD5", "MD5-sess"]] = None
 | 
			
		||||
        """The serializer to generate and validate the nonce and opaque."""
 | 
			
		||||
        self.realm: str = "Login Required" if realm is None else realm
 | 
			
		||||
        """The realm.  Default is "Login Required"."""
 | 
			
		||||
        self.algorithm: Optional[Literal["MD5", "MD5-sess"]] = None
 | 
			
		||||
        """The algorithm, either None, ``MD5``, or ``MD5-sess``.  Default is
 | 
			
		||||
        None."""
 | 
			
		||||
        self.use_opaque: bool = True
 | 
			
		||||
        """Whether to use an opaque.  Default is True."""
 | 
			
		||||
        self.__domain: t.List[str] = []
 | 
			
		||||
        self.__domain: List[str] = []
 | 
			
		||||
        """A list of directories that this username and password applies to.
 | 
			
		||||
        Default is empty."""
 | 
			
		||||
        self.__qop: t.List[t.Literal["auth", "auth-int"]] \
 | 
			
		||||
            = ["auth", "auth-int"]
 | 
			
		||||
        self.__qop: List[Literal["auth", "auth-int"]] = ["auth", "auth-int"]
 | 
			
		||||
        """A list of supported quality of protection supported, either
 | 
			
		||||
        ``qop``, ``auth-int``, both, or empty.  Default is both."""
 | 
			
		||||
        self.app: t.Optional[Flask] = None
 | 
			
		||||
        """The current Flask application."""
 | 
			
		||||
        self.__get_password_hash: BasePasswordHashGetter \
 | 
			
		||||
            = BasePasswordHashGetter()
 | 
			
		||||
        """The callback to return the password hash."""
 | 
			
		||||
        self.__get_user: BaseUserGetter = BaseUserGetter()
 | 
			
		||||
        """The callback to return the user."""
 | 
			
		||||
        self.__on_login: BaseOnLogInCallback = BaseOnLogInCallback()
 | 
			
		||||
        """The callback to run when the user logs in."""
 | 
			
		||||
 | 
			
		||||
    def login_required(self, view) -> t.Callable:
 | 
			
		||||
        """The view decorator for HTTP digest authentication.
 | 
			
		||||
    def login_required(self, view) -> Callable:
 | 
			
		||||
        """The view decorator for the HTTP digest authentication.
 | 
			
		||||
 | 
			
		||||
        :Example:
 | 
			
		||||
 | 
			
		||||
@@ -85,7 +88,7 @@ class DigestAuth:
 | 
			
		||||
        class NoLogInException(Exception):
 | 
			
		||||
            """The exception thrown when the user is not authorized."""
 | 
			
		||||
 | 
			
		||||
        def get_logged_in_user() -> t.Any:
 | 
			
		||||
        def get_logged_in_user() -> Any:
 | 
			
		||||
            """Returns the currently logged-in user.
 | 
			
		||||
 | 
			
		||||
            :return: The currently logged-in user.
 | 
			
		||||
@@ -93,13 +96,13 @@ class DigestAuth:
 | 
			
		||||
            """
 | 
			
		||||
            if "user" not in session:
 | 
			
		||||
                raise NoLogInException
 | 
			
		||||
            user: t.Optional[t.Any] = self.__get_user(session["user"])
 | 
			
		||||
            user: Optional[Any] = self.__get_user(session["user"])
 | 
			
		||||
            if user is None:
 | 
			
		||||
                del session["user"]
 | 
			
		||||
                raise NoLogInException
 | 
			
		||||
            return user
 | 
			
		||||
 | 
			
		||||
        def auth_user(state: AuthState) -> t.Any:
 | 
			
		||||
        def auth_user(state: AuthState) -> Any:
 | 
			
		||||
            """Authenticates a user.
 | 
			
		||||
 | 
			
		||||
            :param state: The authentication state.
 | 
			
		||||
@@ -117,7 +120,7 @@ class DigestAuth:
 | 
			
		||||
            return self.__get_user(authorization.username)
 | 
			
		||||
 | 
			
		||||
        @wraps(view)
 | 
			
		||||
        def login_required_view(*args, **kwargs) -> t.Any:
 | 
			
		||||
        def login_required_view(*args, **kwargs) -> Any:
 | 
			
		||||
            """The login-protected view.
 | 
			
		||||
 | 
			
		||||
            :param args: The positional arguments of the view.
 | 
			
		||||
@@ -167,7 +170,7 @@ class DigestAuth:
 | 
			
		||||
            except BadData:
 | 
			
		||||
                raise UnauthorizedException("Invalid opaque")
 | 
			
		||||
            state.opaque = authorization.opaque
 | 
			
		||||
        password_hash: t.Optional[str] \
 | 
			
		||||
        password_hash: Optional[str] \
 | 
			
		||||
            = self.__get_password_hash(authorization.username)
 | 
			
		||||
        if password_hash is None:
 | 
			
		||||
            raise UnauthorizedException(
 | 
			
		||||
@@ -198,7 +201,7 @@ class DigestAuth:
 | 
			
		||||
        :return: The ``WWW-Authenticate`` response header.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        def get_opaque() -> t.Optional[str]:
 | 
			
		||||
        def get_opaque() -> Optional[str]:
 | 
			
		||||
            """Returns the opaque value.
 | 
			
		||||
 | 
			
		||||
            :return: The opaque value.
 | 
			
		||||
@@ -209,7 +212,7 @@ class DigestAuth:
 | 
			
		||||
                return state.opaque
 | 
			
		||||
            return self.__serializer.dumps(randbits(32), salt="opaque")
 | 
			
		||||
 | 
			
		||||
        opaque: t.Optional[str] = get_opaque()
 | 
			
		||||
        opaque: Optional[str] = get_opaque()
 | 
			
		||||
        nonce: str = self.__serializer.dumps(
 | 
			
		||||
            randbits(32),
 | 
			
		||||
            salt="nonce" if opaque is None else f"nonce-{opaque}")
 | 
			
		||||
@@ -222,7 +225,7 @@ class DigestAuth:
 | 
			
		||||
        if opaque is not None:
 | 
			
		||||
            header += f", opaque=\"{opaque}\""
 | 
			
		||||
        if state.stale is not None:
 | 
			
		||||
            header += f", stale=TRUE" if state.stale else f", stale=FALSE"
 | 
			
		||||
            header += ", stale=TRUE" if state.stale else ", stale=FALSE"
 | 
			
		||||
        if self.algorithm is not None:
 | 
			
		||||
            header += f", algorithm=\"{self.algorithm}\""
 | 
			
		||||
        if len(self.__qop) > 0:
 | 
			
		||||
@@ -230,7 +233,7 @@ class DigestAuth:
 | 
			
		||||
            header += f", qop=\"{qop_list}\""
 | 
			
		||||
        return header
 | 
			
		||||
 | 
			
		||||
    def register_get_password(self, func: t.Callable[[str], t.Optional[str]])\
 | 
			
		||||
    def register_get_password(self, func: Callable[[str], Optional[str]]) \
 | 
			
		||||
            -> None:
 | 
			
		||||
        """The decorator to register the callback to obtain the password hash.
 | 
			
		||||
 | 
			
		||||
@@ -252,7 +255,7 @@ class DigestAuth:
 | 
			
		||||
            """The base password hash getter."""
 | 
			
		||||
 | 
			
		||||
            @staticmethod
 | 
			
		||||
            def __call__(username: str) -> t.Optional[str]:
 | 
			
		||||
            def __call__(username: str) -> Optional[str]:
 | 
			
		||||
                """Returns the password hash of a user.
 | 
			
		||||
 | 
			
		||||
                :param username: The username.
 | 
			
		||||
@@ -262,8 +265,7 @@ class DigestAuth:
 | 
			
		||||
 | 
			
		||||
        self.__get_password_hash = PasswordHashGetter()
 | 
			
		||||
 | 
			
		||||
    def register_get_user(self, func: t.Callable[[str], t.Optional[t.Any]])\
 | 
			
		||||
            -> None:
 | 
			
		||||
    def register_get_user(self, func: Callable[[str], Optional[Any]]) -> None:
 | 
			
		||||
        """The decorator to register the callback to obtain the user.
 | 
			
		||||
 | 
			
		||||
        :Example:
 | 
			
		||||
@@ -283,7 +285,7 @@ class DigestAuth:
 | 
			
		||||
            """The user getter."""
 | 
			
		||||
 | 
			
		||||
            @staticmethod
 | 
			
		||||
            def __call__(username: str) -> t.Optional[t.Any]:
 | 
			
		||||
            def __call__(username: str) -> Optional[Any]:
 | 
			
		||||
                """Returns a user.
 | 
			
		||||
 | 
			
		||||
                :param username: The username.
 | 
			
		||||
@@ -293,7 +295,7 @@ class DigestAuth:
 | 
			
		||||
 | 
			
		||||
        self.__get_user = UserGetter()
 | 
			
		||||
 | 
			
		||||
    def register_on_login(self, func: t.Callable[[t.Any], None]) -> None:
 | 
			
		||||
    def register_on_login(self, func: Callable[[Any], None]) -> None:
 | 
			
		||||
        """The decorator to register the callback to run when the user logs in.
 | 
			
		||||
 | 
			
		||||
        :Example:
 | 
			
		||||
@@ -312,7 +314,7 @@ class DigestAuth:
 | 
			
		||||
            """The callback when the user logs in."""
 | 
			
		||||
 | 
			
		||||
            @staticmethod
 | 
			
		||||
            def __call__(user: t.Any) -> None:
 | 
			
		||||
            def __call__(user: Any) -> None:
 | 
			
		||||
                """Runs the callback when the user logs in.
 | 
			
		||||
 | 
			
		||||
                :param user: The logged-in user.
 | 
			
		||||
@@ -323,7 +325,8 @@ class DigestAuth:
 | 
			
		||||
        self.__on_login = OnLogInCallback()
 | 
			
		||||
 | 
			
		||||
    def init_app(self, app: Flask) -> None:
 | 
			
		||||
        """Initializes the Flask application.
 | 
			
		||||
        """Initializes the Flask application.  The DigestAuth instance will
 | 
			
		||||
        be stored in ``app.extensions["digest_auth"]``.
 | 
			
		||||
 | 
			
		||||
        :Example:
 | 
			
		||||
 | 
			
		||||
@@ -331,58 +334,67 @@ class DigestAuth:
 | 
			
		||||
 | 
			
		||||
            app: flask = Flask(__name__)
 | 
			
		||||
            auth: DigestAuth = DigestAuth()
 | 
			
		||||
            auth.realm = "My Admin"
 | 
			
		||||
            auth.init_app(app)
 | 
			
		||||
 | 
			
		||||
        :param app: The Flask application.
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        app.digest_auth = self
 | 
			
		||||
        self.app = app
 | 
			
		||||
        app.extensions["digest_auth"] = self
 | 
			
		||||
        if "DIGEST_AUTH_REALM" in app.config:
 | 
			
		||||
            self.realm = app.config["DIGEST_AUTH_REALM"]
 | 
			
		||||
 | 
			
		||||
        if hasattr(app, "login_manager"):
 | 
			
		||||
            from flask_login import LoginManager, login_user
 | 
			
		||||
            self.__init_login_manager(app)
 | 
			
		||||
 | 
			
		||||
            login_manager: LoginManager = getattr(app, "login_manager")
 | 
			
		||||
    def __init_login_manager(self, app: Flask) -> None:
 | 
			
		||||
        """Initializes the Flask-Login login manager.
 | 
			
		||||
 | 
			
		||||
            @login_manager.unauthorized_handler
 | 
			
		||||
            def unauthorized() -> None:
 | 
			
		||||
                """Handles when the user is unauthorized.
 | 
			
		||||
        :param app: The Flask application.
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        from flask_login import LoginManager, login_user
 | 
			
		||||
        login_manager: LoginManager = getattr(app, "login_manager")
 | 
			
		||||
 | 
			
		||||
                :return: None.
 | 
			
		||||
                """
 | 
			
		||||
                response: Response = Response()
 | 
			
		||||
                response.status = 401
 | 
			
		||||
                response.headers["WWW-Authenticate"] \
 | 
			
		||||
                    = self.__make_response_header(g.digest_auth_state)
 | 
			
		||||
                abort(response)
 | 
			
		||||
        @login_manager.unauthorized_handler
 | 
			
		||||
        def unauthorized() -> None:
 | 
			
		||||
            """Handles when the user is unauthorized.
 | 
			
		||||
 | 
			
		||||
            @login_manager.request_loader
 | 
			
		||||
            def load_user_from_request(req: Request) -> t.Optional[t.Any]:
 | 
			
		||||
                """Loads the user from the request header.
 | 
			
		||||
            :return: None.
 | 
			
		||||
            """
 | 
			
		||||
            state: AuthState = getattr(request, "_digest_auth_state") \
 | 
			
		||||
                if hasattr(request, "_digest_auth_state") \
 | 
			
		||||
                else AuthState()
 | 
			
		||||
            response: Response = Response()
 | 
			
		||||
            response.status = 401
 | 
			
		||||
            response.headers["WWW-Authenticate"] \
 | 
			
		||||
                = self.__make_response_header(state)
 | 
			
		||||
            abort(response)
 | 
			
		||||
 | 
			
		||||
                :param req: The request.
 | 
			
		||||
                :return: The authenticated user, or None if the
 | 
			
		||||
                    authentication fails
 | 
			
		||||
                """
 | 
			
		||||
                g.digest_auth_state = AuthState()
 | 
			
		||||
                authorization: Authorization = req.authorization
 | 
			
		||||
                try:
 | 
			
		||||
                    if authorization is None:
 | 
			
		||||
                        raise UnauthorizedException
 | 
			
		||||
                    if authorization.type != "digest":
 | 
			
		||||
                        raise UnauthorizedException(
 | 
			
		||||
                            "Not an HTTP digest authorization")
 | 
			
		||||
                    self.__authenticate(g.digest_auth_state)
 | 
			
		||||
                    user = login_manager.user_callback(
 | 
			
		||||
                        authorization.username)
 | 
			
		||||
                    login_user(user)
 | 
			
		||||
                    self.__on_login(user)
 | 
			
		||||
                    return user
 | 
			
		||||
                except UnauthorizedException as e:
 | 
			
		||||
                    if str(e) != "":
 | 
			
		||||
                        app.logger.warning(str(e))
 | 
			
		||||
                    return None
 | 
			
		||||
        @login_manager.request_loader
 | 
			
		||||
        def load_user_from_request(req: Request) -> Optional[Any]:
 | 
			
		||||
            """Loads the user from the request header.
 | 
			
		||||
 | 
			
		||||
            :param req: The request.
 | 
			
		||||
            :return: The authenticated user, or None if the
 | 
			
		||||
                authentication fails
 | 
			
		||||
            """
 | 
			
		||||
            request._digest_auth_state = AuthState()
 | 
			
		||||
            authorization: Authorization = req.authorization
 | 
			
		||||
            try:
 | 
			
		||||
                if authorization is None:
 | 
			
		||||
                    raise UnauthorizedException
 | 
			
		||||
                if authorization.type != "digest":
 | 
			
		||||
                    raise UnauthorizedException(
 | 
			
		||||
                        "Not an HTTP digest authorization")
 | 
			
		||||
                self.__authenticate(request._digest_auth_state)
 | 
			
		||||
                user = login_manager.user_callback(authorization.username)
 | 
			
		||||
                login_user(user)
 | 
			
		||||
                self.__on_login(user)
 | 
			
		||||
                return user
 | 
			
		||||
            except UnauthorizedException as e:
 | 
			
		||||
                if str(e) != "":
 | 
			
		||||
                    app.logger.warning(str(e))
 | 
			
		||||
                return None
 | 
			
		||||
 | 
			
		||||
    def logout(self) -> None:
 | 
			
		||||
        """Logs out the user.
 | 
			
		||||
@@ -404,7 +416,7 @@ class DigestAuth:
 | 
			
		||||
        if "user" in session:
 | 
			
		||||
            del session["user"]
 | 
			
		||||
        try:
 | 
			
		||||
            if hasattr(self.app, "login_manager"):
 | 
			
		||||
            if hasattr(current_app, "login_manager"):
 | 
			
		||||
                from flask_login import logout_user
 | 
			
		||||
                logout_user()
 | 
			
		||||
        except ModuleNotFoundError:
 | 
			
		||||
@@ -413,24 +425,33 @@ class DigestAuth:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthState:
 | 
			
		||||
    """The authorization state."""
 | 
			
		||||
    """The authentication state.  It keeps the status in the earlier
 | 
			
		||||
    authentication stage, so that the latter response stage knows how to
 | 
			
		||||
    response.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        """Constructs the authorization state."""
 | 
			
		||||
        self.opaque: t.Optional[str] = None
 | 
			
		||||
        self.stale: t.Optional[bool] = None
 | 
			
		||||
        self.opaque: Optional[str] = None
 | 
			
		||||
        """The opaque value specified by the client, if valid."""
 | 
			
		||||
        self.stale: Optional[bool] = None
 | 
			
		||||
        """The stale value, if there is a previous log in attempt."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UnauthorizedException(Exception):
 | 
			
		||||
    """The exception thrown when the authentication is failed."""
 | 
			
		||||
    pass
 | 
			
		||||
    """The exception thrown when the authentication fails."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BasePasswordHashGetter:
 | 
			
		||||
    """The base password hash getter."""
 | 
			
		||||
    """The base callback that given the username, returns the password hash,
 | 
			
		||||
    or None if the user does not exist.  The default is to raise an
 | 
			
		||||
    :class:`UnboundLocalError` if the callback is not registered yet.
 | 
			
		||||
 | 
			
		||||
    See :meth:`flask_digest_auth.auth.DigestAuth.register_get_password`
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def __call__(username: str) -> t.Optional[str]:
 | 
			
		||||
    def __call__(username: str) -> Optional[str]:
 | 
			
		||||
        """Returns the password hash of a user.
 | 
			
		||||
 | 
			
		||||
        :param username: The username.
 | 
			
		||||
@@ -443,10 +464,15 @@ class BasePasswordHashGetter:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseUserGetter:
 | 
			
		||||
    """The base user getter."""
 | 
			
		||||
    """The base callback that given the username, returns the user, or None if
 | 
			
		||||
    the user does not exist.  The default is to raise an
 | 
			
		||||
    :class:`UnboundLocalError` if the callback is not registered yet.
 | 
			
		||||
 | 
			
		||||
    See :meth:`flask_digest_auth.auth.DigestAuth.register_get_user`
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def __call__(username: str) -> t.Optional[t.Any]:
 | 
			
		||||
    def __call__(username: str) -> Optional[Any]:
 | 
			
		||||
        """Returns a user.
 | 
			
		||||
 | 
			
		||||
        :param username: The username.
 | 
			
		||||
@@ -459,10 +485,14 @@ class BaseUserGetter:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseOnLogInCallback:
 | 
			
		||||
    """The base callback when the user logs in."""
 | 
			
		||||
    """The base callback to run when the user logs in, given the logged-in
 | 
			
		||||
    user.  The default does nothing.
 | 
			
		||||
 | 
			
		||||
    See :meth:`flask_digest_auth.auth.DigestAuth.register_on_login`
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def __call__(user: t.Any) -> None:
 | 
			
		||||
    def __call__(user: Any) -> None:
 | 
			
		||||
        """Runs the callback when the user logs in.
 | 
			
		||||
 | 
			
		||||
        :param user: The logged-in user.
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,8 @@
 | 
			
		||||
"""The test client with HTTP digest authentication enabled.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import typing as t
 | 
			
		||||
from secrets import token_urlsafe
 | 
			
		||||
from typing import Optional, Literal, Tuple, Dict
 | 
			
		||||
 | 
			
		||||
from flask import g
 | 
			
		||||
from werkzeug.datastructures import Authorization, WWWAuthenticate
 | 
			
		||||
@@ -51,7 +51,7 @@ class Client(WerkzeugClient):
 | 
			
		||||
                response = self.client.get("/admin")
 | 
			
		||||
                self.assertEqual(response.status_code, 401)
 | 
			
		||||
                response = self.client.get(
 | 
			
		||||
                    "/admin", digest_auth=("my_name", "my_pass"))
 | 
			
		||||
                    "/admin", digest_auth=(USERNAME, PASSWORD))
 | 
			
		||||
                self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    For pytest_:
 | 
			
		||||
@@ -76,23 +76,19 @@ class Client(WerkzeugClient):
 | 
			
		||||
                response = client.get("/admin")
 | 
			
		||||
                assert response.status_code == 401
 | 
			
		||||
                response = client.get(
 | 
			
		||||
                    "/admin", digest_auth=("my_name", "my_pass"))
 | 
			
		||||
                    "/admin", digest_auth=(USERNAME, PASSWORD))
 | 
			
		||||
                assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
    .. _unittest: https://docs.python.org/3/library/unittest.html
 | 
			
		||||
    .. _pytest: https://pytest.org
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def open(self, *args, digest_auth: t.Optional[t.Tuple[str, str]] = None,
 | 
			
		||||
    def open(self, *args, digest_auth: Optional[Tuple[str, str]] = None,
 | 
			
		||||
             **kwargs) -> TestResponse:
 | 
			
		||||
        """Opens a request.
 | 
			
		||||
 | 
			
		||||
        .. warning::
 | 
			
		||||
            This is to override the parent ``open`` method.  You should call
 | 
			
		||||
            the ``get``, ``post``, ``put``, and ``delete`` methods instead.
 | 
			
		||||
 | 
			
		||||
        :param args: The arguments.
 | 
			
		||||
        :param digest_auth: A tuple of the username and password for the HTTP
 | 
			
		||||
        :param digest_auth: The (*username*, *password*) tuple for the HTTP
 | 
			
		||||
            digest authentication.
 | 
			
		||||
        :param kwargs: The keyword arguments.
 | 
			
		||||
        :return: The response.
 | 
			
		||||
@@ -115,23 +111,20 @@ class Client(WerkzeugClient):
 | 
			
		||||
                           username: str, password: str) -> Authorization:
 | 
			
		||||
        """Composes and returns the request authorization.
 | 
			
		||||
 | 
			
		||||
        .. warning::
 | 
			
		||||
            This method is not for public.
 | 
			
		||||
 | 
			
		||||
        :param www_authenticate: The ``WWW-Authenticate`` response.
 | 
			
		||||
        :param uri: The request URI.
 | 
			
		||||
        :param username: The username.
 | 
			
		||||
        :param password: The password.
 | 
			
		||||
        :return: The request authorization.
 | 
			
		||||
        """
 | 
			
		||||
        qop: t.Optional[t.Literal["auth", "auth-int"]] = None
 | 
			
		||||
        qop: Optional[Literal["auth", "auth-int"]] = None
 | 
			
		||||
        if www_authenticate.qop is not None and "auth" in www_authenticate.qop:
 | 
			
		||||
            qop = "auth"
 | 
			
		||||
 | 
			
		||||
        cnonce: t.Optional[str] = None
 | 
			
		||||
        cnonce: Optional[str] = None
 | 
			
		||||
        if qop is not None or www_authenticate.algorithm == "MD5-sess":
 | 
			
		||||
            cnonce = token_urlsafe(8)
 | 
			
		||||
        nc: t.Optional[str] = None
 | 
			
		||||
        nc: Optional[str] = None
 | 
			
		||||
        count: int = 1
 | 
			
		||||
        if qop is not None:
 | 
			
		||||
            nc: str = hex(count)[2:].zfill(8)
 | 
			
		||||
@@ -144,7 +137,7 @@ class Client(WerkzeugClient):
 | 
			
		||||
            algorithm=www_authenticate.algorithm, cnonce=cnonce, nc=nc,
 | 
			
		||||
            body=None)
 | 
			
		||||
 | 
			
		||||
        data: t.Dict[str, str] = {
 | 
			
		||||
        data: Dict[str, str] = {
 | 
			
		||||
            "username": username, "realm": www_authenticate.realm,
 | 
			
		||||
            "nonce": www_authenticate.nonce, "uri": uri, "response": expected}
 | 
			
		||||
        if www_authenticate.algorithm is not None:
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,8 @@
 | 
			
		||||
"""The test case for the HTTP digest authentication algorithm.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import typing as t
 | 
			
		||||
import unittest
 | 
			
		||||
from typing import Optional, Literal
 | 
			
		||||
 | 
			
		||||
from flask_digest_auth import make_password_hash, calc_response
 | 
			
		||||
 | 
			
		||||
@@ -39,11 +39,11 @@ class AlgorithmTestCase(unittest.TestCase):
 | 
			
		||||
        method: str = "GET"
 | 
			
		||||
        uri: str = "/dir/index.html"
 | 
			
		||||
        nonce: str = "dcd98b7102dd2f0e8b11d0f600bfb0c093"
 | 
			
		||||
        qop: t.Optional[t.Literal["auth", "auth-int"]] = "auth"
 | 
			
		||||
        algorithm: t.Optional[t.Literal["MD5", "MD5-sess"]] = None
 | 
			
		||||
        cnonce: t.Optional[str] = "0a4f113b"
 | 
			
		||||
        nc: t.Optional[str] = "00000001"
 | 
			
		||||
        body: t.Optional[bytes] = None
 | 
			
		||||
        qop: Optional[Literal["auth", "auth-int"]] = "auth"
 | 
			
		||||
        algorithm: Optional[Literal["MD5", "MD5-sess"]] = None
 | 
			
		||||
        cnonce: Optional[str] = "0a4f113b"
 | 
			
		||||
        nc: Optional[str] = "00000001"
 | 
			
		||||
        body: Optional[bytes] = None
 | 
			
		||||
 | 
			
		||||
        password_hash: str = make_password_hash(realm, username, password)
 | 
			
		||||
        response: str = calc_response(method, uri, password_hash, nonce, qop,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# The Flask HTTP Digest Authentication Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/10/22
 | 
			
		||||
 | 
			
		||||
#  Copyright (c) 2022 imacat.
 | 
			
		||||
#  Copyright (c) 2022-2023 imacat.
 | 
			
		||||
#
 | 
			
		||||
#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
#  you may not use this file except in compliance with the License.
 | 
			
		||||
@@ -18,8 +18,8 @@
 | 
			
		||||
"""The test case for the HTTP digest authentication.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import typing as t
 | 
			
		||||
from secrets import token_urlsafe
 | 
			
		||||
from typing import Any, Optional, Dict
 | 
			
		||||
 | 
			
		||||
from flask import Response, Flask, g, redirect, request
 | 
			
		||||
from flask_testing import TestCase
 | 
			
		||||
@@ -57,18 +57,19 @@ class AuthenticationTestCase(TestCase):
 | 
			
		||||
        """
 | 
			
		||||
        app: Flask = Flask(__name__)
 | 
			
		||||
        app.config.from_mapping({
 | 
			
		||||
            "TESTING": True,
 | 
			
		||||
            "SECRET_KEY": token_urlsafe(32),
 | 
			
		||||
            "TESTING": True
 | 
			
		||||
            "DIGEST_AUTH_REALM": _REALM,
 | 
			
		||||
        })
 | 
			
		||||
        app.test_client_class = Client
 | 
			
		||||
 | 
			
		||||
        auth: DigestAuth = DigestAuth(realm=_REALM)
 | 
			
		||||
        auth: DigestAuth = DigestAuth()
 | 
			
		||||
        auth.init_app(app)
 | 
			
		||||
        self.user: User = User(_USERNAME, _PASSWORD)
 | 
			
		||||
        user_db: t.Dict[str, User] = {_USERNAME: self.user}
 | 
			
		||||
        user_db: Dict[str, User] = {_USERNAME: self.user}
 | 
			
		||||
 | 
			
		||||
        @auth.register_get_password
 | 
			
		||||
        def get_password_hash(username: str) -> t.Optional[str]:
 | 
			
		||||
        def get_password_hash(username: str) -> Optional[str]:
 | 
			
		||||
            """Returns the password hash of a user.
 | 
			
		||||
 | 
			
		||||
            :param username: The username.
 | 
			
		||||
@@ -78,7 +79,7 @@ class AuthenticationTestCase(TestCase):
 | 
			
		||||
                else None
 | 
			
		||||
 | 
			
		||||
        @auth.register_get_user
 | 
			
		||||
        def get_user(username: str) -> t.Optional[t.Any]:
 | 
			
		||||
        def get_user(username: str) -> Optional[Any]:
 | 
			
		||||
            """Returns a user.
 | 
			
		||||
 | 
			
		||||
            :param username: The username.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# The Flask HTTP Digest Authentication Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/11/23
 | 
			
		||||
 | 
			
		||||
#  Copyright (c) 2022 imacat.
 | 
			
		||||
#  Copyright (c) 2022-2023 imacat.
 | 
			
		||||
#
 | 
			
		||||
#  Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
#  you may not use this file except in compliance with the License.
 | 
			
		||||
@@ -18,8 +18,8 @@
 | 
			
		||||
"""The test case for the Flask-Login integration.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import typing as t
 | 
			
		||||
from secrets import token_urlsafe
 | 
			
		||||
from typing import Optional, Dict
 | 
			
		||||
 | 
			
		||||
from flask import Response, Flask, g, redirect, request
 | 
			
		||||
from flask_testing import TestCase
 | 
			
		||||
@@ -45,7 +45,6 @@ class User:
 | 
			
		||||
        self.password_hash: str = make_password_hash(
 | 
			
		||||
            _REALM, username, password)
 | 
			
		||||
        self.visits: int = 0
 | 
			
		||||
        self.is_authenticated: bool = True
 | 
			
		||||
        self.is_active: bool = True
 | 
			
		||||
        self.is_anonymous: bool = False
 | 
			
		||||
 | 
			
		||||
@@ -57,6 +56,16 @@ class User:
 | 
			
		||||
        """
 | 
			
		||||
        return self.username
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_authenticated(self) -> bool:
 | 
			
		||||
        """Returns whether the user is authenticated.
 | 
			
		||||
        This is required by Flask-Login.
 | 
			
		||||
        This should return self.is_active.
 | 
			
		||||
 | 
			
		||||
        :return: True if the user is active, or False otherwise.
 | 
			
		||||
        """
 | 
			
		||||
        return self.is_active
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FlaskLoginTestCase(TestCase):
 | 
			
		||||
    """The test case with the Flask-Login integration."""
 | 
			
		||||
@@ -68,8 +77,9 @@ class FlaskLoginTestCase(TestCase):
 | 
			
		||||
        """
 | 
			
		||||
        app: Flask = Flask(__name__)
 | 
			
		||||
        app.config.from_mapping({
 | 
			
		||||
            "TESTING": True,
 | 
			
		||||
            "SECRET_KEY": token_urlsafe(32),
 | 
			
		||||
            "TESTING": True
 | 
			
		||||
            "DIGEST_AUTH_REALM": _REALM,
 | 
			
		||||
        })
 | 
			
		||||
        app.test_client_class = Client
 | 
			
		||||
 | 
			
		||||
@@ -83,14 +93,14 @@ class FlaskLoginTestCase(TestCase):
 | 
			
		||||
        login_manager: flask_login.LoginManager = flask_login.LoginManager()
 | 
			
		||||
        login_manager.init_app(app)
 | 
			
		||||
 | 
			
		||||
        auth: DigestAuth = DigestAuth(realm=_REALM)
 | 
			
		||||
        auth: DigestAuth = DigestAuth()
 | 
			
		||||
        auth.init_app(app)
 | 
			
		||||
 | 
			
		||||
        self.user: User = User(_USERNAME, _PASSWORD)
 | 
			
		||||
        user_db: t.Dict[str, User] = {_USERNAME: self.user}
 | 
			
		||||
        user_db: Dict[str, User] = {_USERNAME: self.user}
 | 
			
		||||
 | 
			
		||||
        @auth.register_get_password
 | 
			
		||||
        def get_password_hash(username: str) -> t.Optional[str]:
 | 
			
		||||
        def get_password_hash(username: str) -> Optional[str]:
 | 
			
		||||
            """Returns the password hash of a user.
 | 
			
		||||
 | 
			
		||||
            :param username: The username.
 | 
			
		||||
@@ -109,7 +119,7 @@ class FlaskLoginTestCase(TestCase):
 | 
			
		||||
            user.visits = user.visits + 1
 | 
			
		||||
 | 
			
		||||
        @login_manager.user_loader
 | 
			
		||||
        def load_user(user_id: str) -> t.Optional[User]:
 | 
			
		||||
        def load_user(user_id: str) -> Optional[User]:
 | 
			
		||||
            """Loads a user.
 | 
			
		||||
 | 
			
		||||
            :param user_id: The username.
 | 
			
		||||
@@ -256,3 +266,34 @@ class FlaskLoginTestCase(TestCase):
 | 
			
		||||
        response = self.client.get(admin_uri)
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        self.assertEqual(self.user.visits, 2)
 | 
			
		||||
 | 
			
		||||
    def test_disabled(self) -> None:
 | 
			
		||||
        """Tests the disabled user.
 | 
			
		||||
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        if not self.has_flask_login:
 | 
			
		||||
            self.skipTest("Skipped without Flask-Login.")
 | 
			
		||||
 | 
			
		||||
        response: Response
 | 
			
		||||
 | 
			
		||||
        self.user.is_active = False
 | 
			
		||||
        response = self.client.get(self.app.url_for("admin-1"))
 | 
			
		||||
        self.assertEqual(response.status_code, 401)
 | 
			
		||||
        response = self.client.get(self.app.url_for("admin-1"),
 | 
			
		||||
                                   digest_auth=(_USERNAME, _PASSWORD))
 | 
			
		||||
        self.assertEqual(response.status_code, 401)
 | 
			
		||||
 | 
			
		||||
        self.user.is_active = True
 | 
			
		||||
        response = self.client.get(self.app.url_for("admin-1"),
 | 
			
		||||
                                   digest_auth=(_USERNAME, _PASSWORD))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        response = self.client.get(self.app.url_for("admin-1"))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.user.is_active = False
 | 
			
		||||
        response = self.client.get(self.app.url_for("admin-1"))
 | 
			
		||||
        self.assertEqual(response.status_code, 401)
 | 
			
		||||
        response = self.client.get(self.app.url_for("admin-1"),
 | 
			
		||||
                                   digest_auth=(_USERNAME, _PASSWORD))
 | 
			
		||||
        self.assertEqual(response.status_code, 401)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user