Skip to content

Commit ce6ae11

Browse files
committed
Refactor existing / new implementations, bump year
Bump the year numbers 2024->2025, 2025->2026, to reflect that the existing implementation changed in 2025 and the new implementation probably won't be tested until 2026. Add the existing implementations as new files, matching the test filenames: * api/authentication_2025.py * api/views/terms_accepted_user_2025.py (extracted from ./privaterelay.py)
1 parent a566b7a commit ce6ae11

File tree

8 files changed

+307
-280
lines changed

8 files changed

+307
-280
lines changed

api/authentication.py

Lines changed: 15 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -16,48 +16,40 @@
1616
from allauth.socialaccount.models import SocialAccount
1717
from codetiming import Timer
1818
from markus.utils import generate_tag
19-
from rest_framework.authentication import (
20-
BaseAuthentication,
21-
TokenAuthentication,
22-
get_authorization_header,
23-
)
24-
from rest_framework.exceptions import (
25-
APIException,
26-
AuthenticationFailed,
27-
NotFound,
28-
ParseError,
29-
PermissionDenied,
30-
)
19+
from rest_framework.authentication import BaseAuthentication, TokenAuthentication
20+
from rest_framework.exceptions import APIException, AuthenticationFailed
3121
from rest_framework.request import Request
3222

3323
from emails.utils import histogram_if_enabled
3424

25+
from .authentication_2025 import FxaTokenAuthentication as FxaTokenAuthentication2025
26+
3527
logger = logging.getLogger("events")
3628
INTROSPECT_TOKEN_URL = "{}/introspect".format(
3729
settings.SOCIALACCOUNT_PROVIDERS["fxa"]["OAUTH_ENDPOINT"]
3830
)
3931

4032
# Specify the version strings in FXA_TOKEN_AUTH_VERSION
4133
#
42-
# The older version ("2024") works, but has a few issues.
34+
# The older version ("2025") works, but has a few issues.
4335
# The cache key changes between Python instances, so little or no cache hits are used.
4436
# Fetching a profile takes a few seconds, in which time another process can create a
4537
# SocialAccount, leading to IntegrityError. Some of these are tracked in MPP-3505.
4638
#
47-
# The newer version ("2025") addresses these issues, works more like a standard DRF
39+
# The newer version ("2026") addresses these issues, works more like a standard DRF
4840
# authentication class, expands the logged data, and tracks the time to call Accounts
4941
# introspection and profile APIs. However, it is unproven, so we're using an
5042
# environment variable to be able to try it in stage before production, and to
5143
# revert with a config change only.
5244
#
5345
# The names are designed to be annoying so they will be removed. The old code has
54-
# the suffix _2024 and the new code _2025 (when needed). When the new code is
46+
# the suffix _2025 and the new code _2026 (when needed). When the new code is
5547
# proven, the old code can be removed with minimal name changes.
5648
#
57-
# ruff thinks the strings "2024" and "2025" are passwords (check S105 / S106).
49+
# ruff thinks the strings "2025" and "2026" are passwords (check S105 / S106).
5850
# These constants allow telling ruff to ignore them once.
59-
FXA_TOKEN_AUTH_OLD_AND_PROVEN = "2024" # noqa: S105
60-
FXA_TOKEN_AUTH_NEW_AND_BUSTED = "2025" # noqa: S105
51+
FXA_TOKEN_AUTH_OLD_AND_PROVEN = "2025" # noqa: S105
52+
FXA_TOKEN_AUTH_NEW_AND_BUSTED = "2026" # noqa: S105
6153

6254

6355
class CachedFxaIntrospectResponse(TypedDict, total=False):
@@ -94,11 +86,6 @@ class FxaIntrospectCompleteData(TypedDict):
9486
exp: int
9587

9688

97-
def get_cache_key_2024(token):
98-
"""note: hash() returns different results in different Python processes."""
99-
return hash(token)
100-
101-
10289
def get_cache_key(token: str) -> str:
10390
return f"introspect_result:v1:{sha256(token.encode()).hexdigest()}"
10491

@@ -358,96 +345,6 @@ def load_introspection_result_from_cache(
358345
return response
359346

360347

361-
def introspect_token_2024(token: str) -> dict[str, Any]:
362-
try:
363-
fxa_resp = requests.post(
364-
INTROSPECT_TOKEN_URL,
365-
json={"token": token},
366-
timeout=settings.FXA_REQUESTS_TIMEOUT_SECONDS,
367-
)
368-
except Exception as exc:
369-
logger.error(
370-
"Could not introspect token with FXA.",
371-
extra={"error_cls": type(exc), "error": shlex.quote(str(exc))},
372-
)
373-
raise AuthenticationFailed("Could not introspect token with FXA.")
374-
375-
fxa_resp_data = {"status_code": fxa_resp.status_code, "json": {}}
376-
try:
377-
fxa_resp_data["json"] = fxa_resp.json()
378-
except requests.exceptions.JSONDecodeError:
379-
logger.error(
380-
"JSONDecodeError from FXA introspect response.",
381-
extra={"fxa_response": shlex.quote(fxa_resp.text)},
382-
)
383-
raise AuthenticationFailed("JSONDecodeError from FXA introspect response")
384-
return fxa_resp_data
385-
386-
387-
def get_fxa_uid_from_oauth_token_2024(token: str, use_cache: bool = True) -> str:
388-
# set a default cache_timeout, but this will be overridden to match
389-
# the 'exp' time in the JWT returned by FxA
390-
cache_timeout = 60
391-
cache_key = get_cache_key_2024(token)
392-
393-
if not use_cache:
394-
fxa_resp_data = introspect_token_2024(token)
395-
else:
396-
# set a default fxa_resp_data, so any error during introspection
397-
# will still cache for at least cache_timeout to prevent an outage
398-
# from causing useless run-away repetitive introspection requests
399-
fxa_resp_data = {"status_code": None, "json": {}}
400-
try:
401-
cached_fxa_resp_data = cache.get(cache_key)
402-
403-
if cached_fxa_resp_data:
404-
fxa_resp_data = cached_fxa_resp_data
405-
else:
406-
# no cached data, get new
407-
fxa_resp_data = introspect_token_2024(token)
408-
except AuthenticationFailed:
409-
raise
410-
finally:
411-
# Store potential valid response, errors, inactive users, etc. from FxA
412-
# for at least 60 seconds. Valid access_token cache extended after checking.
413-
cache.set(cache_key, fxa_resp_data, cache_timeout)
414-
415-
if fxa_resp_data["status_code"] is None:
416-
raise APIException("Previous FXA call failed, wait to retry.")
417-
418-
if not fxa_resp_data["status_code"] == 200:
419-
raise APIException("Did not receive a 200 response from FXA.")
420-
421-
if not fxa_resp_data["json"].get("active"):
422-
raise AuthenticationFailed("FXA returned active: False for token.")
423-
424-
# FxA user is active, check for the associated Relay account
425-
if (raw_fxa_uid := fxa_resp_data.get("json", {}).get("sub")) is None:
426-
raise NotFound("FXA did not return an FXA UID.")
427-
fxa_uid = str(raw_fxa_uid)
428-
429-
scopes = fxa_resp_data.get("json", {}).get("scope", "").split()
430-
if settings.RELAY_SCOPE not in scopes:
431-
raise AuthenticationFailed(
432-
"FXA token is missing scope: https://identity.mozilla.com/apps/relay."
433-
)
434-
435-
# cache valid access_token and fxa_resp_data until access_token expiration
436-
# TODO: revisit this since the token can expire before its time
437-
if isinstance(fxa_resp_data.get("json", {}).get("exp"), int):
438-
# Note: FXA iat and exp are timestamps in *milliseconds*
439-
fxa_token_exp_time = int(fxa_resp_data["json"]["exp"] / 1000)
440-
now_time = int(datetime.now(UTC).timestamp())
441-
fxa_token_exp_cache_timeout = fxa_token_exp_time - now_time
442-
if fxa_token_exp_cache_timeout > cache_timeout:
443-
# cache until access_token expires (matched Relay user)
444-
# this handles cases where the token already expired
445-
cache_timeout = fxa_token_exp_cache_timeout
446-
cache.set(cache_key, fxa_resp_data, cache_timeout)
447-
448-
return fxa_uid
449-
450-
451348
def introspect_token(token: str) -> IntrospectionResponse | IntrospectionError:
452349
"""
453350
Validate an Accounts OAuth token with the introspect API.
@@ -601,15 +498,15 @@ def introspect_and_cache_token(
601498

602499

603500
class FxaTokenAuthentication(BaseAuthentication):
604-
"""Pick 2024 or 2025 version based on settings"""
501+
"""Pick 2025 or 2026 version based on settings"""
605502

606-
_impl: FxaTokenAuthentication2024 | FxaTokenAuthentication2025
503+
_impl: FxaTokenAuthentication2025 | FxaTokenAuthentication2026
607504

608505
def __init__(self) -> None:
609506
if settings.FXA_TOKEN_AUTH_VERSION == FXA_TOKEN_AUTH_NEW_AND_BUSTED:
610-
self._impl = FxaTokenAuthentication2025()
507+
self._impl = FxaTokenAuthentication2026()
611508
else:
612-
self._impl = FxaTokenAuthentication2024()
509+
self._impl = FxaTokenAuthentication2025()
613510

614511
def authenticate_header(self, request: Request) -> Any | str | None:
615512
return self._impl.authenticate_header(request)
@@ -620,56 +517,7 @@ def authenticate(
620517
return self._impl.authenticate(request)
621518

622519

623-
class FxaTokenAuthentication2024(BaseAuthentication):
624-
def authenticate_header(self, request):
625-
# Note: we need to implement this function to make DRF return a 401 status code
626-
# when we raise AuthenticationFailed, rather than a 403. See:
627-
# https://www.django-rest-framework.org/api-guide/authentication/#custom-authentication
628-
return "Bearer"
629-
630-
def authenticate(self, request):
631-
authorization = get_authorization_header(request).decode()
632-
if not authorization or not authorization.startswith("Bearer "):
633-
# If the request has no Bearer token, return None to attempt the next
634-
# auth scheme in the REST_FRAMEWORK AUTHENTICATION_CLASSES list
635-
return None
636-
637-
token = authorization.split(" ")[1]
638-
if token == "":
639-
raise ParseError("Missing FXA Token after 'Bearer'.")
640-
641-
use_cache = True
642-
method = request.method
643-
if method in ["POST", "DELETE", "PUT"]:
644-
use_cache = False
645-
if method == "POST" and request.path == "/api/v1/relayaddresses/":
646-
use_cache = True
647-
fxa_uid = get_fxa_uid_from_oauth_token_2024(token, use_cache)
648-
try:
649-
# MPP-3021: select_related user object to save DB query
650-
sa = SocialAccount.objects.filter(
651-
uid=fxa_uid, provider="fxa"
652-
).select_related("user")[0]
653-
except IndexError:
654-
raise PermissionDenied(
655-
"Authenticated user does not have a Relay account."
656-
" Have they accepted the terms?"
657-
)
658-
user = sa.user
659-
660-
if not user.is_active:
661-
raise PermissionDenied(
662-
"Authenticated user does not have an active Relay account."
663-
" Have they been deactivated?"
664-
)
665-
666-
if user:
667-
return (user, token)
668-
else:
669-
raise NotFound()
670-
671-
672-
class FxaTokenAuthentication2025(TokenAuthentication):
520+
class FxaTokenAuthentication2026(TokenAuthentication):
673521
"""
674522
Implement authentication with a Mozilla Account bearer token.
675523

0 commit comments

Comments
 (0)