32 Commits

Author SHA1 Message Date
4f30756dc5 Advanced to version 0.4.0. 2023-01-04 21:29:51 +08:00
cdc057f851 Renamed the package from flask-digest-auth to Flask-DigestAuth, by the Flask recommended extension guidelines https://flask.palletsprojects.com/en/latest/extensiondev/. 2023-01-04 21:29:12 +08:00
574ecade05 Revised the copyright year in auth.py. 2023-01-04 21:29:04 +08:00
84b9c5f62e Renamed digest_auth_state to _digest_auth_state as the state stored in the request instance. 2023-01-04 20:48:45 +08:00
4990de085c Changed to store the DigestAuth instance from app.digest_auth to app.extensions["digest_auth"]. 2023-01-04 20:42:51 +08:00
51e51ae4e2 Replaced auth.app with current_app. 2023-01-04 20:37:57 +08:00
2de770aed0 Advanced to version 0.3.1. 2022-12-29 23:49:28 +08:00
9ab413d583 Added the test_disabled test to the FlaskLoginTestCase test case. 2022-12-29 23:44:02 +08:00
aeb93a60e5 Fixed to store the auth state in request instead of the g global object in the flask_login load_user_from_request and unauthorized handlers in the init_app method of the DigestAuth class. This is so that the auth state is always reset in the lifecycle of request even if g stays. Revised the unauthorized to create a new auth state if it is not available in the current request, in the case that the load_user_from_request handler was not run previously. 2022-12-29 23:43:35 +08:00
a07118ef9c Revised the documentation for digest_auth parameter of the open method in the test client, to be clear. 2022-12-07 18:59:41 +08:00
514e9255aa Replaced "my_user" and "my_pass" with USERNAME and PASSWORD in the examples of the test client in the documentation, to avoid GitGuardian from detecting them as real passwords. 2022-12-07 18:55:52 +08:00
79abdc9cde Fixed the documentation of the login_required decorator in the DigestAuth class. 2022-12-07 18:48:39 +08:00
038e7a8352 Removed the warnings in the documentation of the test client. It is API document now. All content, for public or not, are available. There is no need to warn now. 2022-12-07 18:45:06 +08:00
0387abb4f6 Revised the documentation in the "flask_digest_auth.auth" module. 2022-12-07 18:39:20 +08:00
10e8add9e6 Replaced the manually-added package content with the automatically-generated package content, and added the modules list to the documentation. 2022-12-07 18:11:35 +08:00
c004e28c37 Fixed the documentation of the AuthState class and the UnauthorizedException exception. 2022-12-07 16:08:07 +08:00
46f05a1022 Updated the Sphinx version that is used to create the document template. 2022-12-07 09:48:56 +08:00
b9384150b7 Added documentation to the private properties of DigestAuth. 2022-12-07 06:55:34 +08:00
4296756ae7 Advanced to version 0.3.0. 2022-12-06 23:59:29 +08:00
83cf83a67c Revised the type hint of the "algorithm" and "__qop" properties of the DigestAuth class. 2022-12-06 23:55:20 +08:00
7aaa7b9abe Revised the documentations. 2022-12-06 23:52:42 +08:00
14b90de059 Revised the documentation of the "use_opaque" and "__domain" properties in the DigestAuth class. 2022-12-06 23:27:28 +08:00
e9013017fb Changed the "domain" and "qop" property to private in the DigestAuth class. 2022-12-06 23:26:42 +08:00
6e6ac8bbe7 Changed the "serializer" property to private in the DigestAuth class. 2022-12-06 23:14:32 +08:00
7ec56ee52d Removed the secret_key attribute from the DigestAuth class. It is actually not used. 2022-12-06 23:13:45 +08:00
b348c872dc Fixed the example in the documentation of the login_required decorator. 2022-12-06 22:59:13 +08:00
d78093ab53 Advanced to version 0.2.4. 2022-12-06 22:48:20 +08:00
c6b8569543 Fixed the documentation on the pytest example. 2022-12-06 22:46:52 +08:00
a1c48beb32 Removed the unused module index from the documentation. 2022-12-06 22:24:39 +08:00
a88fdf81fa Advanced to version 0.2.3. 2022-12-06 22:18:49 +08:00
4993bfdd9b Added the documentation dependencies. 2022-12-06 22:18:22 +08:00
696f350a44 Updated the version number in the documentation. 2022-12-06 22:10:25 +08:00
14 changed files with 244 additions and 170 deletions

View File

@ -6,13 +6,13 @@ Flask HTTP Digest Authentication
Description 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 for Flask_ applications. It authenticates the user for the protected
views. views.
HTTP Digest Authentication is specified in `RFC 2617`_. HTTP Digest Authentication is specified in `RFC 2617`_.
Refer to the full `Flask-Digest-Auth readthedocs documentation`_. Refer to the full `Flask-DigestAuth readthedocs documentation`_.
Why HTTP Digest Authentication? Why HTTP Digest Authentication?
@ -30,7 +30,7 @@ own challenge-response log in form, but then you are reinventing the
wheels. If a pretty log in form is not critical to your project, HTTP wheels. If a pretty log in form is not critical to your project, HTTP
Digest Authentication should be a good choice. 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 separated with the authentication mechanism. You can create protected
Flask modules without knowing the actual authentication mechanisms. Flask modules without knowing the actual authentication mechanisms.
@ -38,18 +38,18 @@ Flask modules without knowing the actual authentication mechanisms.
Installation 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 You may also install the latest source from the
`Flask-Digest-Auth GitHub repository`_. `Flask-DigestAuth GitHub repository`_.
:: ::
pip install git+https://github.com/imacat/flask-digest-auth.git pip install git+https://github.com/imacat/flask-digestauth.git
Setting the Password Setting the Password
@ -70,14 +70,14 @@ you need to ask their password, to generate and store the new password
hash. hash.
Flask-Digest-Auth Alone Flask-DigestAuth Alone
======================= ======================
Flask-Digest-Auth can authenticate the users alone. Flask-DigestAuth can authenticate the users alone.
Simple Applications with Flask-Digest-Auth Alone Simple Applications with Flask-DigestAuth Alone
------------------------------------------------ -----------------------------------------------
In your ``my_app.py``: In your ``my_app.py``:
@ -112,8 +112,8 @@ In your ``my_app.py``:
return redirect(request.form.get("next")) return redirect(request.form.get("next"))
Larger Applications with ``create_app()`` with Flask-Digest-Auth Alone Larger Applications with ``create_app()`` with Flask-DigestAuth Alone
---------------------------------------------------------------------- ---------------------------------------------------------------------
In your ``my_app/__init__.py``: In your ``my_app/__init__.py``:
@ -169,19 +169,19 @@ In your ``my_app/views.py``:
Flask-Login Integration Flask-Login Integration
======================= =======================
Flask-Digest-Auth works with Flask-Login_. You can write a Flask Flask-DigestAuth works with Flask-Login_. You can write a Flask
module that requires log in, without specifying how to log in. The module that requires log in, without specifying how to log in. The
application can use either HTTP Digest Authentication, or the log in application can use either HTTP Digest Authentication, or the log in
forms, as needed. forms, as needed.
To use Flask-Login with Flask-Digest-Auth, To use Flask-Login with Flask-DigestAuth,
``login_manager.init_app(app)`` must be called before ``login_manager.init_app(app)`` must be called before
``auth.init_app(app)``. ``auth.init_app(app)``.
The currently logged-in user can be retrieved at The currently logged-in user can be retrieved at
``flask_login.current_user``, if any. ``flask_login.current_user``, if any.
The views only depend on Flask-Login, but not the Flask-Digest-Auth. The views only depend on Flask-Login, but not the Flask-DigestAuth.
You can change the actual authentication mechanism without changing You can change the actual authentication mechanism without changing
the views. the views.
@ -293,7 +293,7 @@ mechanism without changing the views.
Session Integration Session Integration
=================== ===================
Flask-Digest-Auth features session integration. The user log in Flask-DigestAuth features session integration. The user log in
is remembered in the session. The authentication information is not is remembered in the session. The authentication information is not
requested again. This is different to the practice of the HTTP Digest requested again. This is different to the practice of the HTTP Digest
Authentication, but is convenient for the log in accounting. Authentication, but is convenient for the log in accounting.
@ -315,14 +315,14 @@ logging the log in event, adding the log in counter, etc.
Log Out Log Out
======= =======
Flask-Digest-Auth supports log out. The user will be prompted for the Flask-DigestAuth supports log out. The user will be prompted for the
new username and password. new username and password.
Test Client Test Client
=========== ===========
Flask-Digest-Auth comes with a test client that supports HTTP digest Flask-DigestAuth comes with a test client that supports HTTP digest
authentication. authentication.
@ -350,7 +350,7 @@ A unittest Test Case
response = self.client.get("/admin") response = self.client.get("/admin")
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)
response = self.client.get( response = self.client.get(
"/admin", digest_auth=("my_name", "my_pass")) "/admin", digest_auth=(USERNAME, PASSWORD))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -379,17 +379,17 @@ A pytest Test
def test_admin(app: Flask, client: Client): def test_admin(app: Flask, client: Client):
with app.app_context(): with app.app_context():
response = self.client.get("/admin") response = client.get("/admin")
assert response.status_code == 401 assert response.status_code == 401
response = self.client.get( response = client.get(
"/admin", digest_auth=("my_name", "my_pass")) "/admin", digest_auth=(USERNAME, PASSWORD))
assert response.status_code == 200 assert response.status_code == 200
Copyright Copyright
========= =========
Copyright (c) 2022 imacat. Copyright (c) 2022-2023 imacat.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -414,6 +414,6 @@ Authors
.. _HTTP Digest Authentication: https://en.wikipedia.org/wiki/Digest_access_authentication .. _HTTP Digest Authentication: https://en.wikipedia.org/wiki/Digest_access_authentication
.. _RFC 2617: https://www.rfc-editor.org/rfc/rfc2617 .. _RFC 2617: https://www.rfc-editor.org/rfc/rfc2617
.. _Flask: https://flask.palletsprojects.com .. _Flask: https://flask.palletsprojects.com
.. _Flask-Digest-Auth GitHub repository: https://github.com/imacat/flask-digest-auth .. _Flask-DigestAuth GitHub repository: https://github.com/imacat/flask-digestauth
.. _Flask-Digest-Auth readthedocs documentation: https://flask-digest-auth.readthedocs.io .. _Flask-DigestAuth readthedocs documentation: https://flask-digestauth.readthedocs.io
.. _Flask-Login: https://flask-login.readthedocs.io .. _Flask-Login: https://flask-login.readthedocs.io

View File

@ -10,8 +10,6 @@ if "%SPHINXBUILD%" == "" (
set SOURCEDIR=source set SOURCEDIR=source
set BUILDDIR=build set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL %SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 ( if errorlevel 9009 (
echo. echo.
@ -21,10 +19,12 @@ if errorlevel 9009 (
echo.may add the Sphinx directory to PATH. echo.may add the Sphinx directory to PATH.
echo. echo.
echo.If you don't have Sphinx installed, grab it from echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/ echo.https://www.sphinx-doc.org/
exit /b 1 exit /b 1
) )
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end goto end

1
docs/requirements.txt Normal file
View File

@ -0,0 +1 @@
flask

View File

@ -1,59 +1,32 @@
# Configuration file for the Sphinx documentation builder. # Configuration file for the Sphinx documentation builder.
# #
# This file only contains a selection of the most common options. For a full # For the full list of built-in configuration values, see the documentation:
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html # https://www.sphinx-doc.org/en/master/usage/configuration.html
import os 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 import sys
sys.path.insert(0, os.path.abspath('../../src/')) sys.path.insert(0, os.path.abspath('../../src/'))
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'Flask-Digest-Auth' project = 'Flask-DigestAuth'
copyright = '2022, imacat' copyright = '2022-2023, imacat'
author = 'imacat' author = 'imacat'
release = '0.4.0'
# The full version, including alpha/beta/rc tags
release = '0.2.1'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
# Add any Sphinx extension module names here, as strings. They can be extensions = ["sphinx.ext.autodoc"]
# 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'] 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 = [] exclude_patterns = []
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme' 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'] html_static_path = ['_static']
# For readthedocs.io to work properly.
master_doc = 'index'

View File

@ -4,8 +4,8 @@ Examples
.. _example-alone-simple: .. _example-alone-simple:
Simple Applications with Flask-Digest-Auth Alone Simple Applications with Flask-DigestAuth Alone
------------------------------------------------ -----------------------------------------------
In your ``my_app.py``: In your ``my_app.py``:
@ -42,8 +42,8 @@ In your ``my_app.py``:
.. _example-alone-large: .. _example-alone-large:
Larger Applications with ``create_app()`` with Flask-Digest-Auth Alone Larger Applications with ``create_app()`` with Flask-DigestAuth Alone
---------------------------------------------------------------------- ---------------------------------------------------------------------
In your ``my_app/__init__.py``: In your ``my_app/__init__.py``:
@ -229,7 +229,7 @@ A unittest Test Case
response = self.client.get("/admin") response = self.client.get("/admin")
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)
response = self.client.get( response = self.client.get(
"/admin", digest_auth=("my_name", "my_pass")) "/admin", digest_auth=(USERNAME, PASSWORD))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -261,8 +261,8 @@ A pytest Test
def test_admin(app: Flask, client: Client): def test_admin(app: Flask, client: Client):
with app.app_context(): with app.app_context():
response = self.client.get("/admin") response = client.get("/admin")
assert response.status_code == 401 assert response.status_code == 401
response = self.client.get( response = client.get(
"/admin", digest_auth=("my_name", "my_pass")) "/admin", digest_auth=(USERNAME, PASSWORD))
assert response.status_code == 200 assert response.status_code == 200

View File

@ -1,24 +1,37 @@
flask\_digest\_auth package flask\_digest\_auth package
=========================== ===========================
The ``DigestAuth`` Class Submodules
------------------------ ----------
.. autoclass:: flask_digest_auth.DigestAuth
:members:
:undoc-members:
:show-inheritance:
The ``make_password_hash`` Function flask\_digest\_auth.algo module
----------------------------------- -------------------------------
.. autofunction:: flask_digest_auth.make_password_hash
The ``calc_response`` Function .. automodule:: flask_digest_auth.algo
------------------------------ :members:
.. autofunction:: flask_digest_auth.calc_response :undoc-members:
:show-inheritance:
The ``Client`` Test Class flask\_digest\_auth.auth module
------------------------- -------------------------------
.. autoclass:: flask_digest_auth.Client
:members: .. automodule:: flask_digest_auth.auth
:undoc-members: :members:
:show-inheritance: :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:

View File

@ -1,12 +1,12 @@
.. flask-digest-auth documentation master file, created by .. Flask-DigestAuth documentation master file, created by
sphinx-quickstart on Tue Dec 6 15:15:08 2022. sphinx-quickstart on Wed Dec 7 09:40:48 2022.
You can adapt this file completely to your liking, but it should at least You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive. contain the root `toctree` directive.
Welcome to Flask-Digest-Auth's documentation! Welcome to Flask-DigestAuth's documentation!
============================================= ============================================
*Flask-Digest-Auth* is an `HTTP Digest Authentication`_ implementation *Flask-DigestAuth* is an `HTTP Digest Authentication`_ implementation
for Flask_ applications. It authenticates the user for the protected for Flask_ applications. It authenticates the user for the protected
views. views.

View File

@ -2,7 +2,7 @@ Introduction
============ ============
*Flask-Digest-Auth* is an `HTTP Digest Authentication`_ implementation *Flask-DigestAuth* is an `HTTP Digest Authentication`_ implementation
for Flask_ applications. It authenticates the user for the protected for Flask_ applications. It authenticates the user for the protected
views. views.
@ -24,7 +24,7 @@ own challenge-response log in form, but then you are reinventing the
wheels. If a pretty log in form is not critical to your project, HTTP wheels. If a pretty log in form is not critical to your project, HTTP
Digest Authentication should be a good choice. 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 separated with the authentication mechanism. You can create protected
Flask modules without knowing the actual authentication mechanisms. Flask modules without knowing the actual authentication mechanisms.
@ -32,18 +32,18 @@ Flask modules without knowing the actual authentication mechanisms.
Installation 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 You may also install the latest source from the
`Flask-Digest-Auth GitHub repository`_. `Flask-DigestAuth GitHub repository`_.
:: ::
pip install git+https://github.com/imacat/flask-digest-auth.git pip install git+https://github.com/imacat/flask-digestauth.git
Setting the Password Setting the Password
@ -63,13 +63,13 @@ The username is part of the hash. If the user changes their username,
you need to ask their password, to generate and store the new password you need to ask their password, to generate and store the new password
hash. hash.
See :meth:`flask_digest_auth.make_password_hash`. See :func:`flask_digest_auth.algo.make_password_hash`.
Flask-Digest-Auth Alone Flask-DigestAuth Alone
----------------------- ----------------------
Flask-Digest-Auth can authenticate the users alone. Flask-DigestAuth can authenticate the users alone.
See :ref:`example-alone-simple` and :ref:`example-alone-large`. See :ref:`example-alone-simple` and :ref:`example-alone-large`.
@ -77,12 +77,12 @@ See :ref:`example-alone-simple` and :ref:`example-alone-large`.
Flask-Login Integration Flask-Login Integration
----------------------- -----------------------
Flask-Digest-Auth works with Flask-Login_. You can write a Flask Flask-DigestAuth works with Flask-Login_. You can write a Flask
module that requires log in, without specifying how to log in. The module that requires log in, without specifying how to log in. The
application can use either HTTP Digest Authentication, or the log in application can use either HTTP Digest Authentication, or the log in
forms, as needed. forms, as needed.
To use Flask-Login with Flask-Digest-Auth, To use Flask-Login with Flask-DigestAuth,
``login_manager.init_app(app)`` must be called before ``login_manager.init_app(app)`` must be called before
``auth.init_app(app)``. ``auth.init_app(app)``.
@ -92,7 +92,7 @@ The currently logged-in user can be retrieved at
See :ref:`example-flask-login-simple` and See :ref:`example-flask-login-simple` and
:ref:`example-flask-login-large`. :ref:`example-flask-login-large`.
The views only depend on Flask-Login, but not the Flask-Digest-Auth. The views only depend on Flask-Login, but not the Flask-DigestAuth.
You can change the actual authentication mechanism without changing You can change the actual authentication mechanism without changing
the views. the views.
@ -100,7 +100,7 @@ the views.
Session Integration Session Integration
------------------- -------------------
Flask-Digest-Auth features session integration. The user log in Flask-DigestAuth features session integration. The user log in
is remembered in the session. The authentication information is not is remembered in the session. The authentication information is not
requested again. This is different to the practice of the HTTP Digest requested again. This is different to the practice of the HTTP Digest
Authentication, but is convenient for the log in accounting. Authentication, but is convenient for the log in accounting.
@ -118,25 +118,25 @@ logging the log in event, adding the log in counter, etc.
def on_login(user: User) -> None: def on_login(user: User) -> None:
user.visits = user.visits + 1 user.visits = user.visits + 1
See :meth:`flask_digest_auth.DigestAuth.register_on_login`. See :meth:`flask_digest_auth.auth.DigestAuth.register_on_login`.
Log Out Log Out
------- -------
Flask-Digest-Auth supports log out. The user will be prompted for the Flask-DigestAuth supports log out. The user will be prompted for the
new username and password. new username and password.
See :meth:`flask_digest_auth.DigestAuth.logout`. See :meth:`flask_digest_auth.auth.DigestAuth.logout`.
Test Client Test Client
----------- -----------
Flask-Digest-Auth comes with a test client that supports HTTP digest Flask-DigestAuth comes with a test client that supports HTTP digest
authentication. authentication.
See :class:`flask_digest_auth.Client`. See :class:`flask_digest_auth.test.Client`.
Also see :ref:`example-unittest` and :ref:`example-pytest`. Also see :ref:`example-unittest` and :ref:`example-pytest`.
@ -145,4 +145,4 @@ Also see :ref:`example-unittest` and :ref:`example-pytest`.
.. _RFC 2617: https://www.rfc-editor.org/rfc/rfc2617 .. _RFC 2617: https://www.rfc-editor.org/rfc/rfc2617
.. _Flask: https://flask.palletsprojects.com .. _Flask: https://flask.palletsprojects.com
.. _Flask-Login: https://flask-login.readthedocs.io .. _Flask-Login: https://flask-login.readthedocs.io
.. _Flask-Digest-Auth GitHub repository: https://github.com/imacat/flask-digest-auth .. _Flask-DigestAuth GitHub repository: https://github.com/imacat/flask-digestauth

7
docs/source/modules.rst Normal file
View File

@ -0,0 +1,7 @@
src
===
.. toctree::
:maxdepth: 4
flask_digest_auth

View File

@ -1,7 +1,7 @@
# The Flask HTTP Digest Authentication Project. # The Flask HTTP Digest Authentication Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/11/23 # 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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -16,16 +16,16 @@
# limitations under the License. # limitations under the License.
[metadata] [metadata]
name = flask-digest-auth name = Flask-DigestAuth
version = 0.2.2 version = 0.4.0
author = imacat author = imacat
author_email = imacat@mail.imacat.idv.tw author_email = imacat@mail.imacat.idv.tw
description = The Flask HTTP Digest Authentication project. description = The Flask HTTP Digest Authentication project.
long_description = file: README.rst long_description = file: README.rst
long_description_content_type = text/x-rst long_description_content_type = text/x-rst
url = https://github.com/imacat/flask-digest-auth url = https://github.com/imacat/flask-digestauth
project_urls = project_urls =
Bug Tracker = https://github.com/imacat/flask-digest-auth/issues Bug Tracker = https://github.com/imacat/flask-digestauth/issues
classifiers = classifiers =
Programming Language :: Python :: 3 Programming Language :: Python :: 3
License :: OSI Approved :: Apache Software License License :: OSI Approved :: Apache Software License

View File

@ -28,7 +28,7 @@ def make_password_hash(realm: str, username: str, password: str) -> str:
"""Calculates the password hash for the HTTP digest authentication. """Calculates the password hash for the HTTP digest authentication.
Use this function to set the password for the user. Use this function to set the password for the user.
For example: :Example:
:: ::
@ -54,7 +54,7 @@ def calc_response(
:param uri: The request URI. :param uri: The request URI.
:param password_hash: The password hash for the HTTP digest authentication. :param password_hash: The password hash for the HTTP digest authentication.
:param nonce: The nonce. :param nonce: The nonce.
:param qop: the quality of protection, either ``auth`` or ``auth-int``. :param qop: The quality of protection, either ``auth`` or ``auth-int``.
:param algorithm: The algorithm, either ``MD5`` or ``MD5-sess``. :param algorithm: The algorithm, either ``MD5`` or ``MD5-sess``.
:param cnonce: The client nonce, which must exists when qop exists or :param cnonce: The client nonce, which must exists when qop exists or
algorithm is ``MD5-sess``. algorithm is ``MD5-sess``.

View File

@ -1,7 +1,7 @@
# The Flask HTTP Digest Authentication Project. # The Flask HTTP Digest Authentication Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/10/22 # 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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -16,8 +16,9 @@
# limitations under the License. # limitations under the License.
"""The HTTP Digest Authentication. """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 from __future__ import annotations
@ -26,7 +27,8 @@ import typing as t
from functools import wraps from functools import wraps
from secrets import token_urlsafe, randbits from secrets import token_urlsafe, randbits
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 itsdangerous import URLSafeTimedSerializer, BadData
from werkzeug.datastructures import Authorization from werkzeug.datastructures import Authorization
@ -41,27 +43,39 @@ class DigestAuth:
:param realm: The realm. :param realm: The realm.
""" """
self.secret_key: str = token_urlsafe(32) self.__serializer: URLSafeTimedSerializer \
self.serializer: URLSafeTimedSerializer \ = URLSafeTimedSerializer(token_urlsafe(32))
= URLSafeTimedSerializer(self.secret_key) """The serializer to generate and validate the nonce and opaque."""
self.realm: str = "" if realm is None else realm self.realm: str = "" if realm is None else realm
self.algorithm: t.Optional[str] = None """The realm. Default is an empty string."""
self.algorithm: t.Optional[t.Literal["MD5", "MD5-sess"]] = None
"""The algorithm, either None, ``MD5``, or ``MD5-sess``. Default is
None."""
self.use_opaque: bool = True self.use_opaque: bool = True
self.domain: t.List[str] = [] """Whether to use an opaque. Default is True."""
self.qop: t.List[str] = ["auth", "auth-int"] self.__domain: t.List[str] = []
self.app: t.Optional[Flask] = None """A list of directories that this username and password applies to.
Default is empty."""
self.__qop: t.List[t.Literal["auth", "auth-int"]] \
= ["auth", "auth-int"]
"""A list of supported quality of protection supported, either
``qop``, ``auth-int``, both, or empty. Default is both."""
self.__get_password_hash: BasePasswordHashGetter \ self.__get_password_hash: BasePasswordHashGetter \
= BasePasswordHashGetter() = BasePasswordHashGetter()
"""The callback to return the password hash."""
self.__get_user: BaseUserGetter = BaseUserGetter() self.__get_user: BaseUserGetter = BaseUserGetter()
"""The callback to return the user."""
self.__on_login: BaseOnLogInCallback = BaseOnLogInCallback() self.__on_login: BaseOnLogInCallback = BaseOnLogInCallback()
"""The callback to run when the user logs in."""
def login_required(self, view) -> t.Callable: def login_required(self, view) -> t.Callable:
"""The view decorator for HTTP digest authentication. """The view decorator for the HTTP digest authentication.
For example: :Example:
:: ::
@app.get("/admin")
@auth.login_required @auth.login_required
def admin(): def admin():
return f"Hello, {g.user.username}!" return f"Hello, {g.user.username}!"
@ -152,7 +166,7 @@ class DigestAuth:
raise UnauthorizedException( raise UnauthorizedException(
"Missing \"opaque\" in the Authorization header") "Missing \"opaque\" in the Authorization header")
try: try:
self.serializer.loads( self.__serializer.loads(
authorization.opaque, salt="opaque", max_age=1800) authorization.opaque, salt="opaque", max_age=1800)
except BadData: except BadData:
raise UnauthorizedException("Invalid opaque") raise UnauthorizedException("Invalid opaque")
@ -173,7 +187,7 @@ class DigestAuth:
state.stale = False state.stale = False
raise UnauthorizedException("Incorrect response value") raise UnauthorizedException("Incorrect response value")
try: try:
self.serializer.loads( self.__serializer.loads(
authorization.nonce, authorization.nonce,
salt="nonce" if authorization.opaque is None salt="nonce" if authorization.opaque is None
else f"nonce-{authorization.opaque}") else f"nonce-{authorization.opaque}")
@ -197,16 +211,16 @@ class DigestAuth:
return None return None
if state.opaque is not None: if state.opaque is not None:
return state.opaque 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() opaque: t.Optional[str] = get_opaque()
nonce: str = self.serializer.dumps( nonce: str = self.__serializer.dumps(
randbits(32), randbits(32),
salt="nonce" if opaque is None else f"nonce-{opaque}") salt="nonce" if opaque is None else f"nonce-{opaque}")
header: str = f"Digest realm=\"{self.realm}\"" header: str = f"Digest realm=\"{self.realm}\""
if len(self.domain) > 0: if len(self.__domain) > 0:
domain_list: str = ",".join(self.domain) domain_list: str = ",".join(self.__domain)
header += f", domain=\"{domain_list}\"" header += f", domain=\"{domain_list}\""
header += f", nonce=\"{nonce}\"" header += f", nonce=\"{nonce}\""
if opaque is not None: if opaque is not None:
@ -215,8 +229,8 @@ class DigestAuth:
header += f", stale=TRUE" if state.stale else f", stale=FALSE" header += f", stale=TRUE" if state.stale else f", stale=FALSE"
if self.algorithm is not None: if self.algorithm is not None:
header += f", algorithm=\"{self.algorithm}\"" header += f", algorithm=\"{self.algorithm}\""
if len(self.qop) > 0: if len(self.__qop) > 0:
qop_list: str = ",".join(self.qop) qop_list: str = ",".join(self.__qop)
header += f", qop=\"{qop_list}\"" header += f", qop=\"{qop_list}\""
return header return header
@ -224,7 +238,7 @@ class DigestAuth:
-> None: -> None:
"""The decorator to register the callback to obtain the password hash. """The decorator to register the callback to obtain the password hash.
For example: :Example:
:: ::
@ -256,7 +270,7 @@ class DigestAuth:
-> None: -> None:
"""The decorator to register the callback to obtain the user. """The decorator to register the callback to obtain the user.
For example: :Example:
:: ::
@ -286,7 +300,7 @@ class DigestAuth:
def register_on_login(self, func: t.Callable[[t.Any], None]) -> None: def register_on_login(self, func: t.Callable[[t.Any], None]) -> None:
"""The decorator to register the callback to run when the user logs in. """The decorator to register the callback to run when the user logs in.
For example: :Example:
:: ::
@ -313,9 +327,10 @@ class DigestAuth:
self.__on_login = OnLogInCallback() self.__on_login = OnLogInCallback()
def init_app(self, app: Flask) -> None: 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"]``.
For example: :Example:
:: ::
@ -327,8 +342,7 @@ class DigestAuth:
:param app: The Flask application. :param app: The Flask application.
:return: None. :return: None.
""" """
app.digest_auth = self app.extensions["digest_auth"] = self
self.app = app
if hasattr(app, "login_manager"): if hasattr(app, "login_manager"):
from flask_login import LoginManager, login_user from flask_login import LoginManager, login_user
@ -341,10 +355,13 @@ class DigestAuth:
:return: None. :return: None.
""" """
state: AuthState = getattr(request, "_digest_auth_state") \
if hasattr(request, "_digest_auth_state") \
else AuthState()
response: Response = Response() response: Response = Response()
response.status = 401 response.status = 401
response.headers["WWW-Authenticate"] \ response.headers["WWW-Authenticate"] \
= self.__make_response_header(g.digest_auth_state) = self.__make_response_header(state)
abort(response) abort(response)
@login_manager.request_loader @login_manager.request_loader
@ -355,7 +372,7 @@ class DigestAuth:
:return: The authenticated user, or None if the :return: The authenticated user, or None if the
authentication fails authentication fails
""" """
g.digest_auth_state = AuthState() request._digest_auth_state = AuthState()
authorization: Authorization = req.authorization authorization: Authorization = req.authorization
try: try:
if authorization is None: if authorization is None:
@ -363,7 +380,7 @@ class DigestAuth:
if authorization.type != "digest": if authorization.type != "digest":
raise UnauthorizedException( raise UnauthorizedException(
"Not an HTTP digest authorization") "Not an HTTP digest authorization")
self.__authenticate(g.digest_auth_state) self.__authenticate(request._digest_auth_state)
user = login_manager.user_callback( user = login_manager.user_callback(
authorization.username) authorization.username)
login_user(user) login_user(user)
@ -379,7 +396,7 @@ class DigestAuth:
This actually causes the next authentication to fail, which forces This actually causes the next authentication to fail, which forces
the browser to ask the user for the username and password again. the browser to ask the user for the username and password again.
For example: :Example:
:: ::
@ -394,7 +411,7 @@ class DigestAuth:
if "user" in session: if "user" in session:
del session["user"] del session["user"]
try: try:
if hasattr(self.app, "login_manager"): if hasattr(current_app, "login_manager"):
from flask_login import logout_user from flask_login import logout_user
logout_user() logout_user()
except ModuleNotFoundError: except ModuleNotFoundError:
@ -403,21 +420,30 @@ class DigestAuth:
class AuthState: 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): def __init__(self):
"""Constructs the authorization state.""" """Constructs the authorization state."""
self.opaque: t.Optional[str] = None self.opaque: t.Optional[str] = None
"""The opaque value specified by the client, if valid."""
self.stale: t.Optional[bool] = None self.stale: t.Optional[bool] = None
"""The stale value, if there is a previous log in attempt."""
class UnauthorizedException(Exception): class UnauthorizedException(Exception):
"""The exception thrown when the authentication is failed.""" """The exception thrown when the authentication fails."""
pass
class BasePasswordHashGetter: class BasePasswordHashGetter:
"""The base password hash getter.""" """The base callback that given the username, returns the password hash,
or None if the user does not exist. The default is to raise an
:class:`UnboundLocalError` if the callback is not registered yet.
See :meth:`flask_digest_auth.auth.DigestAuth.register_get_password`
"""
@staticmethod @staticmethod
def __call__(username: str) -> t.Optional[str]: def __call__(username: str) -> t.Optional[str]:
@ -433,7 +459,12 @@ class BasePasswordHashGetter:
class BaseUserGetter: class BaseUserGetter:
"""The base user getter.""" """The base callback that given the username, returns the user, or None if
the user does not exist. The default is to raise an
:class:`UnboundLocalError` if the callback is not registered yet.
See :meth:`flask_digest_auth.auth.DigestAuth.register_get_user`
"""
@staticmethod @staticmethod
def __call__(username: str) -> t.Optional[t.Any]: def __call__(username: str) -> t.Optional[t.Any]:
@ -449,7 +480,11 @@ class BaseUserGetter:
class BaseOnLogInCallback: class BaseOnLogInCallback:
"""The base callback when the user logs in.""" """The base callback to run when the user logs in, given the logged-in
user. The default does nothing.
See :meth:`flask_digest_auth.auth.DigestAuth.register_on_login`
"""
@staticmethod @staticmethod
def __call__(user: t.Any) -> None: def __call__(user: t.Any) -> None:

View File

@ -31,7 +31,9 @@ from flask_digest_auth.algo import calc_response, make_password_hash
class Client(WerkzeugClient): class Client(WerkzeugClient):
"""The test client with HTTP digest authentication enabled. """The test client with HTTP digest authentication enabled.
For unittest example: :Example:
For unittest_:
:: ::
@ -49,10 +51,10 @@ class Client(WerkzeugClient):
response = self.client.get("/admin") response = self.client.get("/admin")
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)
response = self.client.get( response = self.client.get(
"/admin", digest_auth=("my_name", "my_pass")) "/admin", digest_auth=(USERNAME, PASSWORD))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
For pytest example: For pytest_:
:: ::
@ -71,11 +73,14 @@ class Client(WerkzeugClient):
def test_admin(app: Flask, client: Client): def test_admin(app: Flask, client: Client):
with app.app_context(): with app.app_context():
response = self.client.get("/admin") response = client.get("/admin")
assert response.status_code == 401 assert response.status_code == 401
response = self.client.get( response = client.get(
"/admin", digest_auth=("my_name", "my_pass")) "/admin", digest_auth=(USERNAME, PASSWORD))
assert response.status_code == 200 assert response.status_code == 200
.. _unittest: https://docs.python.org/3/library/unittest.html
.. _pytest: https://pytest.org
""" """
def open(self, *args, digest_auth: t.Optional[t.Tuple[str, str]] = None, def open(self, *args, digest_auth: t.Optional[t.Tuple[str, str]] = None,
@ -83,7 +88,7 @@ class Client(WerkzeugClient):
"""Opens a request. """Opens a request.
:param args: The arguments. :param args: The arguments.
:param digest_auth: A tuple of the username and password for the HTTP :param digest_auth: The (*username*, *password*) tuple for the HTTP
digest authentication. digest authentication.
:param kwargs: The keyword arguments. :param kwargs: The keyword arguments.
:return: The response. :return: The response.

View File

@ -45,7 +45,6 @@ class User:
self.password_hash: str = make_password_hash( self.password_hash: str = make_password_hash(
_REALM, username, password) _REALM, username, password)
self.visits: int = 0 self.visits: int = 0
self.is_authenticated: bool = True
self.is_active: bool = True self.is_active: bool = True
self.is_anonymous: bool = False self.is_anonymous: bool = False
@ -57,6 +56,16 @@ class User:
""" """
return self.username 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): class FlaskLoginTestCase(TestCase):
"""The test case with the Flask-Login integration.""" """The test case with the Flask-Login integration."""
@ -256,3 +265,34 @@ class FlaskLoginTestCase(TestCase):
response = self.client.get(admin_uri) response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(self.user.visits, 2) 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)