Internal login start endpoint (fixes #2424)
This commit is contained in:
Родитель
6de6a4594c
Коммит
0cffe96db7
|
@ -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.
|
||||
|
|
10
settings.py
10
settings.py
|
@ -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)))
|
||||
|
|
Загрузка…
Ссылка в новой задаче