Revised so that Flask-Login become an optional dependency.
This commit is contained in:
		@@ -40,10 +40,14 @@ package_dir =
 | 
			
		||||
packages = find:
 | 
			
		||||
python_requires = >=3.10
 | 
			
		||||
install_requires =
 | 
			
		||||
  flask-login
 | 
			
		||||
  flask
 | 
			
		||||
tests_require =
 | 
			
		||||
  unittest
 | 
			
		||||
  flask-testing
 | 
			
		||||
 | 
			
		||||
[options.packages.find]
 | 
			
		||||
where = src
 | 
			
		||||
 | 
			
		||||
[options.extras_require]
 | 
			
		||||
flask_login =
 | 
			
		||||
    flask-login
 | 
			
		||||
 
 | 
			
		||||
@@ -20,5 +20,4 @@
 | 
			
		||||
"""
 | 
			
		||||
from flask_digest_auth.algo import make_password_hash, calc_response
 | 
			
		||||
from flask_digest_auth.auth import DigestAuth
 | 
			
		||||
from flask_digest_auth.flask_login import init_login_manager
 | 
			
		||||
from flask_digest_auth.test import Client
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ from functools import wraps
 | 
			
		||||
from random import random
 | 
			
		||||
from secrets import token_urlsafe
 | 
			
		||||
 | 
			
		||||
from flask import g, request, Response, session, abort
 | 
			
		||||
from flask import g, request, Response, session, abort, Flask, Request
 | 
			
		||||
from itsdangerous import URLSafeTimedSerializer, BadData
 | 
			
		||||
from werkzeug.datastructures import Authorization
 | 
			
		||||
 | 
			
		||||
@@ -195,6 +195,63 @@ class DigestAuth:
 | 
			
		||||
        """
 | 
			
		||||
        self.__get_user = func
 | 
			
		||||
 | 
			
		||||
    def init_app(self, app: Flask) -> None:
 | 
			
		||||
        """Initializes the Flask application.
 | 
			
		||||
 | 
			
		||||
        :param app: The Flask application.
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            from flask_login import LoginManager, login_user
 | 
			
		||||
 | 
			
		||||
            if not hasattr(app, "login_manager"):
 | 
			
		||||
                raise AttributeError(
 | 
			
		||||
                    "Please run the Flask-Login init-app() first")
 | 
			
		||||
            login_manager: LoginManager = getattr(app, "login_manager")
 | 
			
		||||
 | 
			
		||||
            @login_manager.unauthorized_handler
 | 
			
		||||
            def unauthorized() -> None:
 | 
			
		||||
                """Handles when the user is unauthorized.
 | 
			
		||||
 | 
			
		||||
                :return: None.
 | 
			
		||||
                """
 | 
			
		||||
                response: Response = Response()
 | 
			
		||||
                response.status = 401
 | 
			
		||||
                response.headers["WWW-Authenticate"] \
 | 
			
		||||
                    = self.make_response_header(g.digest_auth_state)
 | 
			
		||||
                abort(response)
 | 
			
		||||
 | 
			
		||||
            @login_manager.request_loader
 | 
			
		||||
            def load_user_from_request(req: Request) -> t.Optional[t.Any]:
 | 
			
		||||
                """Loads the user from the request header.
 | 
			
		||||
 | 
			
		||||
                :param req: The request.
 | 
			
		||||
                :return: The authenticated user, or None if the
 | 
			
		||||
                    authentication fails
 | 
			
		||||
                """
 | 
			
		||||
                g.digest_auth_state = AuthState()
 | 
			
		||||
                authorization: Authorization = req.authorization
 | 
			
		||||
                try:
 | 
			
		||||
                    if authorization is None:
 | 
			
		||||
                        raise UnauthorizedException
 | 
			
		||||
                    if authorization.type != "digest":
 | 
			
		||||
                        raise UnauthorizedException(
 | 
			
		||||
                            "Not an HTTP digest authorization")
 | 
			
		||||
                    self.authenticate(g.digest_auth_state)
 | 
			
		||||
                    user = login_manager.user_callback(
 | 
			
		||||
                        authorization.username)
 | 
			
		||||
                    login_user(user)
 | 
			
		||||
                    return user
 | 
			
		||||
                except UnauthorizedException as e:
 | 
			
		||||
                    if str(e) != "":
 | 
			
		||||
                        app.logger.warning(str(e))
 | 
			
		||||
                    return None
 | 
			
		||||
 | 
			
		||||
        except ModuleNotFoundError:
 | 
			
		||||
            raise ModuleNotFoundError(
 | 
			
		||||
                "init_app() is only for Flask-Login integration")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthState:
 | 
			
		||||
    """The authorization state."""
 | 
			
		||||
 
 | 
			
		||||
@@ -1,74 +0,0 @@
 | 
			
		||||
# The Flask HTTP Digest Authentication Project.
 | 
			
		||||
# Author: imacat@mail.imacat.idv.tw (imacat), 2022/11/14
 | 
			
		||||
 | 
			
		||||
#  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 Flask-Login integration.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import typing as t
 | 
			
		||||
 | 
			
		||||
from flask import Response, abort, current_app, Request, g
 | 
			
		||||
from flask_login import LoginManager, login_user
 | 
			
		||||
from werkzeug.datastructures import Authorization
 | 
			
		||||
 | 
			
		||||
from flask_digest_auth.auth import DigestAuth, AuthState
 | 
			
		||||
from flask_digest_auth.exception import UnauthorizedException
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def init_login_manager(auth: DigestAuth, login_manager: LoginManager) -> None:
 | 
			
		||||
    """Initialize the login manager.
 | 
			
		||||
 | 
			
		||||
    :param auth: The HTTP digest authentication.
 | 
			
		||||
    :param login_manager: The login manager from FlaskLogin.
 | 
			
		||||
    :return: None.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    @login_manager.unauthorized_handler
 | 
			
		||||
    def unauthorized() -> None:
 | 
			
		||||
        """Handles when the user is unauthorized.
 | 
			
		||||
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        response: Response = Response()
 | 
			
		||||
        response.status = 401
 | 
			
		||||
        response.headers["WWW-Authenticate"] = auth.make_response_header(
 | 
			
		||||
            g.digest_auth_state)
 | 
			
		||||
        abort(response)
 | 
			
		||||
 | 
			
		||||
    @login_manager.request_loader
 | 
			
		||||
    def load_user_from_request(request: Request) -> t.Optional[t.Any]:
 | 
			
		||||
        """Loads the user from the request header.
 | 
			
		||||
 | 
			
		||||
        :param request: The request.
 | 
			
		||||
        :return: The authenticated user, or None if the authentication fails
 | 
			
		||||
        """
 | 
			
		||||
        g.digest_auth_state = AuthState()
 | 
			
		||||
        authorization: Authorization = request.authorization
 | 
			
		||||
        try:
 | 
			
		||||
            if authorization is None:
 | 
			
		||||
                raise UnauthorizedException
 | 
			
		||||
            if authorization.type != "digest":
 | 
			
		||||
                raise UnauthorizedException(
 | 
			
		||||
                    "Not an HTTP digest authorization")
 | 
			
		||||
            auth.authenticate(g.digest_auth_state)
 | 
			
		||||
            user = login_manager.user_callback(authorization.username)
 | 
			
		||||
            login_user(user)
 | 
			
		||||
            return user
 | 
			
		||||
        except UnauthorizedException as e:
 | 
			
		||||
            if str(e) != "":
 | 
			
		||||
                current_app.logger.warning(str(e))
 | 
			
		||||
            return None
 | 
			
		||||
@@ -19,15 +19,13 @@
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import typing as t
 | 
			
		||||
import unittest
 | 
			
		||||
from secrets import token_urlsafe
 | 
			
		||||
 | 
			
		||||
import flask_login
 | 
			
		||||
from flask import Response, Flask
 | 
			
		||||
from flask_login import LoginManager
 | 
			
		||||
from flask_testing import TestCase
 | 
			
		||||
 | 
			
		||||
from flask_digest_auth import DigestAuth, make_password_hash, Client, \
 | 
			
		||||
    init_login_manager
 | 
			
		||||
from flask_digest_auth import DigestAuth, make_password_hash, Client
 | 
			
		||||
 | 
			
		||||
_REALM: str = "testrealm@host.com"
 | 
			
		||||
_USERNAME: str = "Mufasa"
 | 
			
		||||
@@ -53,7 +51,7 @@ class User:
 | 
			
		||||
class FlaskLoginTestCase(TestCase):
 | 
			
		||||
    """The test case with the Flask-Login integration."""
 | 
			
		||||
 | 
			
		||||
    def create_app(self):
 | 
			
		||||
    def create_app(self) -> Flask:
 | 
			
		||||
        """Creates the Flask application.
 | 
			
		||||
 | 
			
		||||
        :return: The Flask application.
 | 
			
		||||
@@ -65,11 +63,18 @@ class FlaskLoginTestCase(TestCase):
 | 
			
		||||
        })
 | 
			
		||||
        app.test_client_class = Client
 | 
			
		||||
 | 
			
		||||
        login_manager: LoginManager = LoginManager()
 | 
			
		||||
        self.has_flask_login: bool = True
 | 
			
		||||
        try:
 | 
			
		||||
            import flask_login
 | 
			
		||||
        except ModuleNotFoundError:
 | 
			
		||||
            self.has_flask_login = False
 | 
			
		||||
            return app
 | 
			
		||||
 | 
			
		||||
        login_manager: flask_login.LoginManager = flask_login.LoginManager()
 | 
			
		||||
        login_manager.init_app(app)
 | 
			
		||||
 | 
			
		||||
        auth: DigestAuth = DigestAuth(realm=_REALM)
 | 
			
		||||
        init_login_manager(auth, login_manager)
 | 
			
		||||
        auth.init_app(app)
 | 
			
		||||
 | 
			
		||||
        user_db: t.Dict[str, str] \
 | 
			
		||||
            = {_USERNAME: make_password_hash(_REALM, _USERNAME, _PASSWORD)}
 | 
			
		||||
@@ -117,6 +122,9 @@ class FlaskLoginTestCase(TestCase):
 | 
			
		||||
 | 
			
		||||
        :return: None.
 | 
			
		||||
        """
 | 
			
		||||
        if not self.has_flask_login:
 | 
			
		||||
            self.skipTest("Skipped testing Flask-Login integration without it.")
 | 
			
		||||
 | 
			
		||||
        response: Response = self.client.get(self.app.url_for("auth-1"))
 | 
			
		||||
        self.assertEqual(response.status_code, 401)
 | 
			
		||||
        response = self.client.get(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user