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

mozilla / fx-private-relay / 84353759-c057-4f20-b282-724c34504dc9

26 Nov 2025 04:22PM UTC coverage: 89.192% (+0.4%) from 88.772%
84353759-c057-4f20-b282-724c34504dc9

Pull #6049

circleci

jwhitlock
Update TermsAcceptedUserViewTest for new errors
Pull Request #6049: fix(relay): Create alternate bearer token auth for FxA (MPP-3505)

3016 of 4041 branches covered (74.63%)

Branch coverage included in aggregate %.

1334 of 1349 new or added lines in 9 files covered. (98.89%)

6 existing lines in 2 files now uncovered.

19067 of 20718 relevant lines covered (92.03%)

11.09 hits per line

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

90.4
/api/views/privaterelay.py
1
from logging import getLogger
1✔
2
from typing import Any, Literal
1✔
3

4
from django.conf import settings
1✔
5
from django.contrib.auth.models import User
1✔
6
from django.db.models.query import QuerySet
1✔
7

8
import requests
1✔
9
from allauth.account.adapter import get_adapter as get_account_adapter
1✔
10
from allauth.socialaccount.adapter import get_adapter as get_social_adapter
1✔
11
from allauth.socialaccount.helpers import complete_social_login
1✔
12
from allauth.socialaccount.models import SocialAccount
1✔
13
from codetiming import Timer
1✔
14
from django_filters.rest_framework import FilterSet
1✔
15
from drf_spectacular.utils import (
1✔
16
    OpenApiExample,
17
    OpenApiParameter,
18
    OpenApiRequest,
19
    OpenApiResponse,
20
    extend_schema,
21
)
22
from markus.utils import generate_tag
1✔
23
from rest_framework.authentication import get_authorization_header
1✔
24
from rest_framework.decorators import (
1✔
25
    api_view,
26
    authentication_classes,
27
    permission_classes,
28
)
29
from rest_framework.exceptions import AuthenticationFailed, ErrorDetail, ParseError
1✔
30
from rest_framework.permissions import AllowAny, IsAuthenticated
1✔
31
from rest_framework.request import Request
1✔
32
from rest_framework.response import Response
1✔
33
from rest_framework.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST
1✔
34
from rest_framework.viewsets import ModelViewSet
1✔
35
from waffle import get_waffle_flag_model
1✔
36
from waffle.models import Sample, Switch
1✔
37

38
from emails.utils import histogram_if_enabled, incr_if_enabled
1✔
39
from privaterelay.models import Profile
1✔
40
from privaterelay.plans import (
1✔
41
    PlanCountryLangMapping,
42
    get_bundle_country_language_mapping,
43
    get_megabundle_country_language_mapping,
44
    get_phone_country_language_mapping,
45
    get_premium_country_language_mapping,
46
)
47
from privaterelay.sp3_plans import (
1✔
48
    SP3PlanCountryLangMapping,
49
    get_sp3_country_language_mapping,
50
)
51
from privaterelay.utils import get_countries_info_from_request_and_mapping
1✔
52

53
from ..authentication import (
1✔
54
    FXA_TOKEN_AUTH_NEW_AND_BUSTED,
55
    FxaTokenAuthentication,
56
    IntrospectionResponse,
57
)
58
from ..authentication import (
1✔
59
    get_fxa_uid_from_oauth_token_2024 as get_fxa_uid_from_oauth_token,
60
)
61
from ..permissions import CanManageFlags, HasValidFxaToken, IsActive, IsNewUser, IsOwner
1✔
62
from ..serializers.privaterelay import (
1✔
63
    FlagSerializer,
64
    ProfileSerializer,
65
    UserSerializer,
66
    WebcompatIssueSerializer,
67
)
68

69
logger = getLogger("events")
1✔
70
info_logger = getLogger("eventsinfo")
1✔
71
FXA_PROFILE_URL = (
1✔
72
    f"{settings.SOCIALACCOUNT_PROVIDERS['fxa']['PROFILE_ENDPOINT']}/profile"
73
)
74

75

76
class FlagFilter(FilterSet):
1✔
77
    class Meta:
1✔
78
        model = get_waffle_flag_model()
1✔
79
        fields = [
1✔
80
            "name",
81
            "everyone",
82
            # "users",
83
            # read-only
84
            "id",
85
        ]
86

87

88
@extend_schema(tags=["privaterelay"])
1✔
89
class FlagViewSet(ModelViewSet):
1✔
90
    """Feature flags."""
91

92
    serializer_class = FlagSerializer
1✔
93
    permission_classes = [IsAuthenticated, CanManageFlags]
1✔
94
    filterset_class = FlagFilter
1✔
95
    http_method_names = ["get", "post", "head", "patch"]
1✔
96

97
    def get_queryset(self):
1✔
98
        flags = get_waffle_flag_model().objects
1✔
99
        return flags
1✔
100

101

102
@extend_schema(tags=["privaterelay"])
1✔
103
class ProfileViewSet(ModelViewSet):
1✔
104
    """Relay user extended profile data."""
105

106
    serializer_class = ProfileSerializer
1✔
107
    permission_classes = [IsAuthenticated, IsOwner]
1✔
108
    http_method_names = ["get", "post", "head", "put", "patch"]
1✔
109

110
    def get_queryset(self) -> QuerySet[Profile]:
1✔
111
        if isinstance(self.request.user, User):
1✔
112
            return Profile.objects.filter(user=self.request.user)
1✔
113
        return Profile.objects.none()
1✔
114

115

116
@extend_schema(tags=["privaterelay"])
1✔
117
class UserViewSet(ModelViewSet):
1✔
118
    """Relay user data stored in Django user model."""
119

120
    serializer_class = UserSerializer
1✔
121
    permission_classes = [IsAuthenticated, IsOwner]
1✔
122
    http_method_names = ["get", "head"]
1✔
123

124
    def get_queryset(self) -> QuerySet[User]:
1✔
125
        if isinstance(self.request.user, User):
1!
UNCOV
126
            return User.objects.filter(id=self.request.user.id)
×
127
        return User.objects.none()
1✔
128

129

130
@permission_classes([IsAuthenticated])
1✔
131
@extend_schema(
1✔
132
    tags=["privaterelay"],
133
    request=WebcompatIssueSerializer,
134
    examples=[
135
        OpenApiExample(
136
            "mask not accepted",
137
            {
138
                "issue_on_domain": "https://accounts.firefox.com",
139
                "user_agent": "Firefox",
140
                "email_mask_not_accepted": True,
141
                "add_on_visual_issue": False,
142
                "email_not_received": False,
143
                "other_issue": "",
144
            },
145
        )
146
    ],
147
    responses={
148
        "201": OpenApiResponse(description="Report was submitted"),
149
        "400": OpenApiResponse(description="Report was rejected due to errors."),
150
        "401": OpenApiResponse(description="Authentication required."),
151
    },
152
)
153
@api_view(["POST"])
1✔
154
def report_webcompat_issue(request):
1✔
155
    """Report a Relay issue from an extension or integration."""
156

157
    serializer = WebcompatIssueSerializer(data=request.data)
×
158
    if serializer.is_valid():
×
159
        info_logger.info("webcompat_issue", extra=serializer.data)
×
160
        incr_if_enabled("webcompat_issue", 1)
×
161
        for k, v in serializer.data.items():
×
UNCOV
162
            if v and k != "issue_on_domain":
×
UNCOV
163
                incr_if_enabled(f"webcompat_issue_{k}", 1)
×
UNCOV
164
        return Response(status=HTTP_201_CREATED)
×
UNCOV
165
    return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
×
166

167

168
def _get_example_plan(
1✔
169
    plan: Literal["premium", "phones", "bundle", "megabundle"],
170
) -> dict[str, Any]:
171
    prices = {
1✔
172
        "premium": {"monthly": 1.99, "yearly": 0.99},
173
        "phones": {"monthly": 4.99, "yearly": 4.99},
174
        "bundle": {"monthly": 6.99, "yearly": 6.99},
175
        "megabundle": {"monthly": 8.25, "yearly": 99},
176
    }
177
    monthly_price = {
1✔
178
        "id": f"price_{plan.title()}Monthlyxxxx",
179
        "currency": "usd",
180
        "price": prices[plan]["monthly"],
181
    }
182
    yearly_price = {
1✔
183
        "id": f"price_{plan.title()}Yearlyxxxx",
184
        "currency": "usd",
185
        "price": prices[plan]["yearly"],
186
    }
187
    return {
1✔
188
        "country_code": "US",
189
        "countries": ["CA", "US"],
190
        "available_in_country": True,
191
        "plan_country_lang_mapping": {
192
            "CA": {
193
                "*": {
194
                    "monthly": monthly_price,
195
                    "yearly": yearly_price,
196
                }
197
            },
198
            "US": {
199
                "*": {
200
                    "monthly": monthly_price,
201
                    "yearly": yearly_price,
202
                }
203
            },
204
        },
205
    }
206

207

208
@extend_schema(
1✔
209
    tags=["privaterelay"],
210
    responses={
211
        "200": OpenApiResponse(
212
            {"type": "object"},
213
            description="Site parameters",
214
            examples=[
215
                OpenApiExample(
216
                    "relay.firefox.com (partial)",
217
                    {
218
                        "FXA_ORIGIN": "https://accounts.firefox.com",
219
                        "PERIODICAL_PREMIUM_PRODUCT_ID": "prod_XXXXXXXXXXXXXX",
220
                        "GOOGLE_ANALYTICS_ID": "UA-########-##",
221
                        "GA4_MEASUREMENT_ID": "G-XXXXXXXXX",
222
                        "BUNDLE_PRODUCT_ID": "prod_XXXXXXXXXXXXXX",
223
                        "MEGABUNDLE_PRODUCT_ID": "prod_XXXXXXXXXXXXXX",
224
                        "PHONE_PRODUCT_ID": "prod_XXXXXXXXXXXXXX",
225
                        "PERIODICAL_PREMIUM_PLANS": _get_example_plan("premium"),
226
                        "PHONE_PLANS": _get_example_plan("phones"),
227
                        "BUNDLE_PLANS": _get_example_plan("bundle"),
228
                        "MEGABUNDLE_PLANS": _get_example_plan("megabundle"),
229
                        "BASKET_ORIGIN": "https://basket.mozilla.org",
230
                        "WAFFLE_FLAGS": [
231
                            ["foxfood", False],
232
                            ["phones", True],
233
                            ["bundle", True],
234
                            ["megabundle", True],
235
                        ],
236
                        "WAFFLE_SWITCHES": [],
237
                        "WAFFLE_SAMPLES": [],
238
                        "MAX_MINUTES_TO_VERIFY_REAL_PHONE": 5,
239
                    },
240
                )
241
            ],
242
        )
243
    },
244
)
245
@api_view()
1✔
246
@permission_classes([AllowAny])
1✔
247
def runtime_data(request):
1✔
248
    """Get data needed to present the Relay dashboard to a visitor or user."""
249
    flags = get_waffle_flag_model().get_all()
1✔
250
    flag_values = [(f.name, f.is_active(request)) for f in flags]
1✔
251
    switches = Switch.get_all()
1✔
252
    switch_values = [(s.name, s.is_active()) for s in switches]
1✔
253
    samples = Sample.get_all()
1✔
254
    sample_values = [(s.name, s.is_active()) for s in samples]
1✔
255
    premium_plans: SP3PlanCountryLangMapping | PlanCountryLangMapping
256
    phone_plans: SP3PlanCountryLangMapping | PlanCountryLangMapping
257
    bundle_plans: SP3PlanCountryLangMapping | PlanCountryLangMapping
258
    megabundle_plans: SP3PlanCountryLangMapping | PlanCountryLangMapping
259
    if settings.USE_SUBPLAT3:
1✔
260
        premium_plans = get_sp3_country_language_mapping("premium")
1✔
261
        phone_plans = get_sp3_country_language_mapping("phones")
1✔
262
        bundle_plans = get_sp3_country_language_mapping("bundle")
1✔
263
        megabundle_plans = get_sp3_country_language_mapping("megabundle")
1✔
264
    else:
265
        premium_plans = get_premium_country_language_mapping()
1✔
266
        phone_plans = get_phone_country_language_mapping()
1✔
267
        bundle_plans = get_bundle_country_language_mapping()
1✔
268
        megabundle_plans = get_megabundle_country_language_mapping()
1✔
269
    return Response(
1✔
270
        {
271
            "FXA_ORIGIN": settings.FXA_BASE_ORIGIN,
272
            "PERIODICAL_PREMIUM_PRODUCT_ID": settings.PERIODICAL_PREMIUM_PROD_ID,
273
            "GOOGLE_ANALYTICS_ID": settings.GOOGLE_ANALYTICS_ID,
274
            "GA4_MEASUREMENT_ID": settings.GA4_MEASUREMENT_ID,
275
            "BUNDLE_PRODUCT_ID": settings.BUNDLE_PROD_ID,
276
            "PHONE_PRODUCT_ID": settings.PHONE_PROD_ID,
277
            "MEGABUNDLE_PRODUCT_ID": settings.MEGABUNDLE_PROD_ID,
278
            "PERIODICAL_PREMIUM_PLANS": get_countries_info_from_request_and_mapping(
279
                request, premium_plans
280
            ),
281
            "PHONE_PLANS": get_countries_info_from_request_and_mapping(
282
                request, phone_plans
283
            ),
284
            "BUNDLE_PLANS": get_countries_info_from_request_and_mapping(
285
                request, bundle_plans
286
            ),
287
            "MEGABUNDLE_PLANS": get_countries_info_from_request_and_mapping(
288
                request, megabundle_plans
289
            ),
290
            "BASKET_ORIGIN": settings.BASKET_ORIGIN,
291
            "WAFFLE_FLAGS": flag_values,
292
            "WAFFLE_SWITCHES": switch_values,
293
            "WAFFLE_SAMPLES": sample_values,
294
            "MAX_MINUTES_TO_VERIFY_REAL_PHONE": (
295
                settings.MAX_MINUTES_TO_VERIFY_REAL_PHONE
296
            ),
297
        }
298
    )
299

300

301
@extend_schema(
1✔
302
    tags=["privaterelay"],
303
    parameters=[
304
        OpenApiParameter(
305
            name="Authorization",
306
            required=True,
307
            location="header",
308
            examples=[OpenApiExample("bearer", "Bearer XXXX-ZZZZ")],
309
            description="FXA Bearer Token. Can not be set in browsable API.",
310
        )
311
    ],
312
    request=OpenApiRequest(),
313
    responses={
314
        201: OpenApiResponse(description="Created; returned when user is created."),
315
        202: OpenApiResponse(
316
            description="Accepted; returned when user already exists."
317
        ),
318
        400: OpenApiResponse(
319
            description=(
320
                "Bad request; returned when request is missing Authorization: Bearer"
321
                " header or token value."
322
            )
323
        ),
324
        401: OpenApiResponse(
325
            description=(
326
                "Unauthorized; returned when the FXA token is invalid or expired."
327
            )
328
        ),
329
        404: OpenApiResponse(description="FXA did not return a user."),
330
        500: OpenApiResponse(description="No response from FXA server."),
331
    },
332
)
333
@api_view(["POST"])
1✔
334
@permission_classes([AllowAny])
1✔
335
@authentication_classes([])
1✔
336
def terms_accepted_user(request: Request) -> Response:
1✔
337
    """Pick 2024 or 2025 version based on settings"""
338
    if settings.FXA_TOKEN_AUTH_VERSION == FXA_TOKEN_AUTH_NEW_AND_BUSTED:
1✔
339
        # Use request._request to re-do authentication, permissions checks
340
        return terms_accepted_user_2025(request._request)
1✔
341
    else:
342
        return terms_accepted_user_2024(request)
1✔
343

344

345
def terms_accepted_user_2024(request: Request) -> Response:
1✔
346
    """
347
    Create a Relay user from an FXA token.
348

349
    See [API Auth doc][api-auth-doc] for details.
350

351
    [api-auth-doc]: https://github.com/mozilla/fx-private-relay/blob/main/docs/api_auth.md#firefox-oauth-token-authentication-and-accept-terms-of-service
352
    """  # noqa: E501
353
    # Setting authentication_classes to empty due to
354
    # authentication still happening despite permissions being set to allowany
355
    # https://forum.djangoproject.com/t/solved-allowany-override-does-not-work-on-apiview/9754
356
    # TODO: Implement an FXA token authentication class
357
    authorization = get_authorization_header(request).decode()
1✔
358
    if not authorization or not authorization.startswith("Bearer "):
1✔
359
        raise ParseError("Missing Bearer header.")
1✔
360

361
    token = authorization.split(" ")[1]
1✔
362
    if token == "":
1✔
363
        raise ParseError("Missing FXA Token after 'Bearer'.")
1✔
364

365
    try:
1✔
366
        fxa_uid = get_fxa_uid_from_oauth_token(token, use_cache=False)
1✔
367
    except AuthenticationFailed as e:
1✔
368
        # AuthenticationFailed exception returns 403 instead of 401 because we are not
369
        # using the proper config that comes with the authentication_classes. See:
370
        # https://www.django-rest-framework.org/api-guide/authentication/#custom-authentication
371
        if isinstance(e.detail, ErrorDetail):
1!
372
            return Response(data={"detail": e.detail.title()}, status=e.status_code)
1✔
373
        else:
374
            return Response(data={"detail": e.get_full_details()}, status=e.status_code)
×
375
    status_code = 201
1✔
376

377
    try:
1✔
378
        sa = SocialAccount.objects.get(uid=fxa_uid, provider="fxa")
1✔
379
        status_code = 202
1✔
380
    except SocialAccount.DoesNotExist:
1✔
381
        # User does not exist, create a new Relay user
382
        fxa_profile_resp = requests.get(
1✔
383
            FXA_PROFILE_URL,
384
            headers={"Authorization": f"Bearer {token}"},
385
            timeout=settings.FXA_REQUESTS_TIMEOUT_SECONDS,
386
        )
387
        if not (fxa_profile_resp.ok and fxa_profile_resp.content):
1✔
388
            logger.error(
1✔
389
                "terms_accepted_user: bad account profile response",
390
                extra={
391
                    "status_code": fxa_profile_resp.status_code,
392
                    "content": fxa_profile_resp.content,
393
                },
394
            )
395
            return Response(
1✔
396
                data={"detail": "Did not receive a 200 response for account profile."},
397
                status=500,
398
            )
399

400
        # This is not exactly the request object that FirefoxAccountsProvider expects,
401
        # but it has all of the necessary attributes to initialize the Provider
402
        provider = get_social_adapter().get_provider(request, "fxa")
1✔
403
        # This may not save the new user that was created
404
        # https://github.com/pennersr/django-allauth/blob/77368a84903d32283f07a260819893ec15df78fb/allauth/socialaccount/providers/base/provider.py#L44
405
        social_login = provider.sociallogin_from_response(
1✔
406
            request, fxa_profile_resp.json()
407
        )
408
        # Complete social login is called by callback, see
409
        # https://github.com/pennersr/django-allauth/blob/77368a84903d32283f07a260819893ec15df78fb/allauth/socialaccount/providers/oauth/views.py#L118
410
        # for what we are mimicking to create new SocialAccount, User, and Profile for
411
        # the new Relay user from Firefox Since this is a Resource Provider/Server flow
412
        # and are NOT a Relying Party (RP) of FXA No social token information is stored
413
        # (no Social Token object created).
414
        complete_social_login(request, social_login)
1✔
415

416
        # complete_social_login writes ['account_verified_email', 'user_created',
417
        # '_auth_user_id', '_auth_user_backend', '_auth_user_hash'] on
418
        # request.session which sets the cookie because complete_social_login does
419
        # the "login" The user did not actually log in, logout to clear the session
420
        if request.user.is_authenticated:
1!
421
            get_account_adapter(request).logout(request)
1✔
422

423
        sa = SocialAccount.objects.get(uid=fxa_uid, provider="fxa")
1✔
424

425
        # Indicate profile was created from the resource flow
426
        profile = sa.user.profile
1✔
427
        profile.created_by = "firefox_resource"
1✔
428
        profile.save()
1✔
429
    info_logger.info(
1✔
430
        "terms_accepted_user",
431
        extra={"social_account": sa.uid, "status_code": status_code},
432
    )
433
    return Response(status=status_code)
1✔
434

435

436
@api_view(["POST"])
1✔
437
@permission_classes([HasValidFxaToken, IsNewUser | IsActive])
1✔
438
@authentication_classes([FxaTokenAuthentication])
1✔
439
def terms_accepted_user_2025(request: Request) -> Response:
1✔
440
    """
441
    Create a Relay user from an FXA token.
442

443
    See API Auth doc for details:
444

445
    https://github.com/mozilla/fx-private-relay/blob/main/docs/api_auth.md#firefox-oauth-token-authentication-and-accept-terms-of-service
446
    """
447

448
    user = request.user
1✔
449
    introspect_response = request.auth
1✔
450
    if not isinstance(introspect_response, IntrospectionResponse):
1!
NEW
451
        raise ValueError(
×
452
            "Expected request.auth to be IntrospectionResponse,"
453
            f" got {type(introspect_response)}"
454
        )
455
    fxa_uid = introspect_response.fxa_id
1✔
456
    token = introspect_response.token
1✔
457

458
    existing_sa = False
1✔
459
    action: str | None = None
1✔
460
    profile_time_s: float | None = None
1✔
461
    if isinstance(user, User):
1✔
462
        socialaccount = SocialAccount.objects.get(user=user, provider="fxa")
1✔
463
        existing_sa = True
1✔
464
        action = "found_existing"
1✔
465

466
    if not existing_sa:
1✔
467
        # Get the user's profile
468
        fxa_prof_or_resp, profile_time_s = _get_fxa_profile_from_bearer_token(token)
1✔
469
        if isinstance(fxa_prof_or_resp, Response):
1✔
470
            return fxa_prof_or_resp
1✔
471
        fxa_profile = fxa_prof_or_resp
1✔
472

473
        # Since this takes time, see if another request created the SocialAccount
474
        try:
1✔
475
            socialaccount = SocialAccount.objects.get(uid=fxa_uid, provider="fxa")
1✔
476
            existing_sa = True
1✔
477
            action = "created_during_profile_fetch"
1✔
478
        except SocialAccount.DoesNotExist:
1✔
479
            pass
1✔
480

481
    if not existing_sa:
1✔
482
        # mypy type narrowing, but should always be true
483
        if not isinstance(fxa_profile, dict):  # pragma: no cover
484
            raise ValueError(f"fxa_profile is {type(fxa_profile)}, not dict")
485

486
        # Still no SocialAccount, attempt to create it
487
        socialaccount, response = _create_socialaccount_from_bearer_token(
1✔
488
            request, token, fxa_uid, fxa_profile
489
        )
490
        if response:
1!
NEW
491
            return response
×
492
        action = "created"
1✔
493

494
    status_code = 202 if existing_sa else 201
1✔
495
    info_logger.info(
1✔
496
        "terms_accepted_user",
497
        extra={
498
            "social_account": socialaccount.uid,
499
            "status_code": status_code,
500
            "action": action,
501
            "introspection_from_cache": introspect_response.from_cache,
502
            "introspection_time_s": introspect_response.request_s,
503
            "profile_time_s": profile_time_s,
504
        },
505
    )
506
    return Response(status=status_code)
1✔
507

508

509
def _create_socialaccount_from_bearer_token(
1✔
510
    request: Request, token: str, fxa_uid: str, fxa_profile: dict[str, Any]
511
) -> tuple[SocialAccount, None] | tuple[None, Response]:
512
    """Create a new Relay user with a SocialAccount from an FXA Bearer Token."""
513

514
    # request is not exactly the request object that FirefoxAccountsProvider expects,
515
    # but it has all of the necessary attributes to initialize the Provider
516
    provider = get_social_adapter().get_provider(request, "fxa")
1✔
517

518
    # sociallogin_from_response does not save the new user that was created
519
    # https://github.com/pennersr/django-allauth/blob/65.3.0/allauth/socialaccount/providers/base/provider.py#L84
520
    social_login = provider.sociallogin_from_response(request, fxa_profile)
1✔
521

522
    # Complete social login is called by callback, see
523
    # https://github.com/pennersr/django-allauth/blob/65.3.0/allauth/socialaccount/providers/oauth/views.py#L116
524
    # for what we are mimicking to create new SocialAccount, User, and Profile for
525
    # the new Relay user from Firefox Since this is a Resource Provider/Server flow
526
    # and are NOT a Relying Party (RP) of FXA No social token information is stored
527
    # (no Social Token object created).
528
    complete_social_login(request, social_login)
1✔
529

530
    # complete_social_login writes ['account_verified_email', 'user_created',
531
    # '_auth_user_id', '_auth_user_backend', '_auth_user_hash'] on
532
    # request.session which sets the cookie because complete_social_login does
533
    # the "login" The user did not actually log in, logout to clear the session
534
    get_account_adapter(request).logout(request)
1✔
535

536
    sa: SocialAccount = SocialAccount.objects.get(uid=fxa_uid, provider="fxa")
1✔
537

538
    # Indicate profile was created from the resource flow
539
    profile = sa.user.profile
1✔
540
    profile.created_by = "firefox_resource"
1✔
541
    profile.save()
1✔
542
    return sa, None
1✔
543

544

545
def _get_fxa_profile_from_bearer_token(
1✔
546
    token: str,
547
) -> tuple[dict[str, Any] | Response, float]:
548
    """Use a bearer token to get the Mozilla Account user's profile data"""
549

550
    error: None | Literal["timeout", "bad_response"] = None
1✔
551

552
    try:
1✔
553
        with Timer(logger=None) as profile_timer:
1✔
554
            fxa_profile_resp = requests.get(
1✔
555
                FXA_PROFILE_URL,
556
                headers={"Authorization": f"Bearer {token}"},
557
                timeout=settings.FXA_REQUESTS_TIMEOUT_SECONDS,
558
            )
559
    except requests.Timeout:
1✔
560
        error = "timeout"
1✔
561
    profile_time_s = round(profile_timer.last, 3)
1✔
562

563
    if error is None and not (fxa_profile_resp.ok and fxa_profile_resp.content):
1✔
564
        error = "bad_response"
1✔
565

566
    histogram_if_enabled(
1✔
567
        name="accounts_profile_ms",
568
        value=int(profile_time_s * 1000),
569
        tags=[generate_tag("result", error or "OK")],
570
    )
571

572
    if error == "timeout":
1✔
573
        logger.error(
1✔
574
            "terms_accepted_user: timeout",
575
            extra={
576
                "profile_time_s": profile_time_s,
577
            },
578
        )
579
        return (
1✔
580
            Response(
581
                "Account profile request timeout, try again later.",
582
                status=503,
583
            ),
584
            profile_time_s,
585
        )
586
    elif error == "bad_response":
1✔
587
        logger.error(
1✔
588
            "terms_accepted_user: bad account profile response",
589
            extra={
590
                "status_code": fxa_profile_resp.status_code,
591
                "content": fxa_profile_resp.content,
592
                "profile_time_s": profile_time_s,
593
            },
594
        )
595
        return (
1✔
596
            Response(
597
                data={"detail": "Did not receive a 200 response for account profile."},
598
                status=500,
599
            ),
600
            profile_time_s,
601
        )
602
    else:
603
        return fxa_profile_resp.json(), profile_time_s
1✔
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