21 Commits

Author SHA1 Message Date
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
409f794835 Advanced to version 0.2.2. 2022-12-06 22:05:12 +08:00
92eb011470 Added the documentation to MANIFEST.in. 2022-12-06 22:04:58 +08:00
b5ecd1552b Added the .keep files to the _static and _templates subdirectories in the docs directory to keep them in the source distribution. 2022-12-06 22:04:33 +08:00
765822a300 Revised the documentation. 2022-12-06 21:54:10 +08:00
e9a6449505 Changed the "authenticate" and "make_response_header" methods to private in the DigestAuth class. 2022-12-06 21:22:16 +08:00
df15f0b0d5 Revised the introduction in the documentation. 2022-12-06 20:58:13 +08:00
6c7f7e8c8e Added the comment to the master_doc setting in conf.py. 2022-12-06 20:02:53 +08:00
14 changed files with 355 additions and 201 deletions

View File

@ -15,4 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
include docs/*
include docs/source/*
include docs/source/_static/*
include docs/source/_templates/*
include tests/* include tests/*

View File

@ -12,15 +12,17 @@ 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`_.
Why HTTP Digest Authentication? Why HTTP Digest Authentication?
------------------------------- -------------------------------
HTTP Digest Authentication has the advantage that it does not send the *HTTP Digest Authentication* has the advantage that it does not send
actual password to the server, which greatly enhances the security. thee actual password to the server, which greatly enhances the
It uses the challenge-response authentication scheme. The client security. It uses the challenge-response authentication scheme. The
returns the response calculated from the challenge and the password, client returns the response calculated from the challenge and the
but not the original password. password, but not the original password.
Log in forms has the advantage of freedom, in the senses of both the Log in forms has the advantage of freedom, in the senses of both the
visual design and the actual implementation. You may implement your 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. 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 Installation
============ ============
@ -92,11 +49,25 @@ You may also install the latest source from the
:: ::
git clone git@github.com:imacat/flask-digest-auth.git pip install git+https://github.com/imacat/flask-digest-auth.git
cd flask-digest-auth
pip install .
.. _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 Flask-Digest-Auth Alone
@ -104,11 +75,9 @@ Flask-Digest-Auth Alone
Flask-Digest-Auth can authenticate the users alone. Flask-Digest-Auth can authenticate the users alone.
The currently logged-in user can be retrieved at ``g.user``, if any.
Simple Applications with Flask-Digest-Auth Alone
Example for Simple Applications with Flask-Digest-Auth Alone ------------------------------------------------
------------------------------------------------------------
In your ``my_app.py``: In your ``my_app.py``:
@ -143,8 +112,8 @@ In your ``my_app.py``:
return redirect(request.form.get("next")) 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``: In your ``my_app/__init__.py``:
@ -196,23 +165,29 @@ In your ``my_app/views.py``:
app.register_blueprint(bp) app.register_blueprint(bp)
Flask-Login Integration Flask-Login Integration
======================= =======================
Flask-Digest-Auth can work with Flask-Login. You can write a Flask Flask-Digest-Auth works with Flask-Login_. You can write a Flask
module that requires log in, without specifying the authentication module that requires log in, without specifying how to log in. The
mechanism. The Flask application can specify the actual application can use either HTTP Digest Authentication, or the log in
authentication mechanism as it sees fit. forms, as needed.
To use Flask-Login with Flask-Digest-Auth,
``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.
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``: In your ``my_app.py``:
@ -252,8 +227,8 @@ In your ``my_app.py``:
return redirect(request.form.get("next")) 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``: In your ``my_app/__init__.py``:
@ -315,31 +290,13 @@ authentication mechanism. You can change the actual authentication
mechanism without changing the views. mechanism without changing the views.
Setting the Password Hash Session Integration
========================= ===================
The password hash of the HTTP Digest Authentication is composed of the Flask-Digest-Auth features session integration. The user log in
realm, the username, and the password. Example for setting the is remembered in the session. The authentication information is not
password: requested again. This is different to the practice of the HTTP Digest
Authentication, but is convenient for the log in accounting.
::
from flask_digest_auth import make_password_hash
user.password = make_password_hash(realm, username, password)
The username is part of the hash. If the user changes their username,
you need to ask their password, to generate and store the new password
hash.
Log Out
=======
Call ``auth.logout()`` when the user wants to log out.
Besides the usual log out routine, ``auth.logout()`` actually causes
the next browser automatic authentication to fail, forcing the browser
to ask the user for the username and password again.
Log In Bookkeeping Log In Bookkeeping
@ -355,13 +312,22 @@ logging the log in event, adding the log in counter, etc.
user.visits = user.visits + 1 user.visits = user.visits + 1
Writing Tests Log Out
============= =======
You can write tests with our test client that handles HTTP Digest Flask-Digest-Auth supports log out. The user will be prompted for the
Authentication. 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) self.assertEqual(response.status_code, 200)
A pytest Test
Example for a pytest_ test: -------------
:: ::
@ -413,15 +379,12 @@ Example for 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=("my_name", "my_pass"))
assert response.status_code == 200 assert response.status_code == 200
.. _unittest: https://docs.python.org/3/library/unittest.html
.. _pytest: https://pytest.org
Copyright Copyright
========= =========
@ -447,3 +410,10 @@ Authors
| imacat | imacat
| imacat@mail.imacat.idv.tw | imacat@mail.imacat.idv.tw
| 2022/11/23 | 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

1
docs/requirements.txt Normal file
View File

@ -0,0 +1 @@
flask

View File

View File

View File

@ -22,7 +22,7 @@ copyright = '2022, imacat'
author = 'imacat' author = 'imacat'
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = '0.2.1' release = '0.3.0'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
@ -55,4 +55,5 @@ html_theme = 'sphinx_rtd_theme'
# so a file named "default.css" will overwrite the builtin "default.css". # 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' master_doc = 'index'

View File

@ -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=("my_name", "my_pass"))
assert response.status_code == 200 assert response.status_code == 200

View File

@ -1,23 +1,23 @@
flask\_digest\_auth package flask\_digest\_auth package
=========================== ===========================
The ``DigestAuth`` class The ``DigestAuth`` Class
************************ ------------------------
.. autoclass:: flask_digest_auth.DigestAuth .. autoclass:: flask_digest_auth.DigestAuth
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
The ``make_password_hash`` Function The ``make_password_hash`` Function
*********************************** -----------------------------------
.. autofunction:: flask_digest_auth.make_password_hash .. autofunction:: flask_digest_auth.make_password_hash
The ``calc_response`` Function The ``calc_response`` Function
****************************** ------------------------------
.. autofunction:: flask_digest_auth.calc_response .. autofunction:: flask_digest_auth.calc_response
The ``Client`` Test Class The ``Client`` Test Class
************************* -------------------------
.. autoclass:: flask_digest_auth.Client .. autoclass:: flask_digest_auth.Client
:members: :members:
:undoc-members: :undoc-members:

View File

@ -6,6 +6,12 @@
Welcome to Flask-Digest-Auth's documentation! 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:: .. toctree::
:maxdepth: 2 :maxdepth: 2
:caption: Contents: :caption: Contents:
@ -20,5 +26,8 @@ Indices and tables
================== ==================
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex`
* :ref:`search` * :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

View File

@ -8,17 +8,15 @@ views.
HTTP Digest Authentication is specified in `RFC 2617`_. HTTP Digest Authentication is specified in `RFC 2617`_.
See :ref:`example-alone-simple` and :ref:`example-alone-large`.
Why HTTP Digest Authentication? Why HTTP Digest Authentication?
------------------------------- -------------------------------
HTTP Digest Authentication has the advantage that it does not send the *HTTP Digest Authentication* has the advantage that it does not send
actual password to the server, which greatly enhances the security. the actual password to the server, which greatly enhances the
It uses the challenge-response authentication scheme. The client security. It uses the challenge-response authentication scheme. The
returns the response calculated from the challenge and the password, client returns the response calculated from the challenge and the
but not the original password. password, but not the original password.
Log in forms has the advantage of freedom, in the senses of both the Log in forms has the advantage of freedom, in the senses of both the
visual design and the actual implementation. You may implement your visual design and the actual implementation. You may implement your
@ -31,60 +29,6 @@ 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.
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.
See :ref:`example-flask-login-simple` and
:ref:`example-flask-login-large`.
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.
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`.
Installation Installation
------------ ------------
@ -99,7 +43,102 @@ You may also install the latest source from the
:: ::
pip install git+https://github.com/imacat/flask-digest-auth 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 .. _HTTP Digest Authentication: https://en.wikipedia.org/wiki/Digest_access_authentication

View File

@ -17,7 +17,7 @@
[metadata] [metadata]
name = flask-digest-auth name = flask-digest-auth
version = 0.2.1 version = 0.3.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.

View File

@ -26,6 +26,13 @@ from hashlib import md5
def make_password_hash(realm: str, username: str, password: str) -> str: 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.
:Example:
::
user.password = make_password_hash(realm, username, password)
:param realm: The realm. :param realm: The realm.
:param username: The username. :param username: The username.
@ -47,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

@ -41,15 +41,24 @@ 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)
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] = []
"""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.app: t.Optional[Flask] = None self.app: t.Optional[Flask] = None
"""The current Flask application."""
self.__get_password_hash: BasePasswordHashGetter \ self.__get_password_hash: BasePasswordHashGetter \
= BasePasswordHashGetter() = BasePasswordHashGetter()
self.__get_user: BaseUserGetter = BaseUserGetter() self.__get_user: BaseUserGetter = BaseUserGetter()
@ -58,6 +67,17 @@ class DigestAuth:
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 HTTP digest authentication.
:Example:
::
@app.get("/admin")
@auth.login_required
def admin():
return f"Hello, {g.user.username}!"
The logged-in user can be retrieved at ``g.user``.
:param view: The view. :param view: The view.
:return: The login-protected view. :return: The login-protected view.
""" """
@ -92,7 +112,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(state) self.__authenticate(state)
session["user"] = authorization.username session["user"] = authorization.username
return self.__get_user(authorization.username) return self.__get_user(authorization.username)
@ -121,12 +141,12 @@ class DigestAuth:
response: Response = Response() response: Response = Response()
response.status = 401 response.status = 401
response.headers["WWW-Authenticate"] \ response.headers["WWW-Authenticate"] \
= self.make_response_header(state) = self.__make_response_header(state)
abort(response) abort(response)
return login_required_view return login_required_view
def authenticate(self, state: AuthState) -> None: def __authenticate(self, state: AuthState) -> None:
"""Authenticate a user. """Authenticate a user.
:param state: The authorization state. :param state: The authorization state.
@ -142,7 +162,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")
@ -163,7 +183,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}")
@ -171,7 +191,7 @@ class DigestAuth:
state.stale = True state.stale = True
raise UnauthorizedException("Invalid nonce") raise UnauthorizedException("Invalid nonce")
def make_response_header(self, state: AuthState) -> str: def __make_response_header(self, state: AuthState) -> str:
"""Composes and returns the ``WWW-Authenticate`` response header. """Composes and returns the ``WWW-Authenticate`` response header.
:param state: The authorization state. :param state: The authorization state.
@ -187,16 +207,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:
@ -205,14 +225,23 @@ 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
def register_get_password(self, func: t.Callable[[str], t.Optional[str]])\ def register_get_password(self, func: t.Callable[[str], t.Optional[str]])\
-> None: -> None:
"""Registers the callback to obtain the password hash. """The decorator to register the callback to obtain the password hash.
:Example:
::
@auth.register_get_password
def get_password_hash(username: str) -> Optional[str]:
user = User.query.filter(User.username == username).first()
return None if user is None else user.password
:param func: The callback that given the username, returns the password :param func: The callback that given the username, returns the password
hash, or None if the user does not exist. hash, or None if the user does not exist.
@ -235,7 +264,15 @@ class DigestAuth:
def register_get_user(self, func: t.Callable[[str], t.Optional[t.Any]])\ def register_get_user(self, func: t.Callable[[str], t.Optional[t.Any]])\
-> None: -> None:
"""Registers the callback to obtain the user. """The decorator to register the callback to obtain the user.
:Example:
::
@auth.register_get_user
def get_user(username: str) -> Optional[User]:
return User.query.filter(User.username == username).first()
:param func: The callback that given the username, returns the user, :param func: The callback that given the username, returns the user,
or None if the user does not exist. or None if the user does not exist.
@ -257,7 +294,15 @@ class DigestAuth:
self.__get_user = UserGetter() self.__get_user = UserGetter()
def register_on_login(self, func: t.Callable[[t.Any], None]) -> None: 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.
: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. :param func: The callback given the logged-in user.
:return: None. :return: None.
@ -280,6 +325,15 @@ class DigestAuth:
def init_app(self, app: Flask) -> None: def init_app(self, app: Flask) -> None:
"""Initializes the Flask application. """Initializes the Flask application.
:Example:
::
app: flask = Flask(__name__)
auth: DigestAuth = DigestAuth()
auth.realm = "My Admin"
auth.init_app(app)
:param app: The Flask application. :param app: The Flask application.
:return: None. :return: None.
""" """
@ -300,7 +354,7 @@ class DigestAuth:
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(g.digest_auth_state)
abort(response) abort(response)
@login_manager.request_loader @login_manager.request_loader
@ -319,7 +373,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(g.digest_auth_state)
user = login_manager.user_callback( user = login_manager.user_callback(
authorization.username) authorization.username)
login_user(user) login_user(user)
@ -335,6 +389,16 @@ 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.
:Example:
::
@app.post("/logout")
@auth.login_required
def logout():
auth.logout()
return redirect(request.form.get("next"))
:return: None. :return: None.
""" """
if "user" in session: if "user" in session:

View File

@ -29,12 +29,68 @@ 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.
:Example:
For unittest_:
::
class MyTestCase(flask_testing.TestCase):
def create_app(self):
app: Flask = create_app({
"SECRET_KEY": token_urlsafe(32),
"TESTING": True
})
app.test_client_class = Client
return app
def test_admin(self):
response = self.client.get("/admin")
self.assertEqual(response.status_code, 401)
response = self.client.get(
"/admin", digest_auth=("my_name", "my_pass"))
self.assertEqual(response.status_code, 200)
For pytest_:
::
@pytest.fixture()
def app():
app: Flask = create_app({
"SECRET_KEY": token_urlsafe(32),
"TESTING": True
})
app.test_client_class = Client
yield app
@pytest.fixture()
def client(app):
return app.test_client()
def test_admin(app: Flask, client: Client):
with app.app_context():
response = client.get("/admin")
assert response.status_code == 401
response = client.get(
"/admin", digest_auth=("my_name", "my_pass"))
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,
**kwargs) -> TestResponse: **kwargs) -> TestResponse:
"""Opens a request. """Opens a request.
.. warning::
This is to override the parent ``open`` method. You should call
the ``get``, ``post``, ``put``, and ``delete`` methods instead.
:param args: The arguments. :param args: The arguments.
:param digest_auth: A tuple of the username and password for the HTTP :param digest_auth: A tuple of the username and password for the HTTP
digest authentication. digest authentication.
@ -59,6 +115,9 @@ class Client(WerkzeugClient):
username: str, password: str) -> Authorization: username: str, password: str) -> Authorization:
"""Composes and returns the request authorization. """Composes and returns the request authorization.
.. warning::
This method is not for public.
:param www_authenticate: The ``WWW-Authenticate`` response. :param www_authenticate: The ``WWW-Authenticate`` response.
:param uri: The request URI. :param uri: The request URI.
:param username: The username. :param username: The username.