Skip to content

Commit

Permalink
fix(socialaccount): JWT: Prevent replay using JTI
Browse files Browse the repository at this point in the history
  • Loading branch information
pennersr committed Aug 29, 2024
1 parent d30b641 commit 0304b1b
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 0 deletions.
8 changes: 8 additions & 0 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*******************

Expand Down
19 changes: 19 additions & 0 deletions allauth/socialaccount/internal/jwtkit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import json
import time

from django.core.cache import cache

import jwt
from cryptography.hazmat.backends import default_backend
Expand Down Expand Up @@ -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
):
Expand All @@ -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
36 changes: 36 additions & 0 deletions allauth/socialaccount/internal/tests/test_jwtkit.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 0304b1b

Please sign in to comment.