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

Commit

Permalink
Add an FXA refresh token endpoint #215
Browse files Browse the repository at this point in the history
- Add refresh method to client
- Add client tests
- Add refresh API endpoint
- Add refresh API tests
- Add refresh API docs
  • Loading branch information
jaredlockhart committed Jan 9, 2016
1 parent 6452295 commit aee0af7
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 1 deletion.
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')

0 comments on commit aee0af7

Please sign in to comment.