Repository: maxcountryman/flask-login

Issue:
datetime.utcnow and datetime.utcfromtimestamp are deprecated in Python 3.12
**Describe the bug**

datetime.utcnow and datetime.utcfromtimestamp are deprecated in Python 3.12 as per the changelog.

> [datetime.datetime](https://docs.python.org/3.12/library/datetime.html#datetime.datetime)’s [utcnow()](https://docs.python.org/3.12/library/datetime.html#datetime.datetime.utcnow) and [utcfromtimestamp()](https://docs.python.org/3.12/library/datetime.html#datetime.datetime.utcfromtimestamp) are deprecated and will be removed in a future version. Instead, use timezone-aware objects to represent datetimes in UTC: respectively, call [now()](https://docs.python.org/3.12/library/datetime.html#datetime.datetime.now) and [fromtimestamp()](https://docs.python.org/3.12/library/datetime.html#datetime.datetime.fromtimestamp) with the tz parameter set to [datetime.UTC](https://docs.python.org/3.12/library/datetime.html#datetime.UTC). (Contributed by Paul Ganssle in https://github.com/python/cpython/issues/103857.)

**To Reproduce**

Following lines might raise DeprecationWarning in Python 3.12

```
flask-login/src/flask_login/login_manager.py
430:            expires = datetime.utcnow() + duration

flask-login/tests/test_login.py
689:            expiration_date = datetime.utcfromtimestamp(cookie.expires)
690:            expected_date = datetime.utcnow() + duration
722:            expiration_date = datetime.utcfromtimestamp(cookie.expires)
723:            expected_date = datetime.utcnow() + duration
745:            expiration_date = datetime.utcfromtimestamp(cookie.expires)
746:            expected_date = datetime.utcnow() + duration
805:            expiration_date_1 = datetime.utcfromtimestamp(
813:            expiration_date_2 = datetime.utcfromtimestamp(
821:            now = datetime.utcnow()
827:                expiration_date_1 = datetime.utcfromtimestamp(
837:                expiration_date_2 = datetime.utcfromtimestamp(
```

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


Discussion:

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index e2cb677..accad15 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,6 +10,7 @@ Unreleased
 - Bump to Flask >= 2.3.0.
 - Bump to Werkzeug >= 2.3.2.
 - Remove previously deprecated code. #694
+- Use `datetime.now(timezone.utc)` instead of deprecated `datetime.utcnow`. #758
 
 
 Version 0.6.3
diff --git a/src/flask_login/login_manager.py b/src/flask_login/login_manager.py
index f7eb25f..795e744 100644
--- a/src/flask_login/login_manager.py
+++ b/src/flask_login/login_manager.py
@@ -1,5 +1,6 @@
 from datetime import datetime
 from datetime import timedelta
+from datetime import timezone
 
 from flask import abort
 from flask import current_app
@@ -427,7 +428,7 @@ class LoginManager:
             duration = timedelta(seconds=duration)
 
         try:
-            expires = datetime.utcnow() + duration
+            expires = datetime.now(timezone.utc) + duration
         except TypeError as e:
             raise Exception(
                 "REMEMBER_COOKIE_DURATION must be a datetime.timedelta,"


Test patch:
diff --git a/tests/test_login.py b/tests/test_login.py
index b285a4f..edf0ad0 100644
--- a/tests/test_login.py
+++ b/tests/test_login.py
@@ -765,8 +765,8 @@ class LoginTestCase(unittest.TestCase):
 
     def test_remember_me_refresh_each_request(self):
         with patch("flask_login.login_manager.datetime") as mock_dt:
-            now = datetime.utcnow()
-            mock_dt.utcnow = Mock(return_value=now)
+            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"] = "/"
@@ -775,7 +775,7 @@ class LoginTestCase(unittest.TestCase):
             c.get("/login-notch-remember")
             cookie1 = c.get_cookie("remember", domain, path)
             self.assertIsNotNone(cookie1.expires)
-            mock_dt.utcnow.return_value = now + timedelta(seconds=1)
+            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)