privaterelay/fxa_utils.py (128 lines of code) (raw):

import logging from datetime import UTC, datetime, timedelta from typing import Any, cast from django.conf import settings import sentry_sdk from allauth.socialaccount.models import SocialAccount, SocialToken from allauth.socialaccount.providers.fxa.views import FirefoxAccountsOAuth2Adapter from oauthlib.oauth2.rfc6749.errors import CustomOAuth2Error, TokenExpiredError from requests_oauthlib import OAuth2Session from .utils import flag_is_active_in_task logger = logging.getLogger("events") class NoSocialToken(Exception): """The SocialAccount has no SocialToken""" def __init__(self, uid: str): self.uid = uid super().__init__() def __str__(self) -> str: return f'NoSocialToken: The SocialAccount "{self.uid}" has no token.' def __repr__(self) -> str: return f'{self.__class__.__name__}("{self.uid}")' def update_social_token( existing_social_token: SocialToken, new_oauth2_token: dict[str, Any] ) -> None: existing_social_token.token = new_oauth2_token["access_token"] existing_social_token.token_secret = new_oauth2_token["refresh_token"] existing_social_token.expires_at = datetime.now(UTC) + timedelta( seconds=int(new_oauth2_token["expires_in"]) ) existing_social_token.save() # use "raw" requests_oauthlib to automatically refresh the access token # https://github.com/pennersr/django-allauth/issues/420#issuecomment-301805706 def _get_oauth2_session(social_account: SocialAccount) -> OAuth2Session: refresh_token_url = FirefoxAccountsOAuth2Adapter.access_token_url social_token = social_account.socialtoken_set.first() if social_token is None: raise NoSocialToken(uid=social_account.uid) def _token_updater(new_token): update_social_token(social_token, new_token) client_id = social_token.app.client_id client_secret = social_token.app.secret extra = { "client_id": client_id, "client_secret": client_secret, } expires_in = (social_token.expires_at - datetime.now(UTC)).total_seconds() token = { "access_token": social_token.token, "refresh_token": social_token.token_secret, "token_type": "Bearer", "expires_in": expires_in, } # TODO: find out why the auto_refresh and token_updater is not working # and instead we are manually refreshing the token at get_subscription_data_from_fxa client = OAuth2Session( client_id, scope=settings.SOCIALACCOUNT_PROVIDERS["fxa"]["SCOPE"], token=token, auto_refresh_url=refresh_token_url, auto_refresh_kwargs=extra, token_updater=_token_updater, ) return client def _refresh_token(client, social_account): social_token = SocialToken.objects.get(account=social_account) # refresh user token to expand the scope to get accounts subscription data new_token = client.refresh_token(FirefoxAccountsOAuth2Adapter.access_token_url) update_social_token(social_token, new_token) return {"social_token": new_token, "refreshed": True} def get_subscription_data_from_fxa(social_account: SocialAccount) -> dict[str, Any]: accounts_subscription_url = ( settings.FXA_ACCOUNTS_ENDPOINT + "/oauth/mozilla-subscriptions/customer/billing-and-subscriptions" ) try: client = _get_oauth2_session(social_account) except NoSocialToken as e: sentry_sdk.capture_exception(e) return {} try: # get detailed subscription data from FxA resp = client.get(accounts_subscription_url) json_resp = cast(dict[str, Any], resp.json()) if "Requested scopes are not allowed" in json_resp.get("message", ""): logger.error("accounts_subscription_scope_failed") json_resp = _refresh_token(client, social_account) except TokenExpiredError as e: sentry_sdk.capture_exception(e) json_resp = _refresh_token(client, social_account) except CustomOAuth2Error as e: sentry_sdk.capture_exception(e) json_resp = {} return json_resp def get_phone_subscription_dates(social_account): subscription_data = get_subscription_data_from_fxa(social_account) if "refreshed" in subscription_data.keys(): # user token refreshed for expanded scope social_account.refresh_from_db() # retry getting detailed subscription data subscription_data = get_subscription_data_from_fxa(social_account) if "refreshed" in subscription_data.keys(): return None, None, None if "subscriptions" not in subscription_data.keys(): # failed to get subscriptions data which may mean user never had subscription # and/or there is data mismatch with FxA if not flag_is_active_in_task("free_phones", social_account.user): # User who was flagged for having phone subscriptions # did not actually have phone subscriptions logger.error( "accounts_subscription_endpoint_failed", extra={"fxa_message": subscription_data.get("message", "")}, ) return None, None, None date_subscribed_phone = start_date = end_date = None product_w_phone_capabilites = [settings.PHONE_PROD_ID, settings.BUNDLE_PROD_ID] for sub in subscription_data.get("subscriptions", []): # Even if a user upgrade subscription e.g. from monthly to yearly # or from phone to VPN bundle use the last subscription subscription dates # Later, when the subscription details only show one valid subsription # this information can be updated subscription_created_timestamp = None subscription_start_timestamp = None subscription_end_timestamp = None if sub.get("product_id") in product_w_phone_capabilites: subscription_created_timestamp = sub.get("created") subscription_start_timestamp = sub.get("current_period_start") subscription_end_timestamp = sub.get("current_period_end") else: # not a product id for phone subscription, continue continue subscription_date_none = ( subscription_created_timestamp and subscription_start_timestamp and subscription_end_timestamp ) is None if subscription_date_none: # subscription dates are required fields according to FxA documentation: # https://mozilla.github.io/ecosystem-platform/api#tag/Subscriptions/operation/getOauthMozillasubscriptionsCustomerBillingandsubscriptions logger.error( "accounts_subscription_subscription_date_invalid", extra={"subscription": sub}, ) return None, None, None date_subscribed_phone = datetime.fromtimestamp( subscription_created_timestamp, tz=UTC ) start_date = datetime.fromtimestamp(subscription_start_timestamp, tz=UTC) end_date = datetime.fromtimestamp(subscription_end_timestamp, tz=UTC) return date_subscribed_phone, start_date, end_date