Skip to content
This repository has been archived by the owner on Jan 25, 2018. It is now read-only.

Commit

Permalink
Upgrade to mainline PyJWT (part of bug 1145024)
Browse files Browse the repository at this point in the history
This switches us from PyJWT-mozilla to PyJWT which
is more maintained and has more features now.
This doesn't introduce any new functionality but
to use the new PyJWT we also needed a mozpay-py upgrade.
  • Loading branch information
kumar303 committed Mar 30, 2015
1 parent 3d024d9 commit 584eb75
Show file tree
Hide file tree
Showing 10 changed files with 72 additions and 64 deletions.
4 changes: 2 additions & 2 deletions requirements/prod.txt
Expand Up @@ -29,15 +29,15 @@ m2secret==0.1.1
mobile-codes==0.2.1
mock==1.0b1
mozilla-logger==0.1
mozpay==2.0.0
mozpay==2.1.0
newrelic==2.4.0.4
# Nose has to be here because django-nose is in installed apps.
nose==1.2.1
nosenicedots==0.5
nose-blockage==0.1.2
oauth2==1.5.211
oauthlib==0.6.2
PyJWT-mozilla==0.1.4.2
PyJWT==1.0.1
python-dateutil==2.1
python-memcached==1.48
pytz==2010e
Expand Down
6 changes: 3 additions & 3 deletions webpay/api/tests/test_api.py
Expand Up @@ -134,10 +134,10 @@ def test_configures_transaction_fail(self):
eq_(res.status_code, 400)

def test_unsupported_jwt_algorithm(self):
with self.settings(SUPPORTED_JWT_ALGORITHMS=['HS256']):
with self.settings(SUPPORTED_JWT_ALGORITHMS=['HS384']):
res = self.post(
request_kwargs={'jwt_kwargs': {'algorithm': 'none'}})
eq_(json.loads(res.content)['error_code'], 'INVALID_JWT_OBJ',
request_kwargs={'jwt_kwargs': {'algorithm': 'HS256'}})
eq_(json.loads(res.content)['error_code'], 'INVALID_JWT',
res.content)
eq_(res.status_code, 400)

Expand Down
13 changes: 1 addition & 12 deletions webpay/pay/forms.py
@@ -1,5 +1,3 @@
import json

from django import forms
from django.conf import settings

Expand Down Expand Up @@ -46,7 +44,7 @@ def clean_req(self):
log.debug('incoming JWT data: %r' % jwt_data)
try:
payload = jwt.decode(jwt_data, verify=False)
except jwt.DecodeError, exc:
except jwt.InvalidTokenError, exc:
log.debug('Error decoding JWT: {0}'.format(exc))
raise forms.ValidationError(msg.JWT_DECODE_ERR)
log.debug('Received JWT: %r' % payload)
Expand All @@ -58,15 +56,6 @@ def clean_req(self):
log.info('JWT was not a dict, it was %r' % type(payload))
raise forms.ValidationError(msg.INVALID_JWT_OBJ)

# The decode method already parses the header like this
# but does not give us access to it so we need to re-parse it.
header = json.loads(jwt.base64url_decode(jwt_data.split('.')[0]))
if header['alg'] not in settings.SUPPORTED_JWT_ALGORITHMS:
log.debug(
'JWT header alg is {alg} which is not supported. JWT: {jwt}'
.format(alg=header['alg'], jwt=jwt_data))
raise forms.ValidationError(msg.INVALID_JWT_OBJ)

try:
sim = payload['request']['simulate']
except KeyError:
Expand Down
2 changes: 2 additions & 0 deletions webpay/pay/tasks.py
Expand Up @@ -453,6 +453,8 @@ def _notify(notifier_task, trans, extra_response=None, simulated=NOT_SIMULATED,
'currency': trans['currency']}
issued_at = gmtime()
notice = {'iss': settings.NOTIFY_ISSUER,
# The original issuer of the request will now become the
# audience of the notice.
'aud': notes['issuer_key'],
'typ': typ,
'iat': issued_at,
Expand Down
1 change: 1 addition & 0 deletions webpay/pay/tests/__init__.py
Expand Up @@ -30,6 +30,7 @@ def payload(self, **kw):
def request(self, **kw):
# This simulates payment requests which do not have response.
kw.setdefault('include_response', False)
# By default, Marketplace will issue payment requests.
kw.setdefault('iss', settings.KEY)
kw.setdefault('app_secret', settings.SECRET)
return super(Base, self).request(**kw)
Expand Down
12 changes: 0 additions & 12 deletions webpay/pay/tests/test_forms.py
@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
import json

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist

Expand Down Expand Up @@ -124,16 +122,6 @@ def test_valid_inapp(self, get_active_product):
eq_(form.key, self.key)
eq_(form.secret, self.secret)

def test_double_encoded_jwt(self):
payload = self.payload()
# Some jwt libraries are doing this, I think.
payload = json.dumps(payload)
req = self.request(iss=self.key, app_secret=self.secret,
payload=payload)
with self.settings(INAPP_KEY_PATHS={None: sample}, DEBUG=True):
form = VerifyForm({'req': req})
assert not form.is_valid()

def test_valid_purchase(self):
payload = self.request(iss=settings.KEY, app_secret=settings.SECRET)
form = VerifyForm({'req': payload})
Expand Down
73 changes: 45 additions & 28 deletions webpay/pay/tests/test_tasks.py
Expand Up @@ -36,6 +36,15 @@ class NotifyTest(JWTtester, TestCase):
def setUp(self):
super(NotifyTest, self).setUp()
self.trans_uuid = 'some:uuid'
# This points to the app that issued the original payment request.
# It will become the audience when receiving a notice about the
# payment.
self.payment_issuer = 'some-apps-public-id'

def payload(self, **kw):
# Create a notice for the original issuer of the payment.
kw.setdefault('aud', self.payment_issuer)
return super(NotifyTest, self).payload(**kw)

def set_secret_mock(self, slumber, s):
slumber.generic.product.get_object_or_404.return_value = {'secret': s}
Expand All @@ -46,31 +55,31 @@ def url(self, path, protocol='https'):

class TestNotifyApp(NotifyTest):

@mock.patch('lib.solitude.api.client.get_transaction')
def do_chargeback(self, reason, get_transaction):
get_transaction.return_value = {
'amount': 1,
'currency': 'USD',
'status': constants.STATUS_COMPLETED,
'notes': {'pay_request': self.payload(),
'issuer_key': 'k'},
'type': constants.TYPE_REFUND,
def transaction(self, amount=1, currency='USD',
status=constants.STATUS_COMPLETED, payload=None,
trans_type=constants.TYPE_PAYMENT):
return {
'amount': amount,
'currency': currency,
'status': status,
'notes': {'pay_request': payload or self.payload(),
'issuer_key': self.payment_issuer},
'type': trans_type,
'uuid': self.trans_uuid
}

@mock.patch('lib.solitude.api.client.get_transaction')
def do_chargeback(self, reason, get_transaction):
get_transaction.return_value = self.transaction(
trans_type=constants.TYPE_REFUND)
with self.settings(INAPP_KEY_PATHS={None: sample}, DEBUG=True):
tasks.chargeback_notify(self.trans_uuid, reason=reason)

@mock.patch('lib.solitude.api.client.get_transaction')
def notify(self, get_transaction):
get_transaction.return_value = {
'amount': 1,
'currency': 'USD',
'status': constants.STATUS_COMPLETED,
'notes': {'pay_request': self.payload(),
'issuer_key': 'k'},
'type': constants.TYPE_PAYMENT,
'uuid': self.trans_uuid,
}
def notify(self, get_transaction, **trans_kw):
if 'paylod' not in trans_kw:
trans_kw['payload'] = self.payload()
get_transaction.return_value = self.transaction(**trans_kw)
with self.settings(INAPP_KEY_PATHS={None: sample}, DEBUG=True):
tasks.payment_notify('some:uuid')

Expand All @@ -87,15 +96,17 @@ def req_ok(req):
eq_(dd['typ'], payload['typ'])
eq_(dd['response']['price']['amount'], 1)
eq_(dd['response']['price']['currency'], u'USD')
jwt.decode(req['notice'], 'f', verify=True)
jwt.decode(req['notice'], 'f', verify=True,
audience=self.payment_issuer)
return True

(fake_req.expects('post').with_args(url, arg.passes_test(req_ok),
timeout=5)
.returns_fake()
.has_attr(text=self.trans_uuid)
.expects('raise_for_status'))
self.notify()

self.notify(payload=payload)

@fudge.patch('webpay.pay.utils.requests')
@mock.patch('lib.solitude.api.client.slumber')
Expand All @@ -110,14 +121,16 @@ def req_ok(req):
eq_(dd['typ'], payload['typ'])
eq_(dd['response']['transactionID'], self.trans_uuid)
eq_(dd['response']['reason'], 'refund')
jwt.decode(req['notice'], 'f', verify=True)
jwt.decode(req['notice'], 'f', verify=True,
audience=self.payment_issuer)
return True

(fake_req.expects('post').with_args(url, arg.passes_test(req_ok),
timeout=5)
.returns_fake()
.has_attr(text=self.trans_uuid)
.expects('raise_for_status'))

self.do_chargeback('refund')

@fudge.patch('webpay.pay.utils.requests')
Expand Down Expand Up @@ -234,7 +247,7 @@ def test_signed_app_response(self, fake_req, slumber):
# includes the same payment data that the app originally sent.
def is_valid(payload):
data = jwt.decode(payload['notice'], 'f', # secret key
verify=True)
verify=True, audience=self.payment_issuer)
eq_(data['iss'], settings.NOTIFY_ISSUER)
eq_(data['typ'], TYP_POSTBACK)
eq_(data['request']['pricePoint'], 1)
Expand All @@ -258,7 +271,8 @@ def is_valid(payload):
.returns_fake()
.has_attr(text='some:uuid')
.provides('raise_for_status'))
self.notify()

self.notify(payload=app_payment)


@mock.patch('lib.solitude.api.client.slumber')
Expand Down Expand Up @@ -292,7 +306,7 @@ class TestSimulatedNotifications(NotifyTest):
def notify(self, payload, get_price, prices=None):
get_price.return_value = (
prices or {'prices': [{'price': '0.99', 'currency': 'USD'}]})
tasks.simulate_notify('issuer-key', payload,
tasks.simulate_notify(self.payment_issuer, payload,
trans_uuid=self.trans_uuid)

@fudge.patch('webpay.pay.utils.requests')
Expand All @@ -306,7 +320,8 @@ def req_ok(req):
dd = jwt.decode(req['notice'], verify=False)
eq_(dd['request'], payload['request'])
eq_(dd['typ'], payload['typ'])
jwt.decode(req['notice'], 'f', verify=True)
jwt.decode(req['notice'], 'f', verify=True,
audience=self.payment_issuer)
return True

(fake_req.expects('post').with_args(url, arg.passes_test(req_ok),
Expand All @@ -328,14 +343,16 @@ def req_ok(req):
dd = jwt.decode(req['notice'], verify=False)
eq_(dd['request'], payload['request'])
eq_(dd['typ'], payload['typ'])
jwt.decode(req['notice'], 'f', verify=True)
jwt.decode(req['notice'], 'f', verify=True,
audience=self.payment_issuer)
return True

(fake_req.expects('post').with_args(url, arg.passes_test(req_ok),
timeout=arg.any())
.returns_fake()
.has_attr(text=self.trans_uuid)
.expects('raise_for_status'))

self.notify(payload)

@fudge.patch('webpay.pay.utils.requests')
Expand Down Expand Up @@ -374,7 +391,7 @@ def test_retry_http_error(self, post, retry, slumber):

assert post.called, 'notification was sent'
assert retry.called, 'task should be retried after error'
retry.assert_called_with(args=['issuer-key', payload],
retry.assert_called_with(args=[self.payment_issuer, payload],
max_retries=ANY, eta=ANY, exc=ANY)

@mock.patch('webpay.pay.utils.requests.post')
Expand Down
6 changes: 5 additions & 1 deletion webpay/pay/views.py
Expand Up @@ -60,15 +60,18 @@ def process_pay_req(request, data=None):
form.cleaned_data['req'],
settings.DOMAIN, # JWT audience.
form.secret,
algorithms=settings.SUPPORTED_JWT_ALGORITHMS,
required_keys=('request.id',
'request.pricePoint', # A price tier we'll lookup.
'request.pricePoint', # A price tier we'll look up.
'request.name',
'request.description',
'request.postbackURL',
'request.chargebackURL'))
except RequestExpired, exc:
log.debug('exception in mozpay.verify_jwt(): {e}'.format(e=exc))
er = msg.EXPIRED_JWT
except InvalidJWT, exc:
log.debug('exception in mozpay.verify_jwt(): {e}'.format(e=exc))
er = msg.INVALID_JWT

if exc:
Expand Down Expand Up @@ -106,6 +109,7 @@ def process_pay_req(request, data=None):
# Otherwise it is used for simulations and fake payments.
notes = request.session.get('notes', {})
notes['pay_request'] = pay_req
# The issuer key points to the app that issued the payment request.
notes['issuer_key'] = form.key
request.session['notes'] = notes
tx = trans_id()
Expand Down
11 changes: 8 additions & 3 deletions webpay/spa/tests/test_views.py
Expand Up @@ -71,16 +71,20 @@ def test_has_bango_logout_url(self):
@test.utils.override_settings(SPA_ENABLE=True,
SPA_ENABLE_URLS=True,
KEY='marketplace.mozilla.com',
DOMAIN='marketplace.mozilla.com',
SECRET='test secret')
class TestBuyerEmailAuth(Base):

@mock.patch('webpay.auth.utils.client')
def test_marketplace_purchase(self, solitude):
solitude.get_buyer.return_value = {'uuid': 'some-uuid'}
jwt = self.request(
iss='marketplace.mozilla.com', app_secret='test secret',
pay_request = self.request(
# Make a purchase issued by Marketplace for Marketplace.
iss=settings.KEY, aud=settings.KEY, app_secret='test secret',
extra_req={'productData':
'my_product_id=1234&buyer_email=user@example.com'})
res = self.client.get('/mozpay/', {'req': jwt})
res = self.client.get('/mozpay/', {'req': pay_request})
eq_(res.status_code, 200)
doc = pq(res.content)
eq_(doc('body').attr('data-logged-in-user'), 'user@example.com')
eq_(doc('body').attr('data-mkt-user'), 'true')
Expand Down Expand Up @@ -127,6 +131,7 @@ def test_non_marketplace(self):
extra_req={'productData':
'my_product_id=1234&buyer_email=user@example.com'})
res = self.client.get('/mozpay/', {'req': jwt})
eq_(res.status_code, 200)
doc = pq(res.content)
eq_(doc('body').attr('data-logged-in-user'), '')
eq_(doc('body').attr('data-mkt-user'), 'false')
8 changes: 5 additions & 3 deletions webpay/spa/views.py
Expand Up @@ -27,10 +27,12 @@ def index(request, view_name=None, start_view=None):
# for the purchaser named in it.
if jwt and _get_issuer(jwt) == settings.KEY:
try:
data = verify_sig(jwt, settings.SECRET)
data = verify_sig(jwt, settings.SECRET,
expected_aud=settings.DOMAIN)
data = data['request'].get('productData', '')
except InvalidJWT:
pass
except InvalidJWT, exc:
log.debug('ignoring invalid Marketplace JWT error: {e}'
.format(e=exc))
else:
product_data = urlparse.parse_qs(data)
emails = product_data.get('buyer_email')
Expand Down

0 comments on commit 584eb75

Please sign in to comment.