flask-digestauth/src/flask_digest_auth/test.py

156 lines
5.4 KiB
Python
Raw Normal View History

2022-11-23 15:08:30 +08:00
# The Flask HTTP Digest Authentication Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/11/3
# Copyright (c) 2022 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The test client with HTTP digest authentication enabled.
"""
from secrets import token_urlsafe
from typing import Optional, Literal, Tuple, Dict
2022-11-23 15:08:30 +08:00
from flask import g
from werkzeug.datastructures import Authorization, WWWAuthenticate
from werkzeug.http import parse_set_header
2022-11-23 15:08:30 +08:00
from werkzeug.test import TestResponse, Client as WerkzeugClient
from flask_digest_auth.algo import calc_response, make_password_hash
class Client(WerkzeugClient):
2022-12-06 21:54:10 +08:00
"""The test client with HTTP digest authentication enabled.
2022-12-06 23:50:46 +08:00
:Example:
For unittest_:
2022-12-06 21:54:10 +08:00
::
class MyTestCase(flask_testing.TestCase):
def create_app(self):
app: Flask = create_app({
"SECRET_KEY": token_urlsafe(32),
"TESTING": True
})
app.test_client_class = Client
return app
def test_admin(self):
response = self.client.get("/admin")
self.assertEqual(response.status_code, 401)
response = self.client.get(
"/admin", digest_auth=(USERNAME, PASSWORD))
2022-12-06 21:54:10 +08:00
self.assertEqual(response.status_code, 200)
2022-12-06 23:50:46 +08:00
For pytest_:
2022-12-06 21:54:10 +08:00
::
@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")
2022-12-06 21:54:10 +08:00
assert response.status_code == 401
response = client.get(
"/admin", digest_auth=(USERNAME, PASSWORD))
2022-12-06 21:54:10 +08:00
assert response.status_code == 200
2022-12-06 23:50:46 +08:00
.. _unittest: https://docs.python.org/3/library/unittest.html
.. _pytest: https://pytest.org
2022-12-06 21:54:10 +08:00
"""
2022-11-23 15:08:30 +08:00
def open(self, *args, digest_auth: Optional[Tuple[str, str]] = None,
2022-11-23 15:08:30 +08:00
**kwargs) -> TestResponse:
"""Opens a request.
:param args: The arguments.
:param digest_auth: The (*username*, *password*) tuple for the HTTP
digest authentication.
2022-11-23 15:08:30 +08:00
:param kwargs: The keyword arguments.
:return: The response.
"""
response: TestResponse = super(Client, self).open(*args, **kwargs)
www_authenticate: WWWAuthenticate = response.www_authenticate
if not (response.status_code == 401
and www_authenticate.type == "digest"
and digest_auth is not None):
return response
if hasattr(g, "_login_user"):
delattr(g, "_login_user")
auth_data: Authorization = self.__class__.make_authorization(
2022-11-23 15:08:30 +08:00
www_authenticate, args[0], digest_auth[0], digest_auth[1])
response = super(Client, self).open(*args, auth=auth_data, **kwargs)
return response
@staticmethod
def make_authorization(www_authenticate: WWWAuthenticate, uri: str,
username: str, password: str) -> Authorization:
"""Composes and returns the request authorization.
2022-11-23 15:08:30 +08:00
:param www_authenticate: The ``WWW-Authenticate`` response.
:param uri: The request URI.
:param username: The username.
:param password: The password.
:return: The request authorization.
"""
qop: Optional[Literal["auth", "auth-int"]] = None
if "auth" in parse_set_header(www_authenticate.get("qop")):
qop = "auth"
cnonce: Optional[str] = None
if qop is not None or www_authenticate.algorithm == "MD5-sess":
cnonce = token_urlsafe(8)
nc: Optional[str] = None
count: int = 1
if qop is not None:
nc: str = hex(count)[2:].zfill(8)
expected: str = calc_response(
method="GET", uri=uri,
password_hash=make_password_hash(www_authenticate.realm,
username, password),
nonce=www_authenticate.nonce, qop=qop,
algorithm=www_authenticate.algorithm, cnonce=cnonce, nc=nc,
body=None)
data: Dict[str, str] = {
"username": username, "realm": www_authenticate.realm,
"nonce": www_authenticate.nonce, "uri": uri, "response": expected}
if www_authenticate.algorithm is not None:
data["algorithm"] = www_authenticate.algorithm
if cnonce is not None:
data["cnonce"] = cnonce
if www_authenticate.opaque is not None:
data["opaque"] = www_authenticate.opaque
if qop is not None:
data["qop"] = qop
if nc is not None:
data["nc"] = nc
return Authorization("digest", data=data)