Compare commits
	
		
			66 Commits
		
	
	
		
			v0.2.1
			...
			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 | |||
| 4296756ae7 | |||
| 83cf83a67c | |||
| 7aaa7b9abe | |||
| 14b90de059 | |||
| e9013017fb | |||
| 6e6ac8bbe7 | |||
| 7ec56ee52d | |||
| b348c872dc | |||
| d78093ab53 | |||
| c6b8569543 | |||
| a1c48beb32 | |||
| a88fdf81fa | |||
| 4993bfdd9b | |||
| 696f350a44 | |||
| 409f794835 | |||
| 92eb011470 | |||
| b5ecd1552b | |||
| 765822a300 | |||
| e9a6449505 | |||
| df15f0b0d5 | |||
| 6c7f7e8c8e | |||
| dfc1108b41 | |||
| 4fe57532b0 | |||
| d104b0f28d | |||
| 30106c7e9f | |||
| 9997985d8c | |||
| 6057fc0987 | |||
| a6dc530ac7 | |||
| 6a14c04aaa | |||
| f9e10ecb2f | |||
| b6bfb2eae9 | |||
| e6b4594393 | 
							
								
								
									
										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,4 +15,7 @@
 | 
			
		||||
#  See the License for the specific language governing permissions and
 | 
			
		||||
#  limitations under the License.
 | 
			
		||||
 | 
			
		||||
include tests/*
 | 
			
		||||
recursive-include docs *
 | 
			
		||||
recursive-exclude docs/build *
 | 
			
		||||
recursive-include tests *
 | 
			
		||||
recursive-exclude tests *.pyc
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										403
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										403
									
								
								README.rst
									
									
									
									
									
								
							@@ -6,7 +6,7 @@ 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.
 | 
			
		||||
 | 
			
		||||
@@ -16,11 +16,11 @@ HTTP Digest Authentication is specified in `RFC 2617`_.
 | 
			
		||||
Why HTTP Digest Authentication?
 | 
			
		||||
-------------------------------
 | 
			
		||||
 | 
			
		||||
HTTP Digest Authentication has the advantage that it does not send the
 | 
			
		||||
actual password to the server, which greatly enhances the security.
 | 
			
		||||
It uses the challenge-response authentication scheme.  The client
 | 
			
		||||
returns the response calculated from the challenge and the password,
 | 
			
		||||
but not the original password.
 | 
			
		||||
*HTTP Digest Authentication* has the advantage that it does not send
 | 
			
		||||
thee actual password to the server, which greatly enhances the
 | 
			
		||||
security.  It uses the challenge-response authentication scheme.  The
 | 
			
		||||
client returns the response calculated from the challenge and the
 | 
			
		||||
password, but not the original password.
 | 
			
		||||
 | 
			
		||||
Log in forms has the advantage of freedom, in the senses of both the
 | 
			
		||||
visual design and the actual implementation.  You may implement your
 | 
			
		||||
@@ -28,405 +28,44 @@ 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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Features
 | 
			
		||||
--------
 | 
			
		||||
 | 
			
		||||
There are a couple of Flask HTTP digest authentication
 | 
			
		||||
implementations.  Flask-Digest-Auth has the following features:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Flask-Login Integration
 | 
			
		||||
#######################
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth features Flask-Login integration.  The views
 | 
			
		||||
can be totally independent with the actual authentication mechanism.
 | 
			
		||||
You can write a Flask module that requires log in, without specify
 | 
			
		||||
the actual authentication mechanism.  The application can specify
 | 
			
		||||
either HTTP Digest Authentication, or the log in forms, as needed.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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 Out Support
 | 
			
		||||
###############
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth supports log out.  The user will be prompted for
 | 
			
		||||
new username and password.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Log In Bookkeeping
 | 
			
		||||
##################
 | 
			
		||||
 | 
			
		||||
You can register a callback to run when the user logs in.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. _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-Login: https://flask-login.readthedocs.io
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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`_.
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    git clone git@github.com:imacat/flask-digest-auth.git
 | 
			
		||||
    cd flask-digest-auth
 | 
			
		||||
    pip install .
 | 
			
		||||
    pip install git+https://github.com/imacat/flask-digestauth.git
 | 
			
		||||
 | 
			
		||||
.. _Flask-Digest-Auth GitHub repository: https://github.com/imacat/flask-digest-auth
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth Alone
 | 
			
		||||
=======================
 | 
			
		||||
 | 
			
		||||
Flask-Digest-Auth can authenticate the users alone.
 | 
			
		||||
 | 
			
		||||
The currently logged-in user can be retrieved at ``g.user``, if any.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Example for 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"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Example for 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 can work with Flask-Login.  You can write a Flask
 | 
			
		||||
module that requires log in, without specifying the authentication
 | 
			
		||||
mechanism.  The Flask application can specify the actual
 | 
			
		||||
authentication mechanism as it sees fit.
 | 
			
		||||
 | 
			
		||||
``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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Example for 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"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Example for 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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Setting the Password Hash
 | 
			
		||||
=========================
 | 
			
		||||
 | 
			
		||||
The password hash of the HTTP Digest Authentication is composed of the
 | 
			
		||||
realm, the username, and the password.  Example for setting the
 | 
			
		||||
password:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    from flask_digest_auth import make_password_hash
 | 
			
		||||
 | 
			
		||||
    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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Log Out
 | 
			
		||||
=======
 | 
			
		||||
 | 
			
		||||
Call ``auth.logout()`` when the user wants to log out.
 | 
			
		||||
Besides the usual log out routine, ``auth.logout()`` actually causes
 | 
			
		||||
the next browser automatic authentication to fail, forcing the browser
 | 
			
		||||
to ask the user for the username and password again.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Writing Tests
 | 
			
		||||
Documentation
 | 
			
		||||
=============
 | 
			
		||||
 | 
			
		||||
You can write tests with our test client that handles HTTP Digest
 | 
			
		||||
Authentication.
 | 
			
		||||
 | 
			
		||||
Example for 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)
 | 
			
		||||
Refer to the `documentation on Read the Docs`_.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Change Log
 | 
			
		||||
==========
 | 
			
		||||
 | 
			
		||||
Example for 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 = self.client.get("/admin")
 | 
			
		||||
            assert response.status_code == 401
 | 
			
		||||
            response = self.client.get(
 | 
			
		||||
                "/admin", digest_auth=("my_name", "my_pass"))
 | 
			
		||||
            assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
.. _unittest: https://docs.python.org/3/library/unittest.html
 | 
			
		||||
.. _pytest: https://pytest.org
 | 
			
		||||
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.
 | 
			
		||||
@@ -447,3 +86,11 @@ Authors
 | 
			
		||||
| imacat
 | 
			
		||||
| imacat@mail.imacat.idv.tw
 | 
			
		||||
| 2022/11/23
 | 
			
		||||
 | 
			
		||||
.. _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-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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								docs/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								docs/Makefile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
# Minimal makefile for Sphinx documentation
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
# You can set these variables from the command line, and also
 | 
			
		||||
# from the environment for the first two.
 | 
			
		||||
SPHINXOPTS    ?=
 | 
			
		||||
SPHINXBUILD   ?= sphinx-build
 | 
			
		||||
SOURCEDIR     = source
 | 
			
		||||
BUILDDIR      = build
 | 
			
		||||
 | 
			
		||||
# Put it first so that "make" without argument is like "make help".
 | 
			
		||||
help:
 | 
			
		||||
	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 | 
			
		||||
 | 
			
		||||
.PHONY: help Makefile
 | 
			
		||||
 | 
			
		||||
# Catch-all target: route all unknown targets to Sphinx using the new
 | 
			
		||||
# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
 | 
			
		||||
%: Makefile
 | 
			
		||||
	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 | 
			
		||||
							
								
								
									
										35
									
								
								docs/make.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								docs/make.bat
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
@ECHO OFF
 | 
			
		||||
 | 
			
		||||
pushd %~dp0
 | 
			
		||||
 | 
			
		||||
REM Command file for Sphinx documentation
 | 
			
		||||
 | 
			
		||||
if "%SPHINXBUILD%" == "" (
 | 
			
		||||
	set SPHINXBUILD=sphinx-build
 | 
			
		||||
)
 | 
			
		||||
set SOURCEDIR=source
 | 
			
		||||
set BUILDDIR=build
 | 
			
		||||
 | 
			
		||||
%SPHINXBUILD% >NUL 2>NUL
 | 
			
		||||
if errorlevel 9009 (
 | 
			
		||||
	echo.
 | 
			
		||||
	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
 | 
			
		||||
	echo.installed, then set the SPHINXBUILD environment variable to point
 | 
			
		||||
	echo.to the full path of the 'sphinx-build' executable. Alternatively you
 | 
			
		||||
	echo.may add the Sphinx directory to PATH.
 | 
			
		||||
	echo.
 | 
			
		||||
	echo.If you don't have Sphinx installed, grab it from
 | 
			
		||||
	echo.https://www.sphinx-doc.org/
 | 
			
		||||
	exit /b 1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if "%1" == "" goto help
 | 
			
		||||
 | 
			
		||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
 | 
			
		||||
goto end
 | 
			
		||||
 | 
			
		||||
:help
 | 
			
		||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
 | 
			
		||||
 | 
			
		||||
:end
 | 
			
		||||
popd
 | 
			
		||||
							
								
								
									
										0
									
								
								docs/source/_static/.keep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								docs/source/_static/.keep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								docs/source/_templates/.keep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								docs/source/_templates/.keep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										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.
 | 
			
		||||
							
								
								
									
										33
									
								
								docs/source/conf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								docs/source/conf.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
# Configuration file for the Sphinx documentation builder.
 | 
			
		||||
#
 | 
			
		||||
# For the full list of built-in configuration values, see the documentation:
 | 
			
		||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
 | 
			
		||||
import os
 | 
			
		||||
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-DigestAuth'
 | 
			
		||||
copyright = '2022-2023, imacat'
 | 
			
		||||
author = 'imacat'
 | 
			
		||||
release = flask_digest_auth.VERSION
 | 
			
		||||
 | 
			
		||||
# -- General configuration ---------------------------------------------------
 | 
			
		||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 | 
			
		||||
 | 
			
		||||
extensions = ["sphinx.ext.autodoc"]
 | 
			
		||||
 | 
			
		||||
templates_path = ['_templates']
 | 
			
		||||
exclude_patterns = []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# -- Options for HTML output -------------------------------------------------
 | 
			
		||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
 | 
			
		||||
 | 
			
		||||
html_theme = 'sphinx_rtd_theme'
 | 
			
		||||
html_static_path = ['_static']
 | 
			
		||||
							
								
								
									
										268
									
								
								docs/source/examples.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								docs/source/examples.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,268 @@
 | 
			
		||||
Examples
 | 
			
		||||
========
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. _example-alone-simple:
 | 
			
		||||
 | 
			
		||||
Simple Applications with Flask-DigestAuth 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()
 | 
			
		||||
    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"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. _example-alone-large:
 | 
			
		||||
 | 
			
		||||
Larger Applications with ``create_app()`` with Flask-DigestAuth 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.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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. _example-flask-login-simple:
 | 
			
		||||
 | 
			
		||||
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()
 | 
			
		||||
    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"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. _example-flask-login-large:
 | 
			
		||||
 | 
			
		||||
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.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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. _example-unittest:
 | 
			
		||||
 | 
			
		||||
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({
 | 
			
		||||
                "TESTING": True,
 | 
			
		||||
                "SECRET_KEY": token_urlsafe(32),
 | 
			
		||||
                "DIGEST_AUTH_REALM": "admin",
 | 
			
		||||
            })
 | 
			
		||||
            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=(USERNAME, PASSWORD))
 | 
			
		||||
            self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. _example-pytest:
 | 
			
		||||
 | 
			
		||||
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({
 | 
			
		||||
            "TESTING": True,
 | 
			
		||||
            "SECRET_KEY": token_urlsafe(32),
 | 
			
		||||
            "DIGEST_AUTH_REALM": "admin",
 | 
			
		||||
        })
 | 
			
		||||
        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=(USERNAME, PASSWORD))
 | 
			
		||||
            assert response.status_code == 200
 | 
			
		||||
							
								
								
									
										37
									
								
								docs/source/flask_digest_auth.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								docs/source/flask_digest_auth.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
flask\_digest\_auth package
 | 
			
		||||
===========================
 | 
			
		||||
 | 
			
		||||
Submodules
 | 
			
		||||
----------
 | 
			
		||||
 | 
			
		||||
flask\_digest\_auth.algo module
 | 
			
		||||
-------------------------------
 | 
			
		||||
 | 
			
		||||
.. automodule:: flask_digest_auth.algo
 | 
			
		||||
   :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:
 | 
			
		||||
							
								
								
									
										35
									
								
								docs/source/index.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								docs/source/index.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
.. 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-DigestAuth's documentation!
 | 
			
		||||
============================================
 | 
			
		||||
 | 
			
		||||
*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`_.
 | 
			
		||||
 | 
			
		||||
.. toctree::
 | 
			
		||||
   :maxdepth: 2
 | 
			
		||||
   :caption: Contents:
 | 
			
		||||
 | 
			
		||||
   intro
 | 
			
		||||
   flask_digest_auth
 | 
			
		||||
   examples
 | 
			
		||||
   changelog
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Indices and tables
 | 
			
		||||
==================
 | 
			
		||||
 | 
			
		||||
* :ref:`genindex`
 | 
			
		||||
* :ref:`modindex`
 | 
			
		||||
* :ref:`search`
 | 
			
		||||
 | 
			
		||||
.. _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
 | 
			
		||||
							
								
								
									
										155
									
								
								docs/source/intro.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								docs/source/intro.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
			
		||||
Introduction
 | 
			
		||||
============
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
*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`_.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Why HTTP Digest Authentication?
 | 
			
		||||
-------------------------------
 | 
			
		||||
 | 
			
		||||
*HTTP Digest Authentication* has the advantage that it does not send
 | 
			
		||||
the actual password to the server, which greatly enhances the
 | 
			
		||||
security.  It uses the challenge-response authentication scheme.  The
 | 
			
		||||
client returns the response calculated from the challenge and the
 | 
			
		||||
password, but not the original password.
 | 
			
		||||
 | 
			
		||||
Log in forms has the advantage of freedom, in the senses of both the
 | 
			
		||||
visual design and the actual implementation.  You may implement your
 | 
			
		||||
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-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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Installation
 | 
			
		||||
------------
 | 
			
		||||
 | 
			
		||||
You can install Flask-DigestAuth with ``pip``:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    pip install Flask-DigestAuth
 | 
			
		||||
 | 
			
		||||
You may also install the latest source from the
 | 
			
		||||
`Flask-DigestAuth GitHub repository`_.
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
--------------------
 | 
			
		||||
 | 
			
		||||
The password hash of the HTTP Digest Authentication is composed of the
 | 
			
		||||
realm, the username, and the password.  Example for setting the
 | 
			
		||||
password:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    from flask_digest_auth import make_password_hash
 | 
			
		||||
 | 
			
		||||
    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.
 | 
			
		||||
 | 
			
		||||
See :func:`flask_digest_auth.algo.make_password_hash`.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Flask-DigestAuth Alone
 | 
			
		||||
----------------------
 | 
			
		||||
 | 
			
		||||
Flask-DigestAuth can authenticate the users alone.
 | 
			
		||||
 | 
			
		||||
See :ref:`example-alone-simple` and :ref:`example-alone-large`.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Flask-Login Integration
 | 
			
		||||
-----------------------
 | 
			
		||||
 | 
			
		||||
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-DigestAuth,
 | 
			
		||||
``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.
 | 
			
		||||
 | 
			
		||||
See :ref:`example-flask-login-simple` and
 | 
			
		||||
:ref:`example-flask-login-large`.
 | 
			
		||||
 | 
			
		||||
The views only depend on Flask-Login, but not the Flask-DigestAuth.
 | 
			
		||||
You can change the actual authentication mechanism without changing
 | 
			
		||||
the views.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Session Integration
 | 
			
		||||
-------------------
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
See :meth:`flask_digest_auth.auth.DigestAuth.register_on_login`.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Log Out
 | 
			
		||||
-------
 | 
			
		||||
 | 
			
		||||
Flask-DigestAuth supports log out.  The user will be prompted for the
 | 
			
		||||
new username and password.
 | 
			
		||||
 | 
			
		||||
See :meth:`flask_digest_auth.auth.DigestAuth.logout`.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Test Client
 | 
			
		||||
-----------
 | 
			
		||||
 | 
			
		||||
Flask-DigestAuth comes with a test client that supports HTTP digest
 | 
			
		||||
authentication.
 | 
			
		||||
 | 
			
		||||
See :class:`flask_digest_auth.test.Client`.
 | 
			
		||||
 | 
			
		||||
Also see :ref:`example-unittest` and :ref:`example-pytest`.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. _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-Login: https://flask-login.readthedocs.io
 | 
			
		||||
.. _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.2.1
 | 
			
		||||
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,14 +20,19 @@
 | 
			
		||||
"""
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import typing as t
 | 
			
		||||
from hashlib import md5
 | 
			
		||||
 | 
			
		||||
from flask_digest_auth.exception import UnauthorizedException
 | 
			
		||||
from typing import Optional, Literal
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def make_password_hash(realm: str, username: str, password: str) -> str:
 | 
			
		||||
    """Calculates the password hash for the HTTP digest authentication.
 | 
			
		||||
    Use this function to set the password for the user.
 | 
			
		||||
 | 
			
		||||
    :Example:
 | 
			
		||||
 | 
			
		||||
    ::
 | 
			
		||||
 | 
			
		||||
        user.password = make_password_hash(realm, username, password)
 | 
			
		||||
 | 
			
		||||
    :param realm: The realm.
 | 
			
		||||
    :param username: The username.
 | 
			
		||||
@@ -39,48 +44,38 @@ 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.
 | 
			
		||||
    :param uri: The request URI.
 | 
			
		||||
    :param password_hash: The password hash for the HTTP digest authentication.
 | 
			
		||||
    :param nonce: The nonce.
 | 
			
		||||
    :param qop: the quality of protection.
 | 
			
		||||
    :param algorithm: The algorithm, either "MD5" or "MD5-sess".
 | 
			
		||||
    :param qop: The quality of protection, either ``auth`` or ``auth-int``.
 | 
			
		||||
    :param algorithm: The algorithm, either ``MD5`` or ``MD5-sess``.
 | 
			
		||||
    :param cnonce: The client nonce, which must exists when qop exists or
 | 
			
		||||
        algorithm="MD5-sess".
 | 
			
		||||
        algorithm is ``MD5-sess``.
 | 
			
		||||
    :param nc: The request counter, which must exists when qop exists.
 | 
			
		||||
    :param body: The request body, which must exists when qop="auth-int".
 | 
			
		||||
    :param body: The request body, which must exists when qop is ``auth-int``.
 | 
			
		||||
    :return: The response value.
 | 
			
		||||
    :raise UnauthorizedException: When the cnonce is missing with the MD5-sess
 | 
			
		||||
        algorithm, when the body is missing with the auth-int qop, or when the
 | 
			
		||||
        cnonce or nc is missing with the auth or auth-int qop.
 | 
			
		||||
    :raise AssertionError: When cnonce is missing with algorithm is
 | 
			
		||||
        ``MD5-sess``, when body is missing with qop is ``auth-int``, or when
 | 
			
		||||
        cnonce or nc is missing with qop exits.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def validate_required(field: t.Optional[str], error: str) -> None:
 | 
			
		||||
        """Validates a required field.
 | 
			
		||||
 | 
			
		||||
        :param field: The field that is required.
 | 
			
		||||
        :param error: The error message.
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        if field is None:
 | 
			
		||||
            raise UnauthorizedException(error)
 | 
			
		||||
 | 
			
		||||
    def calc_ha1() -> str:
 | 
			
		||||
        """Calculates and returns the first hash.
 | 
			
		||||
 | 
			
		||||
        :return: The first hash.
 | 
			
		||||
        :raise UnauthorizedException: When the cnonce is missing with the MD5-sess
 | 
			
		||||
            algorithm.
 | 
			
		||||
        :raise AssertionError: When cnonce is missing with
 | 
			
		||||
            algorithm is ``MD5-sess``.
 | 
			
		||||
        """
 | 
			
		||||
        if algorithm == "MD5-sess":
 | 
			
		||||
            validate_required(
 | 
			
		||||
                cnonce, f"Missing \"cnonce\" with algorithm=\"{algorithm}\"")
 | 
			
		||||
            assert cnonce is not None,\
 | 
			
		||||
                f"Missing \"cnonce\" with algorithm=\"{algorithm}\""
 | 
			
		||||
            return md5(f"{password_hash}:{nonce}:{cnonce}".encode("utf8")) \
 | 
			
		||||
                .hexdigest()
 | 
			
		||||
        # algorithm is None or algorithm == "MD5"
 | 
			
		||||
@@ -90,11 +85,10 @@ def calc_response(
 | 
			
		||||
        """Calculates the second hash.
 | 
			
		||||
 | 
			
		||||
        :return: The second hash.
 | 
			
		||||
        :raise UnauthorizedException: When the body is missing with
 | 
			
		||||
            qop="auth-int".
 | 
			
		||||
        :raise AssertionError: When body is missing with qop is ``auth-int``.
 | 
			
		||||
        """
 | 
			
		||||
        if qop == "auth-int":
 | 
			
		||||
            validate_required(body, f"Missing \"body\" with qop=\"{qop}\"")
 | 
			
		||||
            assert body is not None, f"Missing \"body\" with qop=\"{qop}\""
 | 
			
		||||
            return md5(
 | 
			
		||||
                f"{method}:{uri}:{md5(body).hexdigest()}".encode("utf8")) \
 | 
			
		||||
                .hexdigest()
 | 
			
		||||
@@ -104,8 +98,8 @@ def calc_response(
 | 
			
		||||
    ha1: str = calc_ha1()
 | 
			
		||||
    ha2: str = calc_ha2()
 | 
			
		||||
    if qop == "auth" or qop == "auth-int":
 | 
			
		||||
        validate_required(cnonce, f"Missing \"cnonce\" with the qop=\"{qop}\"")
 | 
			
		||||
        validate_required(nc, f"Missing \"nc\" with the qop=\"{qop}\"")
 | 
			
		||||
        assert cnonce is not None, f"Missing \"cnonce\" with the qop=\"{qop}\""
 | 
			
		||||
        assert nc is not None, f"Missing \"nc\" with the qop=\"{qop}\""
 | 
			
		||||
        return md5(f"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}".encode("utf8"))\
 | 
			
		||||
            .hexdigest()
 | 
			
		||||
    # qop is None
 | 
			
		||||
 
 | 
			
		||||
@@ -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,101 +16,79 @@
 | 
			
		||||
#  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
 | 
			
		||||
 | 
			
		||||
from flask_digest_auth.algo import calc_response
 | 
			
		||||
from flask_digest_auth.exception import UnauthorizedException
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BasePasswordHashGetter:
 | 
			
		||||
    """The base password hash getter."""
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def __call__(username: str) -> t.Optional[str]:
 | 
			
		||||
        """Returns the password hash of a user.
 | 
			
		||||
 | 
			
		||||
        :param username: The username.
 | 
			
		||||
        :return: The password hash, or None if the user does not exist.
 | 
			
		||||
        :raise UnboundLocalError: When the password hash getter function is
 | 
			
		||||
            not registered yet.
 | 
			
		||||
        """
 | 
			
		||||
        raise UnboundLocalError("The function to return the password hash"
 | 
			
		||||
                                " was not registered yet.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseUserGetter:
 | 
			
		||||
    """The base user getter."""
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def __call__(username: str) -> t.Optional[t.Any]:
 | 
			
		||||
        """Returns a user.
 | 
			
		||||
 | 
			
		||||
        :param username: The username.
 | 
			
		||||
        :return: The user, or None if the user does not exist.
 | 
			
		||||
        :raise UnboundLocalError: When the user getter function is not
 | 
			
		||||
            registered yet.
 | 
			
		||||
        """
 | 
			
		||||
        raise UnboundLocalError("The function to return the user"
 | 
			
		||||
                                " was not registered yet.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseOnLogInCallback:
 | 
			
		||||
    """The base callback when the user logs in."""
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def __call__(user: t.Any) -> None:
 | 
			
		||||
        """Runs the callback when the user logs in.
 | 
			
		||||
 | 
			
		||||
        :param user: The logged-in user.
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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.secret_key: str = token_urlsafe(32)
 | 
			
		||||
        self.serializer: URLSafeTimedSerializer \
 | 
			
		||||
            = URLSafeTimedSerializer(self.secret_key)
 | 
			
		||||
        self.realm: str = "" if realm is None else realm
 | 
			
		||||
        self.algorithm: t.Optional[str] = None
 | 
			
		||||
        self.__serializer: URLSafeTimedSerializer \
 | 
			
		||||
            = URLSafeTimedSerializer(token_urlsafe(32))
 | 
			
		||||
        """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
 | 
			
		||||
        self.domain: t.List[str] = []
 | 
			
		||||
        self.qop: t.List[str] = ["auth", "auth-int"]
 | 
			
		||||
        self.app: t.Optional[Flask] = None
 | 
			
		||||
        """Whether to use an opaque.  Default is True."""
 | 
			
		||||
        self.__domain: List[str] = []
 | 
			
		||||
        """A list of directories that this username and password applies to.
 | 
			
		||||
        Default is empty."""
 | 
			
		||||
        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.__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.
 | 
			
		||||
 | 
			
		||||
        :param view:
 | 
			
		||||
        :Example:
 | 
			
		||||
 | 
			
		||||
        ::
 | 
			
		||||
 | 
			
		||||
            @app.get("/admin")
 | 
			
		||||
            @auth.login_required
 | 
			
		||||
            def admin():
 | 
			
		||||
                return f"Hello, {g.user.username}!"
 | 
			
		||||
 | 
			
		||||
        The logged-in user can be retrieved at ``g.user``.
 | 
			
		||||
 | 
			
		||||
        :param view: The view.
 | 
			
		||||
        :return: The login-protected view.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        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.
 | 
			
		||||
@@ -118,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.
 | 
			
		||||
@@ -137,12 +115,12 @@ class DigestAuth:
 | 
			
		||||
            if authorization.type != "digest":
 | 
			
		||||
                raise UnauthorizedException(
 | 
			
		||||
                    "Not an HTTP digest authorization")
 | 
			
		||||
            self.authenticate(state)
 | 
			
		||||
            self.__authenticate(state)
 | 
			
		||||
            session["user"] = authorization.username
 | 
			
		||||
            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.
 | 
			
		||||
@@ -166,12 +144,12 @@ class DigestAuth:
 | 
			
		||||
                response: Response = Response()
 | 
			
		||||
                response.status = 401
 | 
			
		||||
                response.headers["WWW-Authenticate"] \
 | 
			
		||||
                    = self.make_response_header(state)
 | 
			
		||||
                    = self.__make_response_header(state)
 | 
			
		||||
                abort(response)
 | 
			
		||||
 | 
			
		||||
        return login_required_view
 | 
			
		||||
 | 
			
		||||
    def authenticate(self, state: AuthState) -> None:
 | 
			
		||||
    def __authenticate(self, state: AuthState) -> None:
 | 
			
		||||
        """Authenticate a user.
 | 
			
		||||
 | 
			
		||||
        :param state: The authorization state.
 | 
			
		||||
@@ -187,12 +165,12 @@ class DigestAuth:
 | 
			
		||||
                raise UnauthorizedException(
 | 
			
		||||
                    "Missing \"opaque\" in the Authorization header")
 | 
			
		||||
            try:
 | 
			
		||||
                self.serializer.loads(
 | 
			
		||||
                self.__serializer.loads(
 | 
			
		||||
                    authorization.opaque, salt="opaque", max_age=1800)
 | 
			
		||||
            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(
 | 
			
		||||
@@ -208,7 +186,7 @@ class DigestAuth:
 | 
			
		||||
            state.stale = False
 | 
			
		||||
            raise UnauthorizedException("Incorrect response value")
 | 
			
		||||
        try:
 | 
			
		||||
            self.serializer.loads(
 | 
			
		||||
            self.__serializer.loads(
 | 
			
		||||
                authorization.nonce,
 | 
			
		||||
                salt="nonce" if authorization.opaque is None
 | 
			
		||||
                else f"nonce-{authorization.opaque}")
 | 
			
		||||
@@ -216,14 +194,14 @@ class DigestAuth:
 | 
			
		||||
            state.stale = True
 | 
			
		||||
            raise UnauthorizedException("Invalid nonce")
 | 
			
		||||
 | 
			
		||||
    def make_response_header(self, state: AuthState) -> str:
 | 
			
		||||
        """Composes and returns the WWW-Authenticate response header.
 | 
			
		||||
    def __make_response_header(self, state: AuthState) -> str:
 | 
			
		||||
        """Composes and returns the ``WWW-Authenticate`` response header.
 | 
			
		||||
 | 
			
		||||
        :param state: The authorization state.
 | 
			
		||||
        :return: The WWW-Authenticate response header.
 | 
			
		||||
        :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.
 | 
			
		||||
@@ -232,32 +210,41 @@ class DigestAuth:
 | 
			
		||||
                return None
 | 
			
		||||
            if state.opaque is not None:
 | 
			
		||||
                return state.opaque
 | 
			
		||||
            return self.serializer.dumps(randbits(32), salt="opaque")
 | 
			
		||||
            return self.__serializer.dumps(randbits(32), salt="opaque")
 | 
			
		||||
 | 
			
		||||
        opaque: t.Optional[str] = get_opaque()
 | 
			
		||||
        nonce: str = self.serializer.dumps(
 | 
			
		||||
        opaque: Optional[str] = get_opaque()
 | 
			
		||||
        nonce: str = self.__serializer.dumps(
 | 
			
		||||
            randbits(32),
 | 
			
		||||
            salt="nonce" if opaque is None else f"nonce-{opaque}")
 | 
			
		||||
 | 
			
		||||
        header: str = f"Digest realm=\"{self.realm}\""
 | 
			
		||||
        if len(self.domain) > 0:
 | 
			
		||||
            domain_list: str = ",".join(self.domain)
 | 
			
		||||
        if len(self.__domain) > 0:
 | 
			
		||||
            domain_list: str = ",".join(self.__domain)
 | 
			
		||||
            header += f", domain=\"{domain_list}\""
 | 
			
		||||
        header += f", nonce=\"{nonce}\""
 | 
			
		||||
        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:
 | 
			
		||||
            qop_list: str = ",".join(self.qop)
 | 
			
		||||
        if len(self.__qop) > 0:
 | 
			
		||||
            qop_list: str = ",".join(self.__qop)
 | 
			
		||||
            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:
 | 
			
		||||
        """Registers the callback to obtain the password hash.
 | 
			
		||||
        """The decorator to register the callback to obtain the password hash.
 | 
			
		||||
 | 
			
		||||
        :Example:
 | 
			
		||||
 | 
			
		||||
        ::
 | 
			
		||||
 | 
			
		||||
            @auth.register_get_password
 | 
			
		||||
            def get_password_hash(username: str) -> Optional[str]:
 | 
			
		||||
                user = User.query.filter(User.username == username).first()
 | 
			
		||||
                return None if user is None else user.password
 | 
			
		||||
 | 
			
		||||
        :param func: The callback that given the username, returns the password
 | 
			
		||||
            hash, or None if the user does not exist.
 | 
			
		||||
@@ -268,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.
 | 
			
		||||
@@ -278,9 +265,16 @@ class DigestAuth:
 | 
			
		||||
 | 
			
		||||
        self.__get_password_hash = PasswordHashGetter()
 | 
			
		||||
 | 
			
		||||
    def register_get_user(self, func: t.Callable[[str], t.Optional[t.Any]])\
 | 
			
		||||
            -> None:
 | 
			
		||||
        """Registers the callback to obtain the user.
 | 
			
		||||
    def register_get_user(self, func: Callable[[str], Optional[Any]]) -> None:
 | 
			
		||||
        """The decorator to register the callback to obtain the user.
 | 
			
		||||
 | 
			
		||||
        :Example:
 | 
			
		||||
 | 
			
		||||
        ::
 | 
			
		||||
 | 
			
		||||
            @auth.register_get_user
 | 
			
		||||
            def get_user(username: str) -> Optional[User]:
 | 
			
		||||
                return User.query.filter(User.username == username).first()
 | 
			
		||||
 | 
			
		||||
        :param func: The callback that given the username, returns the user,
 | 
			
		||||
            or None if the user does not exist.
 | 
			
		||||
@@ -291,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.
 | 
			
		||||
@@ -301,8 +295,16 @@ class DigestAuth:
 | 
			
		||||
 | 
			
		||||
        self.__get_user = UserGetter()
 | 
			
		||||
 | 
			
		||||
    def register_on_login(self, func: t.Callable[[t.Any], None]) -> None:
 | 
			
		||||
        """Registers the callback when the user logs in.
 | 
			
		||||
    def register_on_login(self, func: Callable[[Any], None]) -> None:
 | 
			
		||||
        """The decorator to register the callback to run when the user logs in.
 | 
			
		||||
 | 
			
		||||
        :Example:
 | 
			
		||||
 | 
			
		||||
        ::
 | 
			
		||||
 | 
			
		||||
            @auth.register_on_login
 | 
			
		||||
            def on_login(user: User) -> None:
 | 
			
		||||
                user.visits = user.visits + 1
 | 
			
		||||
 | 
			
		||||
        :param func: The callback given the logged-in user.
 | 
			
		||||
        :return: None.
 | 
			
		||||
@@ -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,69 +325,98 @@ 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:
 | 
			
		||||
 | 
			
		||||
        ::
 | 
			
		||||
 | 
			
		||||
            app: flask = Flask(__name__)
 | 
			
		||||
            auth: DigestAuth = DigestAuth()
 | 
			
		||||
            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.
 | 
			
		||||
        This actually causes the next authentication to fail, which forces
 | 
			
		||||
        the browser to ask the user for the username and password again.
 | 
			
		||||
 | 
			
		||||
        :Example:
 | 
			
		||||
 | 
			
		||||
        ::
 | 
			
		||||
 | 
			
		||||
            @app.post("/logout")
 | 
			
		||||
            @auth.login_required
 | 
			
		||||
            def logout():
 | 
			
		||||
                auth.logout()
 | 
			
		||||
                return redirect(request.form.get("next"))
 | 
			
		||||
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        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:
 | 
			
		||||
@@ -394,9 +425,76 @@ 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 fails."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BasePasswordHashGetter:
 | 
			
		||||
    """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) -> Optional[str]:
 | 
			
		||||
        """Returns the password hash of a user.
 | 
			
		||||
 | 
			
		||||
        :param username: The username.
 | 
			
		||||
        :return: The password hash, or None if the user does not exist.
 | 
			
		||||
        :raise UnboundLocalError: When the password hash getter function is
 | 
			
		||||
            not registered yet.
 | 
			
		||||
        """
 | 
			
		||||
        raise UnboundLocalError("The function to return the password hash"
 | 
			
		||||
                                " was not registered yet.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseUserGetter:
 | 
			
		||||
    """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) -> Optional[Any]:
 | 
			
		||||
        """Returns a user.
 | 
			
		||||
 | 
			
		||||
        :param username: The username.
 | 
			
		||||
        :return: The user, or None if the user does not exist.
 | 
			
		||||
        :raise UnboundLocalError: When the user getter function is not
 | 
			
		||||
            registered yet.
 | 
			
		||||
        """
 | 
			
		||||
        raise UnboundLocalError("The function to return the user"
 | 
			
		||||
                                " was not registered yet.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseOnLogInCallback:
 | 
			
		||||
    """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: Any) -> None:
 | 
			
		||||
        """Runs the callback when the user logs in.
 | 
			
		||||
 | 
			
		||||
        :param user: The logged-in user.
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
# The Flask HTTP Digest Authentication Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/11/3
 | 
			
		||||
 | 
			
		||||
#  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.
 | 
			
		||||
 | 
			
		||||
"""The exception.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UnauthorizedException(Exception):
 | 
			
		||||
    """The exception thrown when the authentication is failed."""
 | 
			
		||||
    pass
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -29,15 +29,67 @@ from flask_digest_auth.algo import calc_response, make_password_hash
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Client(WerkzeugClient):
 | 
			
		||||
    """The test client with HTTP digest authentication enabled."""
 | 
			
		||||
    """The test client with HTTP digest authentication enabled.
 | 
			
		||||
 | 
			
		||||
    def open(self, *args, digest_auth: t.Optional[t.Tuple[str, str]] = None,
 | 
			
		||||
    :Example:
 | 
			
		||||
 | 
			
		||||
    For unittest_:
 | 
			
		||||
 | 
			
		||||
    ::
 | 
			
		||||
 | 
			
		||||
        class MyTestCase(flask_testing.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=(USERNAME, PASSWORD))
 | 
			
		||||
                self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    For pytest_:
 | 
			
		||||
 | 
			
		||||
    ::
 | 
			
		||||
 | 
			
		||||
        @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=(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: Optional[Tuple[str, str]] = None,
 | 
			
		||||
             **kwargs) -> TestResponse:
 | 
			
		||||
        """Opens a request.
 | 
			
		||||
 | 
			
		||||
        :param args: The arguments.
 | 
			
		||||
        :param digest_auth: The username and password for the HTTP digest
 | 
			
		||||
            authentication.
 | 
			
		||||
        :param digest_auth: The (*username*, *password*) tuple for the HTTP
 | 
			
		||||
            digest authentication.
 | 
			
		||||
        :param kwargs: The keyword arguments.
 | 
			
		||||
        :return: The response.
 | 
			
		||||
        """
 | 
			
		||||
@@ -59,20 +111,20 @@ class Client(WerkzeugClient):
 | 
			
		||||
                           username: str, password: str) -> Authorization:
 | 
			
		||||
        """Composes and returns the request authorization.
 | 
			
		||||
 | 
			
		||||
        :param www_authenticate: The WWW-Authenticate response.
 | 
			
		||||
        :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)
 | 
			
		||||
@@ -85,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