Repository: pennersr/django-allauth
Pull request #4081 (2024-08-29 13:28:06)

docs: update CERN provider
Update CERN provider links, add clarifications to the description


Patch:
diff --git a/ChangeLog.rst b/ChangeLog.rst
index 6e206282..090369f3 100644
--- a/ChangeLog.rst
+++ b/ChangeLog.rst
@@ -9,6 +9,14 @@
   ``settings.ACCOUNT_LOGIN_BY_CODE_REQUIRED``.
 
 
+Security notice
+---------------
+
+- In case an ID token is used for authentication, the JTI is now respected to
+  prevent the possibility of replays instead of solely relying on the expiration
+  time.
+
+
 64.1.0 (2024-08-15)
 *******************
 
diff --git a/allauth/locale/ko/LC_MESSAGES/django.po b/allauth/locale/ko/LC_MESSAGES/django.po
index 43997abb..f588796e 100644
--- a/allauth/locale/ko/LC_MESSAGES/django.po
+++ b/allauth/locale/ko/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-08-22 08:29-0500\n"
+"POT-Creation-Date: 2024-08-27 04:25-0500\n"
 "PO-Revision-Date: 2024-04-20 21:50+0200\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -84,15 +84,15 @@ msgstr "해당 아이디는 이미 사용중입니다. 다른 사용자명을 
 msgid "The username and/or password you specified are not correct."
 msgstr "아이디 또는 비밀번호가 올바르지 않습니다."
 
-#: account/adapter.py:732
+#: account/adapter.py:736
 msgid "Use your password"
 msgstr "비밀번호를 사용하세요"
 
-#: account/adapter.py:741
+#: account/adapter.py:745
 msgid "Use authenticator app or code"
 msgstr "인증 앱 또는 코드를 사용하세요"
 
-#: account/adapter.py:748 templates/mfa/authenticate.html:36
+#: account/adapter.py:752 templates/mfa/authenticate.html:36
 #: templates/mfa/webauthn/reauthenticate.html:15
 msgid "Use a security key"
 msgstr "보안키를 사용하세요"
@@ -110,7 +110,7 @@ msgid "You must type the same password each time."
 msgstr "동일한 비밀번호를 입력해야 합니다."
 
 #: account/forms.py:91 account/forms.py:409 account/forms.py:569
-#: account/forms.py:683
+#: account/forms.py:681
 msgid "Password"
 msgstr "비밀번호"
 
@@ -119,7 +119,7 @@ msgid "Remember Me"
 msgstr "아이디 저장"
 
 #: account/forms.py:102 account/forms.py:269 account/forms.py:495
-#: account/forms.py:587 account/forms.py:700
+#: account/forms.py:587 account/forms.py:698
 msgid "Email address"
 msgstr "이메일 주소"
 
@@ -170,15 +170,15 @@ msgstr "비밀번호 (확인)"
 msgid "Current Password"
 msgstr "현재 비밀번호"
 
-#: account/forms.py:552 account/forms.py:636
+#: account/forms.py:552 account/forms.py:634
 msgid "New Password"
 msgstr "새 비밀번호"
 
-#: account/forms.py:553 account/forms.py:637
+#: account/forms.py:553 account/forms.py:635
 msgid "New Password (again)"
 msgstr "새 비밀번호 (확인)"
 
-#: account/forms.py:723 account/forms.py:725 mfa/base/forms.py:15
+#: account/forms.py:721 account/forms.py:723 mfa/base/forms.py:15
 #: mfa/base/forms.py:17 mfa/totp/forms.py:13
 msgid "Code"
 msgstr "코드"
@@ -317,7 +317,7 @@ msgstr "당신의 계정에는 인증된 이메일이 없습니다."
 
 #: socialaccount/adapter.py:44
 msgid "You cannot disconnect your last remaining third-party account."
-msgstr "마지막 남은 서드파트 계정은 연결해제 할수 없습니다."
+msgstr "마지막 남은 소셜 계정은 연결해제 할수 없습니다."
 
 #: socialaccount/adapter.py:47
 #: templates/socialaccount/messages/account_connected_other.txt:2
@@ -356,7 +356,7 @@ msgstr "비밀 키"
 msgid "API secret, client secret, or consumer secret"
 msgstr "API 비밀 키, 클라이언트 비밀 키, 또는 컨슈머 비밀 키"
 
-#: socialaccount/models.py:69 templates/mfa/webauthn/authenticator_list.html:16
+#: socialaccount/models.py:69 templates/mfa/webauthn/authenticator_list.html:18
 msgid "Key"
 msgstr "키"
 
@@ -569,8 +569,8 @@ msgid "Re-send Verification"
 msgstr "인증 재전송"
 
 #: templates/account/email.html:50
-#: templates/mfa/webauthn/authenticator_confirm_delete.html:14
-#: templates/mfa/webauthn/authenticator_list.html:58
+#: templates/mfa/webauthn/authenticator_confirm_delete.html:16
+#: templates/mfa/webauthn/authenticator_list.html:60
 #: templates/socialaccount/connections.html:40
 msgid "Remove"
 msgstr "제거"
@@ -1214,27 +1214,27 @@ msgid "You have added %(count)s security key."
 msgid_plural "You have added %(count)s security keys."
 msgstr[0] ""
 
-#: templates/mfa/index.html:53
-#: templates/mfa/webauthn/authenticator_list.html:11
+#: templates/mfa/index.html:54
+#: templates/mfa/webauthn/authenticator_list.html:12
 msgid "No security keys have been added."
 msgstr "보안키가 추가되지 않았습니다."
 
-#: templates/mfa/index.html:60
+#: templates/mfa/index.html:62
 msgid "Manage"
 msgstr "관리"
 
-#: templates/mfa/index.html:65 templates/mfa/webauthn/add_form.html:18
-#: templates/mfa/webauthn/authenticator_list.html:68
+#: templates/mfa/index.html:67 templates/mfa/webauthn/add_form.html:18
+#: templates/mfa/webauthn/authenticator_list.html:70
 msgid "Add"
 msgstr "추가"
 
-#: templates/mfa/index.html:75 templates/mfa/recovery_codes/base.html:4
+#: templates/mfa/index.html:77 templates/mfa/recovery_codes/base.html:4
 #: templates/mfa/recovery_codes/generate.html:6
 #: templates/mfa/recovery_codes/index.html:6
 msgid "Recovery Codes"
 msgstr "복구 코드"
 
-#: templates/mfa/index.html:80 templates/mfa/recovery_codes/index.html:9
+#: templates/mfa/index.html:82 templates/mfa/recovery_codes/index.html:9
 #, python-format
 msgid ""
 "There is %(unused_count)s out of %(total_count)s recovery codes available."
@@ -1242,19 +1242,19 @@ msgid_plural ""
 "There are %(unused_count)s out of %(total_count)s recovery codes available."
 msgstr[0] ""
 
-#: templates/mfa/index.html:84
+#: templates/mfa/index.html:86
 msgid "No recovery codes set up."
 msgstr "복구 코드가 설정되지 않았습니다."
 
-#: templates/mfa/index.html:94
+#: templates/mfa/index.html:96
 msgid "View"
 msgstr "보기"
 
-#: templates/mfa/index.html:100
+#: templates/mfa/index.html:102
 msgid "Download"
 msgstr "다운로드"
 
-#: templates/mfa/index.html:108 templates/mfa/recovery_codes/generate.html:29
+#: templates/mfa/index.html:110 templates/mfa/recovery_codes/generate.html:29
 msgid "Generate"
 msgstr "생성"
 
@@ -1342,41 +1342,41 @@ msgstr "보안키 추가"
 msgid "Remove Security Key"
 msgstr "보안키 제거"
 
-#: templates/mfa/webauthn/authenticator_confirm_delete.html:8
+#: templates/mfa/webauthn/authenticator_confirm_delete.html:9
 msgid "Are you sure you want to remove this security key?"
 msgstr "정말로 보안키를 제거하시겠습니까?"
 
-#: templates/mfa/webauthn/authenticator_list.html:19
+#: templates/mfa/webauthn/authenticator_list.html:21
 msgid "Usage"
 msgstr "사용"
 
-#: templates/mfa/webauthn/authenticator_list.html:31
+#: templates/mfa/webauthn/authenticator_list.html:33
 msgid "Passkey"
 msgstr "패스키"
 
-#: templates/mfa/webauthn/authenticator_list.html:35
+#: templates/mfa/webauthn/authenticator_list.html:37
 msgid "Security key"
 msgstr "보안 키"
 
-#: templates/mfa/webauthn/authenticator_list.html:38
+#: templates/mfa/webauthn/authenticator_list.html:40
 msgid "This key does not indicate whether it is a passkey."
 msgstr "이 키는 패스키인지 여부를 나타내지 않습니다."
 
-#: templates/mfa/webauthn/authenticator_list.html:39
+#: templates/mfa/webauthn/authenticator_list.html:41
 msgid "Unspecified"
 msgstr "지정되지 않음"
 
-#: templates/mfa/webauthn/authenticator_list.html:44
+#: templates/mfa/webauthn/authenticator_list.html:46
 #, python-format
 msgid "Added on %(created_at)s"
 msgstr "%(created_at)s에 추가됨"
 
-#: templates/mfa/webauthn/authenticator_list.html:46
+#: templates/mfa/webauthn/authenticator_list.html:48
 #, python-format
 msgid "Last used %(last_used)s"
 msgstr "%(last_used)s에 마지막으로 사용됨"
 
-#: templates/mfa/webauthn/authenticator_list.html:54
+#: templates/mfa/webauthn/authenticator_list.html:56
 msgid "Edit"
 msgstr "변경"
 
@@ -1395,12 +1395,12 @@ msgstr "이 기능은 자바스크립트를 요구합니다."
 #: templates/socialaccount/authentication_error.html:5
 #: templates/socialaccount/authentication_error.html:9
 msgid "Third-Party Login Failure"
-msgstr "서드파티 로그인 실패"
+msgstr "소셜 로그인 실패"
 
 #: templates/socialaccount/authentication_error.html:12
 msgid ""
 "An error occurred while attempting to login via your third-party account."
-msgstr "서드파티 계정을 통해 로그인 하는 도중 오류가 발생했습니다."
+msgstr "소셜 계정을 통해 로그인 하는 도중 오류가 발생했습니다."
 
 #: templates/socialaccount/connections.html:5
 #: templates/socialaccount/connections.html:9
@@ -1411,25 +1411,25 @@ msgstr "계정 연결"
 msgid ""
 "You can sign in to your account using any of the following third-party "
 "accounts:"
-msgstr "다음 서드파티 계정들을 통해 로그인 할 수 있습니다:"
+msgstr "다음 소셜 계정들을 통해 로그인 할 수 있습니다:"
 
 #: templates/socialaccount/connections.html:46
 msgid "You currently have no third-party accounts connected to this account."
-msgstr "해당 계정에 연결되어있는 서드파티 계정이 없습니다."
+msgstr "해당 계정에 연결되어있는 소셜 계정이 없습니다."
 
 #: templates/socialaccount/connections.html:50
 msgid "Add a Third-Party Account"
-msgstr "서드파티 계정을 추가하세요."
+msgstr "소셜 계정을 추가하세요."
 
 #: templates/socialaccount/email/account_connected_message.txt:4
 #, python-format
 msgid ""
 "A third-party account from %(provider)s has been connected to your account."
-msgstr "%(provider)s로 부터 서드파티 계정이 귀하의 계정에 연결되었습니다."
+msgstr "%(provider)s로 부터 소셜 계정이 귀하의 계정에 연결되었습니다."
 
 #: templates/socialaccount/email/account_connected_subject.txt:3
 msgid "Third-Party Account Connected"
-msgstr "서드파티 계정 연결됨."
+msgstr "소셜 계정 연결됨."
 
 #: templates/socialaccount/email/account_disconnected_message.txt:4
 #, python-format
@@ -1437,11 +1437,11 @@ msgid ""
 "A third-party account from %(provider)s has been disconnected from your "
 "account."
 msgstr ""
-"%(provider)s로 부터 서드파티 계정이 귀하의 계정에서 연결이 해제되었습니다."
+"%(provider)s로 부터 소셜 계정이 귀하의 계정에서 연결이 해제되었습니다."
 
 #: templates/socialaccount/email/account_disconnected_subject.txt:3
 msgid "Third-Party Account Disconnected"
-msgstr "서드파티 계정이 연결 해제됨"
+msgstr "소셜 계정이 연결 해제됨"
 
 #: templates/socialaccount/login.html:10
 #, python-format
@@ -1451,7 +1451,7 @@ msgstr "%(provider)s 계정 연결"
 #: templates/socialaccount/login.html:13
 #, python-format
 msgid "You are about to connect a new third-party account from %(provider)s."
-msgstr "%(provider)s에서 제공하는 서드파티 계정을 연결하려 합니다."
+msgstr "%(provider)s에서 제공하는 소셜 계정을 연결하려 합니다."
 
 #: templates/socialaccount/login.html:17
 #, python-format
@@ -1461,7 +1461,7 @@ msgstr "%(provider)s을 통한 로그인"
 #: templates/socialaccount/login.html:20
 #, python-format
 msgid "You are about to sign in using a third-party account from %(provider)s."
-msgstr "서드파티 %(provider)s의 계정을 사용해 로그인을 진행하려 합니다."
+msgstr "소셜 %(provider)s의 계정을 사용해 로그인을 진행하려 합니다."
 
 #: templates/socialaccount/login.html:27
 #: templates/socialaccount/login_redirect.html:10
@@ -1485,11 +1485,11 @@ msgstr ""
 
 #: templates/socialaccount/messages/account_connected.txt:2
 msgid "The third-party account has been connected."
-msgstr "서드파티 계정이 연결되었습니다."
+msgstr "소셜 계정이 연결되었습니다."
 
 #: templates/socialaccount/messages/account_disconnected.txt:2
 msgid "The third-party account has been disconnected."
-msgstr "서드파티 계정 연결이 해제되었습니다."
+msgstr "소셜 계정 연결이 해제되었습니다."
 
 #: templates/socialaccount/signup.html:12
 #, python-format
@@ -1502,7 +1502,7 @@ msgstr ""
 
 #: templates/socialaccount/snippets/login.html:10
 msgid "Or use a third-party"
-msgstr "또는 서드파티를 사용하세요."
+msgstr "또는 소셜을 사용하세요."
 
 #: templates/usersessions/messages/sessions_logged_out.txt:2
 msgid "Signed out of all other sessions."
diff --git a/allauth/socialaccount/internal/jwtkit.py b/allauth/socialaccount/internal/jwtkit.py
index f9ac4997..04d88dab 100644
--- a/allauth/socialaccount/internal/jwtkit.py
+++ b/allauth/socialaccount/internal/jwtkit.py
@@ -1,4 +1,7 @@
 import json
+import time
+
+from django.core.cache import cache
 
 import jwt
 from cryptography.hazmat.backends import default_backend
@@ -58,6 +61,21 @@ def fetch_key(credential, keys_url, lookup):
     return alg, key
 
 
+def verify_jti(data: dict) -> None:
+    """
+    Put the JWT token on a blacklist to prevent replay attacks.
+    """
+    iss = data.get("iss")
+    exp = data.get("exp")
+    jti = data.get("jti")
+    if iss is None or exp is None or jti is None:
+        return
+    timeout = exp - time.time()
+    key = f"jwt:iss={iss},jti={jti}"
+    if not cache.add(key=key, value=True, timeout=timeout):
+        raise OAuth2Error("token already used")
+
+
 def verify_and_decode(
     *, credential, keys_url, issuer, audience, lookup_kid, verify_signature=True
 ):
@@ -81,6 +99,7 @@ def verify_and_decode(
             audience=audience,
             algorithms=algorithms,
         )
+        verify_jti(data)
         return data
     except jwt.PyJWTError as e:
         raise OAuth2Error("Invalid id_token") from e
diff --git a/docs/socialaccount/providers/cern.rst b/docs/socialaccount/providers/cern.rst
index 9f2b65f1..6d9cd891 100644
--- a/docs/socialaccount/providers/cern.rst
+++ b/docs/socialaccount/providers/cern.rst
@@ -1,10 +1,9 @@
 CERN
 ----
 
-Starting from September 1st 2023, CERN upgraded their SSO to a standard OpenID
-Connect based solution. As a result, the previously builtin CERN provider is no
-longer needed and has been removed. Instead, use the regular OpenID Connect
-configuration::
+Members of the scientific commmunity affiliated with CERN (https://home.cern/about)
+can put their applications behind CERN SSO, which supports OIDC and SAML protocols.
+For OIDC, use the regular OpenID Connect configuration:
 
     SOCIALACCOUNT_PROVIDERS = {
         "openid_connect": {
@@ -15,15 +14,15 @@ configuration::
                     "client_id": "<insert-id>",
                     "secret": "<insert-secret>",
                     "settings": {
-                        "server_url": "https://auth.cern.ch/auth/realms/cern/.well-known/openid-configuration",
-                    },
+                        "server_url": "https://auth.cern.ch/auth/realms/cern/.well-known/openid-configuration"
+                    }
                 }
             ]
         }
     }
 
 App registration (get your key and secret here)
-    https://sso-management.web.cern.ch/OAuth/RegisterOAuthClient.aspx
+    https://application-portal.web.cern.ch/
 
-CERN OAuth2 Documentation
-    https://espace.cern.ch/authentication/CERN%20Authentication/OAuth.aspx
+Documentation
+    https://auth.docs.cern.ch/applications/application-configuration/


Test patch:
diff --git a/allauth/socialaccount/internal/tests/test_jwtkit.py b/allauth/socialaccount/internal/tests/test_jwtkit.py
new file mode 100644
index 00000000..2c4bbaf1
--- /dev/null
+++ b/allauth/socialaccount/internal/tests/test_jwtkit.py
@@ -0,0 +1,36 @@
+from datetime import timedelta
+
+from django.utils import timezone
+
+from allauth.socialaccount.internal.jwtkit import verify_and_decode
+from allauth.socialaccount.providers.apple.client import jwt_encode
+from allauth.socialaccount.providers.oauth2.client import OAuth2Error
+
+
+def test_verify_and_decode(enable_cache):
+    now = timezone.now()
+    payload = {
+        "iss": "https://accounts.google.com",
+        "azp": "client_id",
+        "aud": "client_id",
+        "sub": "108204268033311374519",
+        "hd": "example.com",
+        "locale": "en",
+        "iat": now,
+        "jti": "a4e9b64d5e31da48a2037216e4ba9a5f5f4f50a0",
+        "exp": now + timedelta(hours=1),
+    }
+    id_token = jwt_encode(payload, "secret")
+    for attempt in range(2):
+        try:
+            verify_and_decode(
+                credential=id_token,
+                keys_url="/",
+                issuer=payload["iss"],
+                audience=payload["aud"],
+                lookup_kid=False,
+                verify_signature=False,
+            )
+            assert attempt == 0
+        except OAuth2Error:
+            assert attempt == 1