1616from allauth .socialaccount .models import SocialAccount
1717from codetiming import Timer
1818from 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
3121from rest_framework .request import Request
3222
3323from emails .utils import histogram_if_enabled
3424
25+ from .authentication_2025 import FxaTokenAuthentication as FxaTokenAuthentication2025
26+
3527logger = logging .getLogger ("events" )
3628INTROSPECT_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
6355class 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-
10289def 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-
451348def 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
603500class 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