diff --git a/settings.py b/settings.py index ebed365970..0450595166 100644 --- a/settings.py +++ b/settings.py @@ -127,6 +127,11 @@ FXA_OAUTH_HOST = 'https://oauth-stable.dev.lcip.org/v1' FXA_PROFILE_HOST = 'https://stable.dev.lcip.org/profile/v1' ALLOWED_FXA_CONFIGS = ['default', 'amo', 'local'] +# When USE_FAKE_FXA_AUTH and settings.DEBUG are both True, we serve a fake +# authentication page, bypassing FxA. To disable this behavior, set +# USE_FAKE_FXA = False in your local settings. +USE_FAKE_FXA_AUTH = True + # CSP report endpoint which returns a 204 from addons-nginx in local dev. CSP_REPORT_URI = '/csp-report' RESTRICTED_DOWNLOAD_CSP['REPORT_URI'] = CSP_REPORT_URI diff --git a/src/olympia/accounts/tests/test_utils.py b/src/olympia/accounts/tests/test_utils.py index 44d2ac40dd..c0b3ba722f 100644 --- a/src/olympia/accounts/tests/test_utils.py +++ b/src/olympia/accounts/tests/test_utils.py @@ -17,6 +17,7 @@ from waffle.testutils import override_switch from olympia.accounts import utils from olympia.accounts.utils import process_fxa_event from olympia.amo.tests import TestCase, user_factory +from olympia.amo.urlresolvers import reverse from olympia.users.models import UserProfile @@ -208,6 +209,29 @@ def test_redirect_for_login(default_fxa_login_url): assert response['location'] == login_url +@override_settings(DEBUG=True, USE_FAKE_FXA_AUTH=True) +def test_fxa_login_url_when_faking_fxa_auth(): + path = '/en-US/addons/abp/?source=ddg' + request = RequestFactory().get(path) + request.session = {'fxa_state': 'myfxastate'} + raw_url = utils.fxa_login_url( + config=FXA_CONFIG['default'], state=request.session['fxa_state'], + next_path=path, action='signin') + url = urlparse(raw_url) + assert url.scheme == '' + assert url.netloc == '' + assert url.path == reverse('fake-fxa-authorization') + query = parse_qs(url.query) + next_path = urlsafe_b64encode(path.encode('utf-8')).rstrip(b'=') + assert query == { + 'action': ['signin'], + 'client_id': ['foo'], + 'scope': ['profile openid'], + 'state': ['myfxastate:{next_path}'.format( + next_path=force_text(next_path))], + } + + class TestProcessSqsQueue(TestCase): @mock.patch('boto3._get_default_session') diff --git a/src/olympia/accounts/tests/test_views.py b/src/olympia/accounts/tests/test_views.py index 9faaefb7b9..a8f6f71fb5 100644 --- a/src/olympia/accounts/tests/test_views.py +++ b/src/olympia/accounts/tests/test_views.py @@ -159,6 +159,15 @@ class TestLoginStartView(TestCase): assert views.LoginStartView.ALLOWED_FXA_CONFIGS == ( ['default', 'amo', 'local']) + @override_settings(DEBUG=True, USE_FAKE_FXA_AUTH=True) + def test_redirect_url_fake_fxa_auth(self): + response = self.client.get(reverse_ns('accounts.login_start')) + assert response.status_code == 302 + url = urlparse(response['location']) + assert url.path == reverse('fake-fxa-authorization') + query = parse_qs(url.query) + assert query['state'] + class TestLoginUserAndRegisterUser(TestCase): @@ -723,6 +732,25 @@ class TestWithUser(TestCase): addon_factory(users=[self.user]) self._test_should_continue_without_redirect_for_two_factor_auth() + @override_settings(DEBUG=True, USE_FAKE_FXA_AUTH=True) + def test_fake_fxa_auth(self): + self.user = user_factory() + self.find_user.return_value = self.user + self.request.data = { + 'code': 'foo', + 'fake_fxa_email': self.user.email, + 'state': 'some-blob:{next_path}'.format( + next_path=force_text(base64.urlsafe_b64encode(b'/a/path/?'))), + } + args, kwargs = self.fn(self.request) + assert args == (self, self.request) + assert kwargs['user'] == self.user + assert kwargs['identity']['email'] == self.user.email + assert kwargs['identity']['uid'].startswith('fake_fxa_id-') + assert len(kwargs['identity']['uid']) == 44 # 32 random chars + prefix + assert kwargs['next_path'] == '/a/path/?' + assert self.fxa_identify.call_count == 0 + @override_settings(FXA_CONFIG={ 'foo': {'FOO': 123}, diff --git a/src/olympia/accounts/utils.py b/src/olympia/accounts/utils.py index f8dba99ba6..d9d9007af4 100644 --- a/src/olympia/accounts/utils.py +++ b/src/olympia/accounts/utils.py @@ -14,6 +14,8 @@ import boto3 from olympia.accounts.tasks import ( delete_user_event, primary_email_change_event) +from olympia.amo.urlresolvers import reverse +from olympia.amo.utils import use_fake_fxa from olympia.core.logger import getLogger @@ -71,8 +73,12 @@ def fxa_login_url(config, state, next_path=None, action=None, if id_token: query['prompt'] = 'none' query['id_token_hint'] = id_token - return '{host}/authorization?{query}'.format( - host=settings.FXA_OAUTH_HOST, query=urlencode(query)) + if use_fake_fxa(): + base_url = reverse('fake-fxa-authorization') + else: + base_url = '{host}/authorization'.format(host=settings.FXA_OAUTH_HOST) + return '{base_url}?{query}'.format( + base_url=base_url, query=urlencode(query)) def default_fxa_register_url(request): diff --git a/src/olympia/accounts/views.py b/src/olympia/accounts/views.py index 45fb1c15ca..bd028609a4 100644 --- a/src/olympia/accounts/views.py +++ b/src/olympia/accounts/views.py @@ -46,7 +46,7 @@ from olympia.access import acl from olympia.access.models import GroupUser from olympia.amo import messages from olympia.amo.decorators import use_primary_db -from olympia.amo.utils import fetch_subscribed_newsletters +from olympia.amo.utils import fetch_subscribed_newsletters, use_fake_fxa from olympia.api.authentication import ( JWTKeyAuthentication, UnsubscribeTokenAuthentication, WebTokenAuthentication) @@ -261,8 +261,19 @@ def with_user(format): response, request.user) return response try: - identity, id_token = verify.fxa_identify( - data['code'], config=fxa_config) + if use_fake_fxa() and 'fake_fxa_email' in data: + # Bypassing real authentication, we take the email provided + # and generate a random fxa id. + identity = { + 'email': data['fake_fxa_email'], + 'uid': 'fake_fxa_id-%s' % force_text( + binascii.b2a_hex(os.urandom(16)) + ) + } + id_token = identity['email'] + else: + identity, id_token = verify.fxa_identify( + data['code'], config=fxa_config) except verify.IdentificationError: log.info('Profile not found. Code: {}'.format(data['code'])) return render_error( diff --git a/src/olympia/amo/templates/amo/fake_fxa_authorization.html b/src/olympia/amo/templates/amo/fake_fxa_authorization.html new file mode 100644 index 0000000000..a9a0990788 --- /dev/null +++ b/src/olympia/amo/templates/amo/fake_fxa_authorization.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +

{{ _('Register or Log in') }}

+
+ {% for key, value in request.GET.items() %} + + {% endfor %} + + + +
+{% endblock %} diff --git a/src/olympia/amo/tests/test_views.py b/src/olympia/amo/tests/test_views.py index b35f68d0b8..ca533d0837 100644 --- a/src/olympia/amo/tests/test_views.py +++ b/src/olympia/amo/tests/test_views.py @@ -410,6 +410,37 @@ class TestRobots(TestCase): assert 'Allow: {}'.format(url) in response.content.decode('utf-8') +def test_fake_fxa_authorization_correct_values_passed(): + with override_settings(DEBUG=True): # USE_FAKE_FXA_AUTH is already True + url = reverse('fake-fxa-authorization') + response = test.Client().get(url, {'state': 'foobar'}) + assert response.status_code == 200 + doc = pq(response.content) + form = doc('#fake_fxa_authorization')[0] + assert form.attrib['action'] == reverse('auth:accounts.authenticate') + elm = doc('#fake_fxa_authorization input[name=code]')[0] + assert elm.attrib['value'] == 'fakecode' + elm = doc('#fake_fxa_authorization input[name=state]')[0] + assert elm.attrib['value'] == 'foobar' + elm = doc('#fake_fxa_authorization input[name=fake_fxa_email]') + assert elm # No value yet, should just be present. + + +def test_fake_fxa_authorization_deactivated(): + url = reverse('fake-fxa-authorization') + with override_settings(DEBUG=False, USE_FAKE_FXA_AUTH=False): + response = test.Client().get(url) + assert response.status_code == 404 + + with override_settings(DEBUG=False, USE_FAKE_FXA_AUTH=True): + response = test.Client().get(url) + assert response.status_code == 404 + + with override_settings(DEBUG=True, USE_FAKE_FXA_AUTH=False): + response = test.Client().get(url) + assert response.status_code == 404 + + class TestAtomicRequests(WithDynamicEndpointsAndTransactions): def setUp(self): diff --git a/src/olympia/amo/urls.py b/src/olympia/amo/urls.py index 1a50441950..98c986a8d9 100644 --- a/src/olympia/amo/urls.py +++ b/src/olympia/amo/urls.py @@ -28,5 +28,6 @@ urlpatterns = [ re_path(r'^opensearch\.xml$', render_xml, {'template': 'amo/opensearch.xml'}, name='amo.opensearch'), - + re_path(r'^fake-fxa-authorization/$', views.fake_fxa_authorization, + name='fake-fxa-authorization') ] diff --git a/src/olympia/amo/utils.py b/src/olympia/amo/utils.py index ed0e41c366..919fdd2b59 100644 --- a/src/olympia/amo/utils.py +++ b/src/olympia/amo/utils.py @@ -1032,6 +1032,12 @@ def extract_colors_from_image(path): return colors +def use_fake_fxa(): + """Return whether or not to use a fake FxA server for authentication. + Should always return False in production""" + return settings.DEBUG and settings.USE_FAKE_FXA_AUTH + + class AMOJSONEncoder(JSONEncoder): def default(self, obj): if isinstance(obj, Translation): diff --git a/src/olympia/amo/views.py b/src/olympia/amo/views.py index 681530a633..6e2727832d 100644 --- a/src/olympia/amo/views.py +++ b/src/olympia/amo/views.py @@ -7,7 +7,7 @@ from django import http from django.conf import settings from django.core.exceptions import ViewDoesNotExist from django.db.transaction import non_atomic_requests -from django.http import HttpResponse, JsonResponse +from django.http import Http404, HttpResponse, JsonResponse from django.views.decorators.cache import never_cache from django_statsd.clients import statsd @@ -16,7 +16,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from olympia import amo -from olympia.amo.utils import render +from olympia.amo.utils import render, use_fake_fxa from olympia.api.exceptions import base_500_data from olympia.api.serializers import SiteStatusSerializer @@ -152,6 +152,13 @@ def frontend_view(*args, **kwargs): frontend_view.is_frontend_view = True +def fake_fxa_authorization(request): + """Fake authentication page to bypass FxA in local development envs.""" + if not use_fake_fxa(): + raise Http404() + return render(request, 'amo/fake_fxa_authorization.html') + + class SiteStatusView(APIView): authentication_classes = [] permission_classes = [] diff --git a/src/olympia/landfill/serializers.py b/src/olympia/landfill/serializers.py index edeee21f28..689f1d0cb3 100644 --- a/src/olympia/landfill/serializers.py +++ b/src/olympia/landfill/serializers.py @@ -43,11 +43,17 @@ class GenerateAddonsSerializer(serializers.Serializer): count = serializers.IntegerField(default=10) def __init__(self): - self.fxa_email = os.environ.get( - 'UITEST_FXA_EMAIL', 'uitest-%s@restmail.net' % uuid.uuid4()) - self.fxa_password = os.environ.get( - 'UITEST_FXA_PASSWORD', 'uitester') - self.fxa_id = self._create_fxa_user() + self.fxa_email = os.environ.get('UITEST_FXA_EMAIL') + self.fxa_password = os.environ.get('UITEST_FXA_PASSWORD', 'uitester') + if self.fxa_email: + self.fxa_id = self._create_fxa_user() + else: + # If UITEST_FXA_EMAIL was empty/absent, skip creating the fxa + # account: we won't need to log in with that user through FxA. + log.info('UITEST_FXA_EMAIL is empty, skipping FxA user creation') + # We still need those to be set for the content to be created. + self.fxa_id = None + self.fxa_email = 'uitest-%s@restmail.net' % uuid.uuid4() self.user = self._create_addon_user() def _create_fxa_user(self): diff --git a/src/olympia/lib/settings_base.py b/src/olympia/lib/settings_base.py index c34679845e..d1f398b5f2 100644 --- a/src/olympia/lib/settings_base.py +++ b/src/olympia/lib/settings_base.py @@ -1836,6 +1836,7 @@ FXA_OAUTH_HOST = 'https://oauth.accounts.firefox.com/v1' FXA_PROFILE_HOST = 'https://profile.accounts.firefox.com/v1' DEFAULT_FXA_CONFIG_NAME = 'default' ALLOWED_FXA_CONFIGS = ['default'] +USE_FAKE_FXA_AUTH = False # Should only be True for local development envs. # List all jobs that should be callable with cron here. # syntax is: job_and_method_name: full.package.path