Internal login start endpoint (fixes #2424)

This commit is contained in:
Mark Striemer 2016-04-19 17:24:45 -05:00
Родитель 6de6a4594c
Коммит 0cffe96db7
6 изменённых файлов: 150 добавлений и 1 удалений

Просмотреть файл

@ -26,6 +26,13 @@ responses of the following endpoints:
* ``/api/v3/accounts/register/``
* ``/api/v3/accounts/authenticate/``
A JWT may also be obtained through the JSON API as outlined in the
:ref:`internal login JSON API <internal-login-json-api>` section. This is only
accessible through the VPN and requires using the following endpoints:
* ``/api/v3/internal/accounts/login/start/``
* ``/api/v3/internal/accounts/login/``
The token is available in two forms:
* For the endpoints returning JSON, as a property called ``token``.

Просмотреть файл

@ -30,3 +30,21 @@ add-ons, and can return disabled, unreviewer, unlisted or even deleted add-ons.
:>json string next: The URL of the next page of results.
:>json string previous: The URL of the previous page of results.
:>json array results: An array of :ref:`add-ons <addon-detail-object>`.
-----------------------
Internal Login JSON API
-----------------------
.. _internal-login-json-api:
The JSON API login flow is initiated by accessing the start endpoint which
will add an ``fxa_state`` to the user's session and redirect them to Firefox
Accounts. When the user finishes authenticating with Firefox Accounts they
will be redirected to the client application which can make a request to the
login endpoint to exchange the Firefox Accounts token and state for a JWT.
.. http:get:: /api/v3/internal/accounts/login/start/
:param string to: A path to append to the state. The state will be returned
from FxA as ``state:path``, the path will be URL safe base64 encoded.
:status 302: Redirect user to Firefox Accounts.

Просмотреть файл

@ -98,6 +98,16 @@ FXA_CONFIG = {
'redirect_url': 'http://olympia.dev/api/v3/accounts/authorize/',
'scope': 'profile',
}
ADMIN_FXA_CONFIG = {
'client_id': '0f95f6474c24c1dc',
'client_secret':
'ca45e503a1b4ec9e2a3d4855d79849e098da18b7dfe42b6bc76dfed420fc1d38',
'content_host': 'https://stable.dev.lcip.org',
'oauth_host': 'https://oauth-stable.dev.lcip.org/v1',
'profile_host': 'https://stable.dev.lcip.org/profile/v1',
'redirect_uri': 'http://localhost:3000/fxa-authenticate',
'scope': 'profile',
}
# CSP report endpoint which returns a 204 from addons-nginx in local dev.
CSP_REPORT_URI = '/csp-report'

Просмотреть файл

@ -1,11 +1,23 @@
# -*- coding: utf-8 -*-
import base64
import json
import urlparse
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from olympia.amo.tests import addon_factory, ESTestCase
import mock
from olympia.amo.tests import addon_factory, ESTestCase, TestCase
from olympia.users.models import UserProfile
FXA_CONFIG = {
'oauth_host': 'https://accounts.firefox.com/v1',
'client_id': '999abc111',
'redirect_uri': 'https://addons-frontend/fxa-authenticate',
'scope': 'profile',
}
class TestInternalAddonSearchView(ESTestCase):
fixtures = ['base/users']
@ -151,3 +163,73 @@ class TestInternalAddonSearchView(ESTestCase):
result = data['results'][0]
assert result['id'] == addon2.pk
assert result['name'] == {'en-US': u'By second Addôn'}
@override_settings(ADMIN_FXA_CONFIG=FXA_CONFIG)
class TestLoginStartView(TestCase):
def setUp(self):
super(TestLoginStartView, self).setUp()
self.url = reverse('internal-login-start')
def test_state_is_set(self):
self.initialize_session({})
assert 'fxa_state' not in self.client.session
state = 'somerandomstate'
with mock.patch('olympia.internal_tools.views.generate_fxa_state',
lambda: state):
response = self.client.get(self.url)
assert 'fxa_state' in self.client.session
assert self.client.session['fxa_state'] == state
def test_redirect_uri_is_correct(self):
self.initialize_session({})
with mock.patch('olympia.internal_tools.views.generate_fxa_state',
lambda: 'arandomstring'):
response = self.client.get(self.url)
assert response.status_code == 302
url = urlparse.urlparse(response['location'])
assert '{scheme}://{netloc}{path}'.format(
scheme=url.scheme,
netloc=url.netloc,
path=url.path,
) == 'https://accounts.firefox.com/v1/authorization'
assert urlparse.parse_qs(url.query) == {
'client_id': ['999abc111'],
'redirect_uri': ['https://addons-frontend/fxa-authenticate'],
'scope': ['profile'],
'state': ['arandomstring'],
}
def test_state_is_not_overriden(self):
self.initialize_session({'fxa_state': 'thisisthestate'})
response = self.client.get(self.url)
assert self.client.session['fxa_state'] == 'thisisthestate'
def test_to_is_included_in_redirect_state(self):
path = '/addons/unlisted-addon/'
# The =s will be stripped from the URL.
assert '=' in base64.urlsafe_b64encode(path)
state = 'somenewstatestring'
self.initialize_session({})
with mock.patch('olympia.internal_tools.views.generate_fxa_state',
lambda: state):
response = self.client.get(
'{url}?to={path}'.format(path=path, url=self.url))
assert self.client.session['fxa_state'] == state
url = urlparse.urlparse(response['location'])
query = urlparse.parse_qs(url.query)
state_parts = query['state'][0].split(':')
assert len(state_parts) == 2
assert state_parts[0] == state
assert '=' not in state_parts[1]
assert base64.urlsafe_b64decode(state_parts[1] + '====') == path
def test_to_is_excluded_when_unsafe(self):
path = 'https://www.google.com'
self.initialize_session({})
response = self.client.get(
'{url}?to={path}'.format(path=path, url=self.url))
url = urlparse.urlparse(response['location'])
query = urlparse.parse_qs(url.query)
assert ':' not in query['state'][0]

Просмотреть файл

@ -9,4 +9,7 @@ urlpatterns = patterns(
url(r'^addons/search/$',
views.InternalAddonSearchView.as_view(),
name='internal-addon-search'),
url(r'^accounts/login/start/$',
views.LoginStart.as_view(),
name='internal-login-start'),
)

Просмотреть файл

@ -1,3 +1,13 @@
from base64 import urlsafe_b64encode
from urllib import urlencode
from django.conf import settings
from django.http import HttpResponseRedirect
from django.utils.http import is_safe_url
from rest_framework.views import APIView
from olympia.accounts.helpers import generate_fxa_state
from olympia.addons.views import AddonSearchView
from olympia.api.authentication import JSONWebTokenAuthentication
from olympia.api.permissions import AnyOf, GroupPermission
@ -14,3 +24,22 @@ class InternalAddonSearchView(AddonSearchView):
# Restricted to specific permissions.
permission_classes = [AnyOf(GroupPermission('AdminTools', 'View'),
GroupPermission('ReviewerAdminTools', 'View'))]
class LoginStart(APIView):
def get(self, request):
request.session.setdefault('fxa_state', generate_fxa_state())
state = request.session['fxa_state']
next_path = request.GET.get('to')
if next_path and is_safe_url(next_path):
state += ':' + urlsafe_b64encode(next_path).rstrip('=')
query = {
'client_id': settings.ADMIN_FXA_CONFIG['client_id'],
'redirect_uri': settings.ADMIN_FXA_CONFIG['redirect_uri'],
'scope': settings.ADMIN_FXA_CONFIG['scope'],
'state': state,
}
return HttpResponseRedirect('{host}/authorization?{query}'.format(
host=settings.ADMIN_FXA_CONFIG['oauth_host'],
query=urlencode(query)))