Compare commits

...

3 Commits

7 changed files with 306 additions and 179 deletions

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: -------------
:: ::
@ -419,9 +385,6 @@ Example for a pytest_ test:
"/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

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:
@ -22,3 +28,7 @@ Indices and tables
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex` * :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

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

View File

@ -58,6 +58,16 @@ 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:
::
@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 +102,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 +131,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.
@ -171,7 +181,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.
@ -212,7 +222,16 @@ class DigestAuth:
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.
For 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 +254,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.
For 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 +284,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.
For 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 +315,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.
For 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 +344,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 +363,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 +379,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.
For 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,7 +29,54 @@ 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:
::
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 example:
::
@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 = self.client.get("/admin")
assert response.status_code == 401
response = self.client.get(
"/admin", digest_auth=("my_name", "my_pass"))
assert response.status_code == 200
"""
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: