From eaf4188cbfa33c57317bb59b5714d9c066d97933 Mon Sep 17 00:00:00 2001 From: Tero Vuotila Date: Fri, 10 Nov 2023 19:01:51 +0200 Subject: [PATCH 1/2] remove timedelta.total_seconds() reimplementation --- src/flask_login/utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/flask_login/utils.py b/src/flask_login/utils.py index 57d49f60..efe51b7f 100644 --- a/src/flask_login/utils.py +++ b/src/flask_login/utils.py @@ -189,11 +189,7 @@ def login_user(user, remember=False, duration=None, force=False, fresh=True): session["_remember"] = "set" if duration is not None: try: - # equal to timedelta.total_seconds() but works with Python 2.6 - session["_remember_seconds"] = ( - duration.microseconds - + (duration.seconds + duration.days * 24 * 3600) * 10**6 - ) / 10.0**6 + session["_remember_seconds"] = duration.total_seconds() except AttributeError as e: raise Exception( f"duration must be a datetime.timedelta, instead got: {duration}" From 376ea72d40c2341e0848e46e73687d9e6978d2e5 Mon Sep 17 00:00:00 2001 From: Tero Vuotila Date: Fri, 10 Nov 2023 15:04:34 +0200 Subject: [PATCH 2/2] replace expires attribute with max-age in "remember_me" cookie --- CHANGES.md | 1 + src/flask_login/login_manager.py | 20 ++-------- tests/test_login.py | 67 +++++++++----------------------- 3 files changed, 22 insertions(+), 66 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6a98e726..b8dcebe6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ Unreleased - Use `datetime.now(timezone.utc)` instead of deprecated `datetime.utcnow`. #758 - Never look at the `X-Forwarded-For` header, always use `request.remote_addr`, requiring the developer to configure `ProxyFix` appropriately. #700 +- Replace `expires` attribute with `max-age` in "remember_me" cookie. #823 Version 0.6.3 diff --git a/src/flask_login/login_manager.py b/src/flask_login/login_manager.py index 795e7441..d2e99205 100644 --- a/src/flask_login/login_manager.py +++ b/src/flask_login/login_manager.py @@ -1,7 +1,3 @@ -from datetime import datetime -from datetime import timedelta -from datetime import timezone - from flask import abort from flask import current_app from flask import flash @@ -417,29 +413,19 @@ def _set_cookie(self, response): samesite = config.get("REMEMBER_COOKIE_SAMESITE", COOKIE_SAMESITE) if "_remember_seconds" in session: - duration = timedelta(seconds=session["_remember_seconds"]) + # Convert float to int + duration = int(session["_remember_seconds"]) else: duration = config.get("REMEMBER_COOKIE_DURATION", COOKIE_DURATION) # prepare data data = encode_cookie(str(session["_user_id"])) - if isinstance(duration, int): - duration = timedelta(seconds=duration) - - try: - expires = datetime.now(timezone.utc) + duration - except TypeError as e: - raise Exception( - "REMEMBER_COOKIE_DURATION must be a datetime.timedelta," - f" instead got: {duration}" - ) from e - # actually set it response.set_cookie( cookie_name, value=data, - expires=expires, + max_age=duration, domain=domain, path=path, secure=secure, diff --git a/tests/test_login.py b/tests/test_login.py index 50f87872..8b67f730 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -1,12 +1,8 @@ import unittest from collections.abc import Hashable from contextlib import contextmanager -from datetime import datetime from datetime import timedelta -from datetime import timezone from unittest.mock import ANY -from unittest.mock import Mock -from unittest.mock import patch from flask import Blueprint from flask import Flask @@ -638,11 +634,8 @@ def test_remember_me_uses_custom_cookie_parameters(self): c.get("/login-notch-remember") cookie = c.get_cookie(name, domain, path) self.assertIsNotNone(cookie) - self.assertIsNotNone(cookie.expires) - expected_date = datetime.now(timezone.utc) + duration - difference = expected_date - cookie.expires - self.assertLess(difference, timedelta(seconds=10)) - self.assertGreater(difference, timedelta(seconds=-10)) + self.assertIsNotNone(cookie.max_age) + self.assertEqual(cookie.max_age, duration.total_seconds()) def test_remember_me_custom_duration_uses_custom_cookie(self): name = self.app.config["REMEMBER_COOKIE_NAME"] = "myname" @@ -654,11 +647,8 @@ def test_remember_me_custom_duration_uses_custom_cookie(self): c.get("/login-notch-remember-custom") cookie = c.get_cookie(name, domain, path) self.assertIsNotNone(cookie) - self.assertIsNotNone(cookie.expires) - expected_date = datetime.now(timezone.utc) + duration - difference = expected_date - cookie.expires - self.assertLess(difference, timedelta(seconds=10)) - self.assertGreater(difference, timedelta(seconds=-10)) + self.assertIsNotNone(cookie.max_age) + self.assertEqual(cookie.max_age, duration.total_seconds()) def test_remember_me_accepts_duration_as_int(self): self.app.config["REMEMBER_COOKIE_DURATION"] = 172800 @@ -670,11 +660,8 @@ def test_remember_me_accepts_duration_as_int(self): self.assertEqual(result.status_code, 200) cookie = c.get_cookie(name, domain) self.assertIsNotNone(cookie) - self.assertIsNotNone(cookie.expires) - expected_date = datetime.now(timezone.utc) + duration - difference = expected_date - cookie.expires - self.assertLess(difference, timedelta(seconds=10)) - self.assertGreater(difference, timedelta(seconds=-10)) + self.assertIsNotNone(cookie.max_age) + self.assertEqual(cookie.max_age, duration.total_seconds()) def test_remember_me_with_invalid_duration_returns_500_response(self): self.app.config["REMEMBER_COOKIE_DURATION"] = "123" @@ -693,19 +680,6 @@ def login_notch_remember_custom_invalid(): result = c.get("/login-notch-remember-custom-invalid") self.assertEqual(result.status_code, 500) - def test_set_cookie_with_invalid_duration_raises_exception(self): - self.app.config["REMEMBER_COOKIE_DURATION"] = "123" - - with self.assertRaises(Exception) as cm: - with self.app.test_request_context(): - session["_user_id"] = 2 - self.login_manager._set_cookie(None) - - expected_exception_message = ( - "REMEMBER_COOKIE_DURATION must be a datetime.timedelta, instead got: 123" - ) - self.assertIn(expected_exception_message, str(cm.exception)) - def test_set_cookie_with_invalid_custom_duration_raises_exception(self): with self.assertRaises(Exception) as cm: with self.app.test_request_context(): @@ -723,28 +697,23 @@ def test_remember_me_no_refresh_every_request(self): c = self.app.test_client() c.get("/login-notch-remember") cookie1 = c.get_cookie("remember", domain, path) - self.assertIsNotNone(cookie1.expires) + self.assertIsNotNone(cookie1.max_age) self._delete_session(c) c.get("/username") cookie2 = c.get_cookie("remember", domain, path) - self.assertEqual(cookie1.expires, cookie2.expires) + self.assertEqual(cookie1.max_age, cookie2.max_age) def test_remember_me_refresh_each_request(self): - with patch("flask_login.login_manager.datetime") as mock_dt: - now = datetime.now(timezone.utc) - mock_dt.now = Mock(return_value=now) - - domain = self.app.config["REMEMBER_COOKIE_DOMAIN"] = "localhost.local" - path = self.app.config["REMEMBER_COOKIE_PATH"] = "/" - self.app.config["REMEMBER_COOKIE_REFRESH_EACH_REQUEST"] = True - c = self.app.test_client() - c.get("/login-notch-remember") - cookie1 = c.get_cookie("remember", domain, path) - self.assertIsNotNone(cookie1.expires) - mock_dt.now.return_value = now + timedelta(seconds=1) - c.get("/username") - cookie2 = c.get_cookie("remember", domain, path) - self.assertNotEqual(cookie1.expires, cookie2.expires) + domain = self.app.config["REMEMBER_COOKIE_DOMAIN"] = "localhost.local" + path = self.app.config["REMEMBER_COOKIE_PATH"] = "/" + self.app.config["REMEMBER_COOKIE_REFRESH_EACH_REQUEST"] = True + c = self.app.test_client() + c.get("/login-notch-remember") + cookie1 = c.get_cookie("remember", domain, path) + self.assertIsNotNone(cookie1.max_age) + c.get("/username") + cookie2 = c.get_cookie("remember", domain, path) + self.assertEqual(cookie1.max_age, cookie2.max_age) def test_remember_me_is_unfresh(self): with self.app.test_client() as c: