• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / fx-private-relay / b2e067fe-ce4e-4099-9bef-07b368e99782

15 Apr 2024 04:18PM CUT coverage: 75.544% (+0.002%) from 75.542%
b2e067fe-ce4e-4099-9bef-07b368e99782

push

circleci

jwhitlock
Enable pyupgrade, fix issues

2443 of 3405 branches covered (71.75%)

Branch coverage included in aggregate %.

56 of 59 new or added lines in 14 files covered. (94.92%)

234 existing lines in 24 files now uncovered.

6793 of 8821 relevant lines covered (77.01%)

20.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.59
/api/authentication.py
1
import logging
1✔
2
import shlex
1✔
3
from datetime import UTC, datetime
1✔
4
from typing import Any
1✔
5

6
from django.conf import settings
1✔
7
from django.core.cache import cache
1✔
8

9
import requests
1✔
10
from allauth.socialaccount.models import SocialAccount
1✔
11
from rest_framework.authentication import BaseAuthentication, get_authorization_header
1✔
12
from rest_framework.exceptions import (
1✔
13
    APIException,
14
    AuthenticationFailed,
15
    NotFound,
16
    ParseError,
17
    PermissionDenied,
18
)
19

20
logger = logging.getLogger("events")
1✔
21
INTROSPECT_TOKEN_URL = (
1✔
22
    "%s/introspect" % settings.SOCIALACCOUNT_PROVIDERS["fxa"]["OAUTH_ENDPOINT"]
23
)
24

25

26
def get_cache_key(token):
1✔
27
    return hash(token)
1✔
28

29

30
def introspect_token(token: str) -> dict[str, Any]:
1✔
31
    try:
1✔
32
        fxa_resp = requests.post(INTROSPECT_TOKEN_URL, json={"token": token})
1✔
UNCOV
33
    except Exception as exc:
×
UNCOV
34
        logger.error(
×
35
            "Could not introspect token with FXA.",
36
            extra={"error_cls": type(exc), "error": shlex.quote(str(exc))},
37
        )
UNCOV
38
        raise AuthenticationFailed("Could not introspect token with FXA.")
×
39

40
    fxa_resp_data = {"status_code": fxa_resp.status_code, "json": {}}
1✔
41
    try:
1✔
42
        fxa_resp_data["json"] = fxa_resp.json()
1✔
43
    except requests.exceptions.JSONDecodeError:
1✔
44
        logger.error(
1✔
45
            "JSONDecodeError from FXA introspect response.",
46
            extra={"fxa_response": shlex.quote(fxa_resp.text)},
47
        )
48
        raise AuthenticationFailed("JSONDecodeError from FXA introspect response")
1✔
49
    return fxa_resp_data
1✔
50

51

52
def get_fxa_uid_from_oauth_token(token: str, use_cache=True) -> str:
1✔
53
    # set a default cache_timeout, but this will be overriden to match
54
    # the 'exp' time in the JWT returned by FxA
55
    cache_timeout = 60
1✔
56
    cache_key = get_cache_key(token)
1✔
57

58
    if not use_cache:
1✔
59
        fxa_resp_data = introspect_token(token)
1✔
60
    else:
61
        # set a default fxa_resp_data, so any error during introspection
62
        # will still cache for at least cache_timeout to prevent an outage
63
        # from causing useless run-away repetitive introspection requests
64
        fxa_resp_data = {"status_code": None, "json": {}}
1✔
65
        try:
1✔
66
            cached_fxa_resp_data = cache.get(cache_key)
1✔
67

68
            if cached_fxa_resp_data:
1✔
69
                fxa_resp_data = cached_fxa_resp_data
1✔
70
            else:
71
                # no cached data, get new
72
                fxa_resp_data = introspect_token(token)
1✔
73
        except AuthenticationFailed:
1✔
74
            raise
1✔
75
        finally:
76
            # Store potential valid response, errors, inactive users, etc. from FxA
77
            # for at least 60 seconds. Valid access_token cache extended after checking.
78
            cache.set(cache_key, fxa_resp_data, cache_timeout)
1✔
79

80
    if fxa_resp_data["status_code"] is None:
1✔
81
        raise APIException("Previous FXA call failed, wait to retry.")
1✔
82

83
    if not fxa_resp_data["status_code"] == 200:
1✔
84
        raise APIException("Did not receive a 200 response from FXA.")
1✔
85

86
    if not fxa_resp_data["json"].get("active"):
1✔
87
        raise AuthenticationFailed("FXA returned active: False for token.")
1✔
88

89
    # FxA user is active, check for the associated Relay account
90
    if (raw_fxa_uid := fxa_resp_data.get("json", {}).get("sub")) is None:
1✔
91
        raise NotFound("FXA did not return an FXA UID.")
1✔
92
    fxa_uid = str(raw_fxa_uid)
1✔
93

94
    # cache valid access_token and fxa_resp_data until access_token expiration
95
    # TODO: revisit this since the token can expire before its time
96
    if isinstance(fxa_resp_data.get("json", {}).get("exp"), int):
1✔
97
        # Note: FXA iat and exp are timestamps in *milliseconds*
98
        fxa_token_exp_time = int(fxa_resp_data["json"]["exp"] / 1000)
1✔
99
        now_time = int(datetime.now(UTC).timestamp())
1✔
100
        fxa_token_exp_cache_timeout = fxa_token_exp_time - now_time
1✔
101
        if fxa_token_exp_cache_timeout > cache_timeout:
1!
102
            # cache until access_token expires (matched Relay user)
103
            # this handles cases where the token already expired
104
            cache_timeout = fxa_token_exp_cache_timeout
1✔
105
    cache.set(cache_key, fxa_resp_data, cache_timeout)
1✔
106

107
    return fxa_uid
1✔
108

109

110
class FxaTokenAuthentication(BaseAuthentication):
1✔
111
    def authenticate_header(self, request):
1✔
112
        # Note: we need to implement this function to make DRF return a 401 status code
113
        # when we raise AuthenticationFailed, rather than a 403. See:
114
        # https://www.django-rest-framework.org/api-guide/authentication/#custom-authentication
115
        return "Bearer"
1✔
116

117
    def authenticate(self, request):
1✔
118
        authorization = get_authorization_header(request).decode()
1✔
119
        if not authorization or not authorization.startswith("Bearer "):
1✔
120
            # If the request has no Bearer token, return None to attempt the next
121
            # auth scheme in the REST_FRAMEWORK AUTHENTICATION_CLASSES list
122
            return None
1✔
123

124
        token = authorization.split(" ")[1]
1✔
125
        if token == "":
1✔
126
            raise ParseError("Missing FXA Token after 'Bearer'.")
1✔
127

128
        use_cache = True
1✔
129
        method = request.method
1✔
130
        if method in ["POST", "DELETE", "PUT"]:
1✔
131
            use_cache = False
1✔
132
            if method == "POST" and request.path == "/api/v1/relayaddresses/":
1✔
133
                use_cache = True
1✔
134
        fxa_uid = get_fxa_uid_from_oauth_token(token, use_cache)
1✔
135
        try:
1✔
136
            # MPP-3021: select_related user object to save DB query
137
            sa = SocialAccount.objects.filter(
1✔
138
                uid=fxa_uid, provider="fxa"
139
            ).select_related("user")[0]
140
        except IndexError:
1✔
141
            raise PermissionDenied(
1✔
142
                "Authenticated user does not have a Relay account."
143
                " Have they accepted the terms?"
144
            )
145
        user = sa.user
1✔
146

147
        if user:
1!
148
            return (user, token)
1✔
149
        else:
UNCOV
150
            raise NotFound()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc