Revised so that Flask-Login become an optional dependency.
This commit is contained in:
parent
292d0aaf09
commit
2770e1cc12
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user