17 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
12 changed files with 59 additions and 33 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

@ -379,9 +379,9 @@ 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

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 ---------------------------------------------------

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

@ -26,7 +26,6 @@ Indices and tables
================== ==================
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex`
* :ref:`search` * :ref:`search`
.. _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

@ -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

@ -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,10 +67,11 @@ 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.
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 +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")
@ -173,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}")
@ -197,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:
@ -215,8 +225,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 +234,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 +266,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 +296,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:
:: ::
@ -315,7 +325,7 @@ class DigestAuth:
def init_app(self, app: Flask) -> None: def init_app(self, app: Flask) -> None:
"""Initializes the Flask application. """Initializes the Flask application.
For example: :Example:
:: ::
@ -379,7 +389,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:
:: ::

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_:
:: ::
@ -52,7 +54,7 @@ class Client(WerkzeugClient):
"/admin", digest_auth=("my_name", "my_pass")) "/admin", digest_auth=("my_name", "my_pass"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
For pytest example: For pytest_:
:: ::
@ -71,17 +73,24 @@ 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=("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
""" """
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.
@ -106,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.