Compare commits
	
		
			21 Commits
		
	
	
		
			v0.2.1
			...
			a88fdf81fa
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a88fdf81fa | |||
| 4993bfdd9b | |||
| 696f350a44 | |||
| 409f794835 | |||
| 92eb011470 | |||
| b5ecd1552b | |||
| 765822a300 | |||
| e9a6449505 | |||
| df15f0b0d5 | |||
| 6c7f7e8c8e | |||
| dfc1108b41 | |||
| 4fe57532b0 | |||
| d104b0f28d | |||
| 30106c7e9f | |||
| 9997985d8c | |||
| 6057fc0987 | |||
| a6dc530ac7 | |||
| 6a14c04aaa | |||
| f9e10ecb2f | |||
| b6bfb2eae9 | |||
| e6b4594393 | 
| @@ -15,4 +15,8 @@ | ||||
| #  See the License for the specific language governing permissions and | ||||
| #  limitations under the License. | ||||
|  | ||||
| include docs/* | ||||
| include docs/source/* | ||||
| include docs/source/_static/* | ||||
| include docs/source/_templates/* | ||||
| include tests/* | ||||
|   | ||||
							
								
								
									
										174
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										174
									
								
								README.rst
									
									
									
									
									
								
							| @@ -12,15 +12,17 @@ views. | ||||
|  | ||||
| HTTP Digest Authentication is specified in `RFC 2617`_. | ||||
|  | ||||
| Refer to the full `Flask-Digest-Auth readthedocs documentation`_. | ||||
|  | ||||
|  | ||||
| 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 | ||||
| @@ -33,51 +35,6 @@ 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 | ||||
| ============ | ||||
|  | ||||
| @@ -92,11 +49,25 @@ You may also install the latest source from the | ||||
|  | ||||
| :: | ||||
|  | ||||
|     git clone git@github.com:imacat/flask-digest-auth.git | ||||
|     cd flask-digest-auth | ||||
|     pip install . | ||||
|     pip install git+https://github.com/imacat/flask-digest-auth.git | ||||
|  | ||||
| .. _Flask-Digest-Auth GitHub repository: https://github.com/imacat/flask-digest-auth | ||||
|  | ||||
| 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. | ||||
|  | ||||
|  | ||||
| Flask-Digest-Auth Alone | ||||
| @@ -104,11 +75,9 @@ 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 | ||||
| ------------------------------------------------------------ | ||||
| Simple Applications with Flask-Digest-Auth Alone | ||||
| ------------------------------------------------ | ||||
|  | ||||
| In your ``my_app.py``: | ||||
|  | ||||
| @@ -143,8 +112,8 @@ In your ``my_app.py``: | ||||
|         return redirect(request.form.get("next")) | ||||
|  | ||||
|  | ||||
| Example for Larger Applications with ``create_app()`` with Flask-Digest-Auth Alone | ||||
| ---------------------------------------------------------------------------------- | ||||
| Larger Applications with ``create_app()`` with Flask-Digest-Auth Alone | ||||
| ---------------------------------------------------------------------- | ||||
|  | ||||
| In your ``my_app/__init__.py``: | ||||
|  | ||||
| @@ -196,23 +165,29 @@ In your ``my_app/views.py``: | ||||
|         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. | ||||
| Flask-Digest-Auth works with Flask-Login_.  You can write a Flask | ||||
| module that requires log in, without specifying how to log in.  The | ||||
| application can use either HTTP Digest Authentication, or the log in | ||||
| forms, as needed. | ||||
|  | ||||
| To use Flask-Login with Flask-Digest-Auth, | ||||
| ``login_manager.init_app(app)`` must be called before | ||||
| ``auth.init_app(app)``. | ||||
|  | ||||
| The currently logged-in user can be retrieved at | ||||
| ``flask_login.current_user``, if any. | ||||
|  | ||||
| The views only depend on Flask-Login, but not the Flask-Digest-Auth. | ||||
| You can change the actual authentication mechanism without changing | ||||
| the views. | ||||
|  | ||||
| Example for Simple Applications with Flask-Login Integration | ||||
| ------------------------------------------------------------ | ||||
|  | ||||
| Simple Applications with Flask-Login Integration | ||||
| ------------------------------------------------ | ||||
|  | ||||
| In your ``my_app.py``: | ||||
|  | ||||
| @@ -252,8 +227,8 @@ In your ``my_app.py``: | ||||
|         return redirect(request.form.get("next")) | ||||
|  | ||||
|  | ||||
| Example for Larger Applications with ``create_app()`` with Flask-Login Integration | ||||
| ---------------------------------------------------------------------------------- | ||||
| Larger Applications with ``create_app()`` with Flask-Login Integration | ||||
| ---------------------------------------------------------------------- | ||||
|  | ||||
| In your ``my_app/__init__.py``: | ||||
|  | ||||
| @@ -315,31 +290,13 @@ authentication mechanism.  You can change the actual authentication | ||||
| mechanism without changing the views. | ||||
|  | ||||
|  | ||||
| Setting the Password Hash | ||||
| ========================= | ||||
| Session Integration | ||||
| =================== | ||||
|  | ||||
| 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. | ||||
| Flask-Digest-Auth features session integration.  The user log in | ||||
| is remembered in the session.  The authentication information is not | ||||
| requested again.  This is different to the practice of the HTTP Digest | ||||
| Authentication, but is convenient for the log in accounting. | ||||
|  | ||||
|  | ||||
| Log In Bookkeeping | ||||
| @@ -355,13 +312,22 @@ logging the log in event, adding the log in counter, etc. | ||||
|         user.visits = user.visits + 1 | ||||
|  | ||||
|  | ||||
| Writing Tests | ||||
| ============= | ||||
| Log Out | ||||
| ======= | ||||
|  | ||||
| You can write tests with our test client that handles HTTP Digest | ||||
| Authentication. | ||||
| Flask-Digest-Auth supports log out.  The user will be prompted for the | ||||
| new username and password. | ||||
|  | ||||
| Example for a unittest_ test case: | ||||
|  | ||||
| Test Client | ||||
| =========== | ||||
|  | ||||
| Flask-Digest-Auth comes with a test client that supports HTTP digest | ||||
| authentication. | ||||
|  | ||||
|  | ||||
| A unittest Test Case | ||||
| -------------------- | ||||
|  | ||||
| :: | ||||
|  | ||||
| @@ -388,8 +354,8 @@ Example for a unittest_ test case: | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|  | ||||
|  | ||||
| Example for a pytest_ test: | ||||
| A pytest Test | ||||
| ------------- | ||||
|  | ||||
| :: | ||||
|  | ||||
| @@ -419,9 +385,6 @@ Example for a pytest_ test: | ||||
|                 "/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 | ||||
|  | ||||
|  | ||||
| Copyright | ||||
| ========= | ||||
| @@ -447,3 +410,10 @@ 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-Digest-Auth GitHub repository: https://github.com/imacat/flask-digest-auth | ||||
| .. _Flask-Digest-Auth readthedocs documentation: https://flask-digest-auth.readthedocs.io | ||||
| .. _Flask-Login: https://flask-login.readthedocs.io | ||||
|   | ||||
							
								
								
									
										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 | ||||
|  | ||||
| if "%1" == "" goto help | ||||
|  | ||||
| %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.http://sphinx-doc.org/ | ||||
| 	exit /b 1 | ||||
| ) | ||||
|  | ||||
| %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% | ||||
| goto end | ||||
|  | ||||
| :help | ||||
| %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% | ||||
|  | ||||
| :end | ||||
| popd | ||||
							
								
								
									
										1
									
								
								docs/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								docs/requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| flask | ||||
							
								
								
									
										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
									
								
							
							
								
								
									
										59
									
								
								docs/source/conf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								docs/source/conf.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| # Configuration file for the Sphinx documentation builder. | ||||
| # | ||||
| # This file only contains a selection of the most common options. For a full | ||||
| # list see the documentation: | ||||
| # https://www.sphinx-doc.org/en/master/usage/configuration.html | ||||
| import os | ||||
| # -- Path setup -------------------------------------------------------------- | ||||
|  | ||||
| # If extensions (or modules to document with autodoc) are in another directory, | ||||
| # add these directories to sys.path here. If the directory is relative to the | ||||
| # documentation root, use os.path.abspath to make it absolute, like shown here. | ||||
| # | ||||
| import sys | ||||
|  | ||||
| sys.path.insert(0, os.path.abspath('../../src/')) | ||||
|  | ||||
|  | ||||
| # -- Project information ----------------------------------------------------- | ||||
|  | ||||
| project = 'Flask-Digest-Auth' | ||||
| copyright = '2022, imacat' | ||||
| author = 'imacat' | ||||
|  | ||||
| # The full version, including alpha/beta/rc tags | ||||
| release = '0.2.3' | ||||
|  | ||||
|  | ||||
| # -- General configuration --------------------------------------------------- | ||||
|  | ||||
| # Add any Sphinx extension module names here, as strings. They can be | ||||
| # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom | ||||
| # ones. | ||||
| extensions = [ | ||||
|     "sphinx.ext.autodoc" | ||||
| ] | ||||
|  | ||||
| # Add any paths that contain templates here, relative to this directory. | ||||
| templates_path = ['_templates'] | ||||
|  | ||||
| # List of patterns, relative to source directory, that match files and | ||||
| # directories to ignore when looking for source files. | ||||
| # This pattern also affects html_static_path and html_extra_path. | ||||
| exclude_patterns = [] | ||||
|  | ||||
|  | ||||
| # -- Options for HTML output ------------------------------------------------- | ||||
|  | ||||
| # The theme to use for HTML and HTML Help pages.  See the documentation for | ||||
| # a list of builtin themes. | ||||
| # | ||||
| html_theme = 'sphinx_rtd_theme' | ||||
|  | ||||
| # Add any paths that contain custom static files (such as style sheets) here, | ||||
| # relative to this directory. They are copied after the builtin static files, | ||||
| # so a file named "default.css" will overwrite the builtin "default.css". | ||||
| html_static_path = ['_static'] | ||||
|  | ||||
| # For readthedocs.io to work properly. | ||||
| master_doc = 'index' | ||||
							
								
								
									
										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-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-alone-large: | ||||
|  | ||||
| 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) | ||||
|  | ||||
|  | ||||
| .. _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(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-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.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. | ||||
|  | ||||
|  | ||||
| .. _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({ | ||||
|                 "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) | ||||
|  | ||||
|  | ||||
|  | ||||
| .. _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({ | ||||
|             "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 | ||||
							
								
								
									
										24
									
								
								docs/source/flask_digest_auth.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								docs/source/flask_digest_auth.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| flask\_digest\_auth package | ||||
| =========================== | ||||
|  | ||||
| The ``DigestAuth`` Class | ||||
| ------------------------ | ||||
| .. autoclass:: flask_digest_auth.DigestAuth | ||||
|     :members: | ||||
|     :undoc-members: | ||||
|     :show-inheritance: | ||||
|  | ||||
| The ``make_password_hash`` Function | ||||
| ----------------------------------- | ||||
| .. autofunction:: flask_digest_auth.make_password_hash | ||||
|  | ||||
| The ``calc_response`` Function | ||||
| ------------------------------ | ||||
| .. autofunction:: flask_digest_auth.calc_response | ||||
|  | ||||
| The ``Client`` Test Class | ||||
| ------------------------- | ||||
| .. autoclass:: flask_digest_auth.Client | ||||
|     :members: | ||||
|     :undoc-members: | ||||
|     :show-inheritance: | ||||
							
								
								
									
										34
									
								
								docs/source/index.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								docs/source/index.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| .. flask-digest-auth documentation master file, created by | ||||
|    sphinx-quickstart on Tue Dec  6 15:15:08 2022. | ||||
|    You can adapt this file completely to your liking, but it should at least | ||||
|    contain the root `toctree` directive. | ||||
|  | ||||
| Welcome to Flask-Digest-Auth's documentation! | ||||
| ============================================= | ||||
|  | ||||
| *Flask-Digest-Auth* 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 | ||||
|  | ||||
|  | ||||
|  | ||||
| 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 | ||||
							
								
								
									
										148
									
								
								docs/source/intro.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								docs/source/intro.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | ||||
| Introduction | ||||
| ============ | ||||
|  | ||||
|  | ||||
| *Flask-Digest-Auth* 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-Digest-Auth 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-Digest-Auth with ``pip``: | ||||
|  | ||||
| :: | ||||
|  | ||||
|     pip install Flask-Digest-Auth | ||||
|  | ||||
| You may also install the latest source from the | ||||
| `Flask-Digest-Auth GitHub repository`_. | ||||
|  | ||||
| :: | ||||
|  | ||||
|     pip install git+https://github.com/imacat/flask-digest-auth.git | ||||
|  | ||||
|  | ||||
| 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 :meth:`flask_digest_auth.make_password_hash`. | ||||
|  | ||||
|  | ||||
| Flask-Digest-Auth Alone | ||||
| ----------------------- | ||||
|  | ||||
| Flask-Digest-Auth can authenticate the users alone. | ||||
|  | ||||
| See :ref:`example-alone-simple` and :ref:`example-alone-large`. | ||||
|  | ||||
|  | ||||
| Flask-Login Integration | ||||
| ----------------------- | ||||
|  | ||||
| Flask-Digest-Auth works with Flask-Login_.  You can write a Flask | ||||
| module that requires log in, without specifying how to log in.  The | ||||
| application can use either HTTP Digest Authentication, or the log in | ||||
| forms, as needed. | ||||
|  | ||||
| To use Flask-Login with Flask-Digest-Auth, | ||||
| ``login_manager.init_app(app)`` must be called before | ||||
| ``auth.init_app(app)``. | ||||
|  | ||||
| The currently logged-in user can be retrieved at | ||||
| ``flask_login.current_user``, if any. | ||||
|  | ||||
| See :ref:`example-flask-login-simple` and | ||||
| :ref:`example-flask-login-large`. | ||||
|  | ||||
| The views only depend on Flask-Login, but not the Flask-Digest-Auth. | ||||
| You can change the actual authentication mechanism without changing | ||||
| the views. | ||||
|  | ||||
|  | ||||
| Session Integration | ||||
| ------------------- | ||||
|  | ||||
| Flask-Digest-Auth features session integration.  The user log in | ||||
| is remembered in the session.  The authentication information is not | ||||
| requested again.  This is different to the practice of the HTTP Digest | ||||
| Authentication, but is convenient for the log in accounting. | ||||
|  | ||||
|  | ||||
| Log In Bookkeeping | ||||
| ------------------ | ||||
|  | ||||
| You can register a callback to run when the user logs in, for ex., | ||||
| logging the log in event, adding the log in counter, etc. | ||||
|  | ||||
| :: | ||||
|  | ||||
|     @auth.register_on_login | ||||
|     def on_login(user: User) -> None: | ||||
|         user.visits = user.visits + 1 | ||||
|  | ||||
| See :meth:`flask_digest_auth.DigestAuth.register_on_login`. | ||||
|  | ||||
|  | ||||
| Log Out | ||||
| ------- | ||||
|  | ||||
| Flask-Digest-Auth supports log out.  The user will be prompted for the | ||||
| new username and password. | ||||
|  | ||||
| See :meth:`flask_digest_auth.DigestAuth.logout`. | ||||
|  | ||||
|  | ||||
| Test Client | ||||
| ----------- | ||||
|  | ||||
| Flask-Digest-Auth comes with a test client that supports HTTP digest | ||||
| authentication. | ||||
|  | ||||
| See :class:`flask_digest_auth.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-Digest-Auth GitHub repository: https://github.com/imacat/flask-digest-auth | ||||
| @@ -17,7 +17,7 @@ | ||||
|  | ||||
| [metadata] | ||||
| name = flask-digest-auth | ||||
| version = 0.2.1 | ||||
| version = 0.2.3 | ||||
| author = imacat | ||||
| author_email = imacat@mail.imacat.idv.tw | ||||
| description = The Flask HTTP Digest Authentication project. | ||||
|   | ||||
| @@ -23,11 +23,16 @@ from __future__ import annotations | ||||
| import typing as t | ||||
| from hashlib import md5 | ||||
|  | ||||
| from flask_digest_auth.exception import UnauthorizedException | ||||
|  | ||||
|  | ||||
| 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. | ||||
|  | ||||
|     For example: | ||||
|  | ||||
|     :: | ||||
|  | ||||
|         user.password = make_password_hash(realm, username, password) | ||||
|  | ||||
|     :param realm: The realm. | ||||
|     :param username: The username. | ||||
| @@ -49,38 +54,28 @@ def calc_response( | ||||
|     :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 | ||||
|   | ||||
| @@ -31,51 +31,6 @@ 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: | ||||
| @@ -103,7 +58,17 @@ class DigestAuth: | ||||
|     def login_required(self, view) -> t.Callable: | ||||
|         """The view decorator for HTTP digest authentication. | ||||
|  | ||||
|         :param view: | ||||
|         For example: | ||||
|  | ||||
|         :: | ||||
|  | ||||
|             @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. | ||||
|         """ | ||||
|  | ||||
| @@ -137,7 +102,7 @@ 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) | ||||
|  | ||||
| @@ -166,12 +131,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. | ||||
| @@ -216,11 +181,11 @@ 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]: | ||||
| @@ -257,7 +222,16 @@ class DigestAuth: | ||||
|  | ||||
|     def register_get_password(self, func: t.Callable[[str], t.Optional[str]])\ | ||||
|             -> None: | ||||
|         """Registers the callback to obtain the password hash. | ||||
|         """The decorator to register the callback to obtain the password hash. | ||||
|  | ||||
|         For 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. | ||||
| @@ -280,7 +254,15 @@ class DigestAuth: | ||||
|  | ||||
|     def register_get_user(self, func: t.Callable[[str], t.Optional[t.Any]])\ | ||||
|             -> None: | ||||
|         """Registers the callback to obtain the user. | ||||
|         """The decorator to register the callback to obtain the user. | ||||
|  | ||||
|         For 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. | ||||
| @@ -302,7 +284,15 @@ 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. | ||||
|         """The decorator to register the callback to run when the user logs in. | ||||
|  | ||||
|         For 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. | ||||
| @@ -325,6 +315,15 @@ class DigestAuth: | ||||
|     def init_app(self, app: Flask) -> None: | ||||
|         """Initializes the Flask application. | ||||
|  | ||||
|         For example: | ||||
|  | ||||
|         :: | ||||
|  | ||||
|             app: flask = Flask(__name__) | ||||
|             auth: DigestAuth = DigestAuth() | ||||
|             auth.realm = "My Admin" | ||||
|             auth.init_app(app) | ||||
|  | ||||
|         :param app: The Flask application. | ||||
|         :return: None. | ||||
|         """ | ||||
| @@ -345,7 +344,7 @@ class DigestAuth: | ||||
|                 response: Response = Response() | ||||
|                 response.status = 401 | ||||
|                 response.headers["WWW-Authenticate"] \ | ||||
|                     = self.make_response_header(g.digest_auth_state) | ||||
|                     = self.__make_response_header(g.digest_auth_state) | ||||
|                 abort(response) | ||||
|  | ||||
|             @login_manager.request_loader | ||||
| @@ -364,7 +363,7 @@ class DigestAuth: | ||||
|                     if authorization.type != "digest": | ||||
|                         raise UnauthorizedException( | ||||
|                             "Not an HTTP digest authorization") | ||||
|                     self.authenticate(g.digest_auth_state) | ||||
|                     self.__authenticate(g.digest_auth_state) | ||||
|                     user = login_manager.user_callback( | ||||
|                         authorization.username) | ||||
|                     login_user(user) | ||||
| @@ -380,6 +379,16 @@ class DigestAuth: | ||||
|         This actually causes the next authentication to fail, which forces | ||||
|         the browser to ask the user for the username and password again. | ||||
|  | ||||
|         For example: | ||||
|  | ||||
|         :: | ||||
|  | ||||
|             @app.post("/logout") | ||||
|             @auth.login_required | ||||
|             def logout(): | ||||
|                 auth.logout() | ||||
|                 return redirect(request.form.get("next")) | ||||
|  | ||||
|         :return: None. | ||||
|         """ | ||||
|         if "user" in session: | ||||
| @@ -400,3 +409,52 @@ class AuthState: | ||||
|         """Constructs the authorization state.""" | ||||
|         self.opaque: t.Optional[str] = None | ||||
|         self.stale: t.Optional[bool] = None | ||||
|  | ||||
|  | ||||
| class UnauthorizedException(Exception): | ||||
|     """The exception thrown when the authentication is failed.""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| 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. | ||||
|         """ | ||||
|   | ||||
| @@ -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 | ||||
| @@ -29,15 +29,62 @@ 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. | ||||
|  | ||||
|     For unittest example: | ||||
|  | ||||
|     :: | ||||
|  | ||||
|         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=("my_name", "my_pass")) | ||||
|                 self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     For pytest example: | ||||
|  | ||||
|     :: | ||||
|  | ||||
|         @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 | ||||
|     """ | ||||
|  | ||||
|     def open(self, *args, digest_auth: t.Optional[t.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: A tuple of the username and password for the HTTP | ||||
|             digest authentication. | ||||
|         :param kwargs: The keyword arguments. | ||||
|         :return: The response. | ||||
|         """ | ||||
| @@ -59,7 +106,7 @@ 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. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user