Skip to content
This repository has been archived by the owner on May 24, 2019. It is now read-only.

Add an FXA refresh token endpoint #215 #226

Merged
merged 1 commit into from
Jan 9, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,69 @@ Firefox Accounts Redirect
}
});

Firefox Accounts Refresh
----
Refresh a Firefox Accounts access token for a new token using a refresh token.

* **URL**

https://leaderboard.services.mozilla.com/api/v1/fxa/refresh/

* **Method:**

`POST`

* **URL Params**

None

* **Data Params**

* **refresh_token**

A long lived Firefox Accounts refresh token retrieved through an Oauth redirect process.

* **Request Headers**

* Authorization

A successful submission must include a valid Firefox Accounts authorization
bearer token

Example: `Authorization: Bearer kf94k5jsgsl3kj`

* **Success Response:**

* **Code:** 200

JSON encoding

{
access_token: <str>,
expires_in: <int>,
scope: <str>,
token_type: <str>
}

* **Error Responses:**

* **Code:** 400 INVALID
* **Content:** `{"detail":"JSON parse error - Expecting object: line 1 column 1 (char 0)"}`

* **Code:** 401 UNAUTHORIZED
* **Content:** `{"detail":"Unable to determine a valid Firefox Accounts authorization token"}`

* **Sample Call:**

$.ajax({
url: "https://leaderboard.services.mozilla.com/api/v1/fxa/refresh/?refresh_token=asdf",
dataType: "json",
type : "GET",
success : function(r, data) {
console.log(data);
}
});

Get Countries
----
Get a list of all countries that have been contributed to, the total number of contributions,
Expand Down
28 changes: 28 additions & 0 deletions leaderboard/fxa/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,15 @@ def get_authorization_token(self, code):

{
'access_token': 'asdf',
'refresh_token': 'asdf',
'auth_at': 1438019181,
'expires_in': 172800,
'scope': 'profile',
'token_type': 'bearer'
}
"""
params = {
'grant_type': 'authorization_code',
'client_id': settings.FXA_CLIENT_ID,
'client_secret': settings.FXA_SECRET,
'code': code,
Expand All @@ -96,6 +98,32 @@ def get_authorization_token(self, code):

return self._parse_response(response)

def refresh_authorization_token(self, refresh_token):
"""
Exchange a refresh token for a new access token.

Example response:

{
'access_token': 'asdf',
'expires_in': 1209600,
'scope': 'profile',
'token_type': 'bearer'}
}
"""
params = {
'grant_type': 'refresh_token',
'client_id': settings.FXA_CLIENT_ID,
'client_secret': settings.FXA_SECRET,
'scope': settings.FXA_SCOPE,
'refresh_token': refresh_token,
}

token_url = urlparse.urljoin(settings.FXA_OAUTH_URI, 'v1/token')
response = requests.post(token_url, data=json.dumps(params))

return self._parse_response(response)

def get_profile_data(self, access_token):
"""
Retrieve the profile details for a user given a valid access_token.
Expand Down
17 changes: 17 additions & 0 deletions leaderboard/fxa/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ def test_get_authorization_token_returns_token(self):

self.assertEqual(response_data, authorization_data)

def test_get_refresh_token_returns_token(self):
authorization_data = {
'access_token': 'abcdef',
'expires_in': 123,
'scope': 'profile',
'token_type': 'bearer'
}

response = mock.MagicMock()
response.content = json.dumps(authorization_data)
response.status_code = 200
self.mock_post.return_value = response

response_data = self.fxa_client.refresh_authorization_token('asdf')

self.assertEqual(response_data, authorization_data)

def test_get_profile_data_returns_profile(self):
profile_data = {
'email': 'email@-example.com',
Expand Down
63 changes: 63 additions & 0 deletions leaderboard/fxa/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.core.urlresolvers import reverse
from django.test import TestCase

from leaderboard.contributors.tests.test_models import ContributorFactory
from leaderboard.contributors.models import Contributor
from leaderboard.fxa.client import get_fxa_login_url
from leaderboard.fxa.tests.test_client import MockRequestTestMixin
Expand Down Expand Up @@ -182,3 +183,65 @@ def test_multiple_contributors_signin_creates_multiple_contributors(self):

self.assertEqual(contributor.fxa_uid, fxa_profile_data2['uid'])
self.assertTrue(contributor.uid is not None)


class TestFXARefreshView(MockRequestTestMixin, TestCase):

def setUp(self):
super(TestFXARefreshView, self).setUp()
fxa_profile_data = self.setup_profile_call()
self.contributor = ContributorFactory(fxa_uid=fxa_profile_data['uid'])

def test_successful_refresh_returns_new_token(self):
fxa_auth_data = self.setup_auth_call()

response = self.client.post(
reverse('fxa-refresh'),
data={'refresh_token': 'asdf'},
HTTP_AUTHORIZATION='Bearer asdf',
)

self.assertEqual(response.status_code, 200)

response_data = json.loads(response.content)

self.assertEqual(response_data, fxa_auth_data)

def test_missing_access_token_raises_403(self):
response = self.client.post(
reverse('fxa-refresh'),
data={'refresh_token': 'asdf'},
)

self.assertEqual(response.status_code, 401)

def test_invalid_access_token_raises_403(self):
self.set_mock_response(self.mock_get, status_code=400)

response = self.client.post(
reverse('fxa-refresh'),
data={'refresh_token': 'asdf'},
HTTP_AUTHORIZATION='Bearer asdf',
)

self.assertEqual(response.status_code, 401)

def test_missing_refresh_token_raises_400(self):
response = self.client.post(
reverse('fxa-refresh'),
data={},
HTTP_AUTHORIZATION='Bearer asdf',
)

self.assertEqual(response.status_code, 400)

def test_fxa_auth_error_raises_400(self):
self.set_mock_response(self.mock_post, status_code=400)

response = self.client.post(
reverse('fxa-refresh'),
data={'refresh_token': 'asdf'},
HTTP_AUTHORIZATION='Bearer asdf',
)

self.assertEqual(response.status_code, 400)
9 changes: 8 additions & 1 deletion leaderboard/fxa/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from django.conf.urls import url

from leaderboard.fxa.views import FXALoginView, FXAConfigView, FXARedirectView
from leaderboard.fxa.views import (
FXALoginView,
FXAConfigView,
FXARedirectView,
FXARefreshView,
)

urlpatterns = [
url('^login/', FXALoginView.as_view(),
Expand All @@ -9,4 +14,6 @@
name='fxa-config'),
url('^redirect/', FXARedirectView.as_view(),
name='fxa-redirect'),
url('^refresh/', FXARefreshView.as_view(),
name='fxa-refresh'),
]
20 changes: 20 additions & 0 deletions leaderboard/fxa/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from rest_framework.response import Response

from leaderboard.contributors.models import Contributor
from leaderboard.fxa.authenticator import OAuthTokenAuthentication
from leaderboard.fxa.client import (
get_fxa_login_url,
FXAClientMixin,
Expand Down Expand Up @@ -86,3 +87,22 @@ def get(self, request):
},
content_type='application/json',
)


class FXARefreshView(FXAClientMixin, APIView):
authentication_classes = (OAuthTokenAuthentication,)

def post(self, request):
refresh_token = request.POST.get('refresh_token', None)

if refresh_token is None:
raise ValidationError('Unable to determine refresh token.')

try:
fxa_auth_data = self.fxa_client.refresh_authorization_token(
refresh_token)
except FXAException:
raise ValidationError(
'Unable to communicate with Firefox Accounts.')

return Response(fxa_auth_data, content_type='application/json')