Added logging out.

This commit is contained in:
依瑪貓 2022-11-25 08:40:27 +11:00
parent dda8472a76
commit cb5cfaf7d4
4 changed files with 162 additions and 8 deletions

View File

@ -68,7 +68,7 @@ In your ``my_app.py``:
:: ::
from flask import Flask from flask import Flask, request, redirect
from flask_digest_auth import DigestAuth from flask_digest_auth import DigestAuth
app: flask = Flask(__name__) app: flask = Flask(__name__)
@ -89,6 +89,12 @@ In your ``my_app.py``:
def admin(): def admin():
... (Process the view) ... ... (Process the view) ...
@app.post("/logout")
@auth.login_required
def logout():
auth.logout()
return redirect(request.form.get("next"))
Example for Larger Applications with ``create_app()`` with Flask-Digest-Auth Alone Example for Larger Applications with ``create_app()`` with Flask-Digest-Auth Alone
---------------------------------------------------------------------------------- ----------------------------------------------------------------------------------
@ -123,7 +129,7 @@ In your ``my_app/views.py``:
:: ::
from my_app import auth from my_app import auth
from flask import Flask, Blueprint from flask import Flask, Blueprint, request, redirect
bp = Blueprint("admin", __name__, url_prefix="/admin") bp = Blueprint("admin", __name__, url_prefix="/admin")
@ -132,6 +138,12 @@ In your ``my_app/views.py``:
def admin(): def admin():
... (Process the view) ... ... (Process the view) ...
@app.post("/logout")
@auth.login_required
def logout():
auth.logout()
return redirect(request.form.get("next"))
def init_app(app: Flask) -> None: def init_app(app: Flask) -> None:
app.register_blueprint(bp) app.register_blueprint(bp)
@ -152,7 +164,7 @@ In your ``my_app.py``:
:: ::
from flask import Flask from flask import Flask, request, redirect
from flask_digest_auth import DigestAuth from flask_digest_auth import DigestAuth
from flask_login import LoginManager from flask_login import LoginManager
@ -178,6 +190,13 @@ In your ``my_app.py``:
def admin(): def admin():
... (Process the view) ... ... (Process the view) ...
@app.post("/logout")
@flask_login.login_required
def logout():
auth.logout()
# Do not call flask_login.logout_user()
return redirect(request.form.get("next"))
Example for Larger Applications with ``create_app()`` with Flask-Login Integration Example for Larger Applications with ``create_app()`` with Flask-Login Integration
---------------------------------------------------------------------------------- ----------------------------------------------------------------------------------
@ -190,6 +209,8 @@ In your ``my_app/__init__.py``:
from flask_digest_auth import DigestAuth from flask_digest_auth import DigestAuth
from flask_login import LoginManager from flask_login import LoginManager
auth: DigestAuth = DigestAuth()
def create_app(test_config = None) -> Flask: def create_app(test_config = None) -> Flask:
app: flask = Flask(__name__) app: flask = Flask(__name__)
... (Configure the Flask application) ... ... (Configure the Flask application) ...
@ -201,7 +222,7 @@ In your ``my_app/__init__.py``:
def load_user(user_id: str) -> t.Optional[User]: def load_user(user_id: str) -> t.Optional[User]:
... (Load the user with the username) ... ... (Load the user with the username) ...
auth: DigestAuth = DigestAuth(realm=app.config["REALM"]) auth.realm = app.config["REALM"]
auth.init_app(app) auth.init_app(app)
@auth.register_get_password @auth.register_get_password
@ -215,7 +236,8 @@ In your ``my_app/views.py``:
:: ::
import flask_login import flask_login
from flask import Flask, Blueprint from flask import Flask, Blueprint, request, redirect
from my_app import auth
bp = Blueprint("admin", __name__, url_prefix="/admin") bp = Blueprint("admin", __name__, url_prefix="/admin")
@ -224,6 +246,13 @@ In your ``my_app/views.py``:
def admin(): def admin():
... (Process the view) ... ... (Process the view) ...
@app.post("/logout")
@flask_login.login_required
def logout():
auth.logout()
# Do not call flask_login.logout_user()
return redirect(request.form.get("next"))
def init_app(app: Flask) -> None: def init_app(app: Flask) -> None:
app.register_blueprint(bp) app.register_blueprint(bp)
@ -250,6 +279,15 @@ you need to ask their password, to generate and store the new password
hash. 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.
Writing Tests Writing Tests
============= =============

View File

@ -27,7 +27,8 @@ from functools import wraps
from random import random from random import random
from secrets import token_urlsafe from secrets import token_urlsafe
from flask import g, request, Response, session, abort, Flask, Request from flask import g, request, Response, session, abort, Flask, Request, \
current_app
from itsdangerous import URLSafeTimedSerializer, BadData from itsdangerous import URLSafeTimedSerializer, BadData
from werkzeug.datastructures import Authorization from werkzeug.datastructures import Authorization
@ -112,6 +113,9 @@ class DigestAuth:
:return: None. :return: None.
:raise UnauthorizedException: When the authentication failed. :raise UnauthorizedException: When the authentication failed.
""" """
if "digest_auth_logout" in session:
del session["digest_auth_logout"]
raise UnauthorizedException("Logging out")
authorization: Authorization = request.authorization authorization: Authorization = request.authorization
if self.use_opaque: if self.use_opaque:
if authorization.opaque is None: if authorization.opaque is None:
@ -252,6 +256,24 @@ class DigestAuth:
raise ModuleNotFoundError( raise ModuleNotFoundError(
"init_app() is only for Flask-Login integration") "init_app() is only for Flask-Login integration")
@staticmethod
def logout() -> None:
"""Logs out the user.
This actually causes the next authentication to fail, which forces
the browser to ask the user for the username and password again.
:return: None.
"""
if "user" in session:
del session["user"]
try:
if hasattr(current_app, "login_manager"):
from flask_login import logout_user
logout_user()
except ModuleNotFoundError:
pass
session["digest_auth_logout"] = True
class AuthState: class AuthState:
"""The authorization state.""" """The authorization state."""

View File

@ -22,7 +22,7 @@ import typing as t
from secrets import token_urlsafe from secrets import token_urlsafe
from types import SimpleNamespace from types import SimpleNamespace
from flask import Response, Flask, g from flask import Response, Flask, g, redirect, request
from flask_testing import TestCase from flask_testing import TestCase
from werkzeug.datastructures import WWWAuthenticate, Authorization from werkzeug.datastructures import WWWAuthenticate, Authorization
@ -89,6 +89,16 @@ class AuthenticationTestCase(TestCase):
""" """
return f"Hello, {g.user.username}! #2" return f"Hello, {g.user.username}! #2"
@app.post("/logout", endpoint="logout")
@auth.login_required
def logout() -> redirect:
"""Logs out the user.
:return: The response.
"""
auth.logout()
return redirect(request.form.get("next"))
return app return app
def test_auth(self) -> None: def test_auth(self) -> None:
@ -146,3 +156,40 @@ class AuthenticationTestCase(TestCase):
www_authenticate, admin_uri, _USERNAME, _PASSWORD) www_authenticate, admin_uri, _USERNAME, _PASSWORD)
response = super(Client, self.client).get(admin_uri, auth=auth_data) response = super(Client, self.client).get(admin_uri, auth=auth_data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_logout(self) -> None:
"""Tests the logging out.
:return: None.
"""
admin_uri: str = self.app.url_for("admin-1")
logout_uri: str = self.app.url_for("logout")
response: Response
response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 401)
response = self.client.get(admin_uri,
digest_auth=(_USERNAME, _PASSWORD))
self.assertEqual(response.status_code, 200)
response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 200)
response = self.client.post(logout_uri, data={"next": admin_uri})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.location, admin_uri)
response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 401)
response = self.client.get(admin_uri,
digest_auth=(_USERNAME, _PASSWORD))
self.assertEqual(response.status_code, 401)
response = self.client.get(admin_uri,
digest_auth=(_USERNAME, _PASSWORD))
self.assertEqual(response.status_code, 200)
response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 200)

View File

@ -21,7 +21,7 @@
import typing as t import typing as t
from secrets import token_urlsafe from secrets import token_urlsafe
from flask import Response, Flask, g from flask import Response, Flask, g, redirect, request
from flask_testing import TestCase from flask_testing import TestCase
from werkzeug.datastructures import WWWAuthenticate, Authorization from werkzeug.datastructures import WWWAuthenticate, Authorization
@ -121,6 +121,16 @@ class FlaskLoginTestCase(TestCase):
""" """
return f"Hello, {flask_login.current_user.username}! #2" return f"Hello, {flask_login.current_user.username}! #2"
@app.post("/logout", endpoint="logout")
@flask_login.login_required
def logout() -> redirect:
"""Logs out the user.
:return: The response.
"""
auth.logout()
return redirect(request.form.get("next"))
return app return app
def test_auth(self) -> None: def test_auth(self) -> None:
@ -190,3 +200,40 @@ class FlaskLoginTestCase(TestCase):
www_authenticate, admin_uri, _USERNAME, _PASSWORD) www_authenticate, admin_uri, _USERNAME, _PASSWORD)
response = super(Client, self.client).get(admin_uri, auth=auth_data) response = super(Client, self.client).get(admin_uri, auth=auth_data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_logout(self) -> None:
"""Tests the logging out.
:return: None.
"""
admin_uri: str = self.app.url_for("admin-1")
logout_uri: str = self.app.url_for("logout")
response: Response
response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 401)
response = self.client.get(admin_uri,
digest_auth=(_USERNAME, _PASSWORD))
self.assertEqual(response.status_code, 200)
response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 200)
response = self.client.post(logout_uri, data={"next": admin_uri})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.location, admin_uri)
response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 401)
response = self.client.get(admin_uri,
digest_auth=(_USERNAME, _PASSWORD))
self.assertEqual(response.status_code, 401)
response = self.client.get(admin_uri,
digest_auth=(_USERNAME, _PASSWORD))
self.assertEqual(response.status_code, 200)
response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 200)