Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 41f892da87
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 200 lines (163 sloc) 7.402 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
import calendar
from datetime import datetime
import json
import sys
import time

from django import forms
from django.conf import settings

import jwt
from django_statsd.clients import statsd

import amo
from market.models import Price

from .forms import PaymentForm, ContributionForm
from .models import InappConfig


class InappPaymentError(Exception):
    """An error occurred while processing an in-app payment."""

    def __init__(self, msg, app_id=None):
        self.app_id = app_id
        if self.app_id:
            msg = '%s (app ID=%r)' % (msg, self.app_id)
        super(Exception, self).__init__(msg)


class UnknownAppError(InappPaymentError):
    """The application ID is not known."""


class RequestVerificationError(InappPaymentError):
    """The payment request could not be verified."""


class RequestExpired(InappPaymentError):
    """The payment request expired."""


class AppPaymentsDisabled(InappPaymentError):
    """In-app payment functionality for this app has been disabled."""


class AppPaymentsRevoked(InappPaymentError):
    """In-app payment functionality for this app has been revoked."""


class InvalidRequest(InappPaymentError):
    """The payment request has malformed or missing information."""


def _re_raise_as(NewExc, *args, **kw):
    """Raise a new exception using the preserved traceback of the last one."""
    etype, val, tb = sys.exc_info()
    raise NewExc(*args, **kw), None, tb


def verify_request(signed_request):
    """
Verifies a signed in-app payment request.

Returns the trusted JSON data from the original request.
JWT spec: http://openid.net/specs/draft-jones-json-web-token-07.html

One extra key, _config, is added to the returned JSON.
This is the InappConfig instance.

When there's an error, an exception derived from InappPaymentError
will be raised.
"""
    try:
        signed_request = str(signed_request) # must be base64 encoded bytes
    except UnicodeEncodeError, exc:
        _re_raise_as(RequestVerificationError,
                     'Non-ascii payment JWT: %s' % exc)
    try:
        app_req = jwt.decode(signed_request, verify=False)
    except jwt.DecodeError, exc:
        _re_raise_as(RequestVerificationError, 'Invalid payment JWT: %s' % exc)
    try:
        app_req = json.loads(app_req)
    except ValueError, exc:
        _re_raise_as(RequestVerificationError,
                     'Invalid JSON for payment JWT: %s' % exc)

    app_id = app_req.get('iss')

    # Verify the signature:
    try:
        cfg = InappConfig.objects.get(public_key=app_id,
                                      addon__status=amo.STATUS_PUBLIC)
    except InappConfig.DoesNotExist:
        _re_raise_as(UnknownAppError, 'App does not exist or is not public',
                     app_id=app_id)
    if cfg.status == amo.INAPP_STATUS_REVOKED:
        raise AppPaymentsRevoked('Payments revoked', app_id=app_id)
    elif cfg.status != amo.INAPP_STATUS_ACTIVE:
        raise AppPaymentsDisabled('Payments disabled (status=%s)'
                                  % (cfg.status), app_id=app_id)
    app_req['_config'] = cfg

    try:
        with statsd.timer('inapp_pay.verify'):
            jwt.decode(signed_request, cfg.get_private_key(), verify=True)
    except jwt.DecodeError, exc:
        _re_raise_as(RequestVerificationError,
                     'Payment verification failed: %s' % exc,
                     app_id=app_id)

    # Check timestamps:
    try:
        expires = float(str(app_req.get('exp')))
        issued = float(str(app_req.get('iat')))
    except ValueError:
        _re_raise_as(RequestVerificationError,
                     'Payment JWT had an invalid exp (%r) or iat (%r) '
                     % (app_req.get('exp'), app_req.get('iat')),
                     app_id=app_id)
    now = calendar.timegm(time.gmtime())
    if expires < now:
        raise RequestExpired('Payment JWT expired: %s UTC < %s UTC '
                             '(issued at %s UTC)'
                             % (datetime.utcfromtimestamp(expires),
                                datetime.utcfromtimestamp(now),
                                datetime.utcfromtimestamp(issued)),
                             app_id=app_id)
    if issued < (now - 3600): # issued more than an hour ago
        raise RequestExpired('Payment JWT iat expired: %s UTC < %s UTC '
                             % (datetime.utcfromtimestamp(issued),
                                datetime.utcfromtimestamp(now)),
                             app_id=app_id)
    try:
        not_before = float(str(app_req.get('nbf')))
    except ValueError:
        app_req['nbf'] = None # this field is optional
    else:
        about_now = now + 300 # pad 5 minutes for clock skew
        if not_before >= about_now:
            raise InvalidRequest('Payment JWT cannot be processed before '
                                 '%s UTC (nbf must be < %s UTC)'
                                 % (datetime.utcfromtimestamp(not_before),
                                    datetime.utcfromtimestamp(about_now)),
                                 app_id=app_id)

    # Check JWT audience.
    audience = app_req.get('aud', None)
    if not audience:
        raise InvalidRequest('Payment JWT is missing aud (audience)',
                             app_id=app_id)
    if audience != settings.INAPP_MARKET_ID:
        raise InvalidRequest('Payment JWT aud (audience) must be set to %r; '
                             'got: %r' % (settings.INAPP_MARKET_ID,
                                          audience),
                             app_id=app_id)

    request = app_req.get('request', None)

    # Check payment details.
    if not isinstance(request, dict):
        raise InvalidRequest('Payment JWT is missing request dict: %r'
                             % request, app_id=app_id)
    for key in ('priceTier', 'name', 'description'):
        if key not in request:
            raise InvalidRequest('Payment JWT is missing request[%r]'
                                 % key, app_id=app_id)

    # Validate values for model integrity.
    key_trans = {'app_data': 'productdata'}
    for form in (PaymentForm(), ContributionForm()):
        for name, field in form.fields.items():
            if name in ('amount', 'currency'):
                # Since we're using price tiers we don't need to complain
                # about missing amount (which is price in the request)
                # or currency.
                continue
            req_field = key_trans.get(name, name)
            value = request[req_field]
            try:
                field.clean(value)
            except forms.ValidationError, exc:
                _re_raise_as(InvalidRequest,
                             u'request[%r] is invalid: %s' % (req_field, exc))

    # Validate the price tier.
    try:
        if not Price.objects.filter(pk=request['priceTier']).exists():
            raise InvalidRequest(
                    u'priceTier:%s is not a supported price tier. Consult the '
                    u'docs for all supported tiers: '
                    u'https://developer.mozilla.org/en/Apps/In-app_payments'
                    % request['priceTier'])
    except ValueError:
        _re_raise_as(InvalidRequest,
                     u'priceTier:%r is not a valid number'
                     % request['priceTier'])

    return app_req
Something went wrong with that request. Please try again.