user3726962
user3726962

Reputation: 343

iOS Receipt Validation through Node.js using lambda

I am developing an iOS app in Swift and attempting to implement receipt validation for an in-app purchase. I couldn't figure out how to achieve this in Swift, so instead I tried having my app send the request through a Lambda function writtin in Node.js, after seeing Giulio Roggero's example in this question. My Swift code looks like this:

let receiptPath = Bundle.main.appStoreReceiptURL?.path
    if FileManager.default.fileExists(atPath: receiptPath!){
        var receiptData:NSData?
        do{
            receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped)
        }
        catch{
            print("ERROR: " + error.localizedDescription)
        }
        let receiptString = receiptData?.base64EncodedString(options: .endLineWithLineFeed)
        let invocationRequest = AWSLambdaInvokerInvocationRequest()
        invocationRequest?.functionName = "sendReceiptRequest"
        invocationRequest?.invocationType = AWSLambdaInvocationType.requestResponse
        invocationRequest?.payload = ["receipt-data" : receiptString!, "password" : SUBSCRIPTION_SECRET]

        let lambdaInvoker = AWSLambdaInvoker.default()
        lock()
        lambdaInvoker.invoke(invocationRequest!).continue(with: AWSExecutor.mainThread(), with: { (task:AWSTask!) -> AnyObject! in
            if task.error != nil {
                self.sendErrorPopup("Error: \(task.error?.localizedDescription)")
            } else {
                print("TOKEN: ", task.result)
            }
            self.unlock()
            return nil
        })}

My Lambda node.js function looks like this, following the example:

function (receiptData_base64, password, production, cb)
{
var url = production ? 'buy.itunes.apple.com' : 'sandbox.itunes.apple.com'
var receiptEnvelope = {
    "receipt-data": receiptData_base64,
    "password":password
};
var receiptEnvelopeStr = JSON.stringify(receiptEnvelope);
var options = {
    host: url,
    port: 443,
    path: '/verifyReceipt',
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(receiptEnvelopeStr)
    }
};

var req = https.request(options, function(res) {
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log("body: " + chunk);
        cb(true, chunk);
    });
    res.on('error', function (error) {
        console.log("error: " + error);
        cb(false, error);
    });
});
req.write(receiptEnvelopeStr);
req.end();
}

However, when running this code, either through a lambda test or through my app, I get an error message that simply says Response body: {"errorMessage":"true"}. I've noticed that if I tweak the code I can create more expected errors- For instance, if I have some other value for the receipt-data, I get a 21002 error code in response, and if I change "production" to true, I get a 21007 error. Part of the problem is that I don't know exactly how the callback is supposed to work-- is the block inside https.request correct for what I'm trying to do in Swift? I get the impression that the receipt-data is correctly formatted since changing it yields a different result, so why is the end result still an error?

EDIT:

Something I previously didn't notice is that when I run the Lambda function, the line "body: (receipt data)" appears, where (receipt data) is the base 64 encoded data I sent to the function. This makes me suspect I'm not reaching the error callback block at all, and that the error has something to do with the way I send the result of the callback back to my app. What is this block:

var req = https.request(options, function(res) {
res.setEncoding('utf8');
res.on('data', function (chunk) {
    console.log("body: " + chunk);
    cb(true, chunk);
});
res.on('error', function (error) {
    console.log("error: " + error);
    cb(false, error);
});
});

supposed to do? Is it possible I need to enable some permission to receive the callback?

Upvotes: 2

Views: 2869

Answers (4)

Pete Shilling
Pete Shilling

Reputation: 186

Here's what worked for me:

'use strict'
var AWS = require('aws-sdk')
var https = require('https')

exports.handler = (event, context, callback) => {

    var payload = JSON.stringify({
        'receipt-data': event.arguments.input.base64_receipt
    })

    function generateOptions(url) {
        return {
            host: url,
            port: 443,
            path: '/verifyReceipt',
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(payload)
            }
        }
    }

    function validateReceipt(url, payload, errorCallback) {
        var req = https.request(generateOptions(url), function(res) {
            res.setEncoding('utf8')
            res.on('data', function(chunk) {
                var data = JSON.parse(chunk)
                // success
                callback(null, event)
            })
            res.on('error', function(error) {
                //error
                errorCallback ? errorCallback() : callback('There was an error validating the transaction', event)
            })
        })
        req.write(payload)
        req.end()
    }

    // attempt to validate on production and sandbox
    validateReceipt('buy.itunes.apple.com', payload, function() {
        validateReceipt('sandbox.itunes.apple.com', payload)
    })
}

Upvotes: 1

Fredo
Fredo

Reputation: 63

In your Swift code, you check "if task.error != nil" then you have an error

    lambdaInvoker.invoke(invocationRequest!).continue(with: AWSExecutor.mainThread(), with: { (task:AWSTask!) -> AnyObject! in
    if task.error != nil {
        self.sendErrorPopup("Error: \(task.error?.localizedDescription)")
    } else {
        print("TOKEN: ", task.result)
    }

but in your node code, you call the callback that way:

cb(true, chunk);

The first parameter is your error which is not going to be nil. you should replace this line with:

cb(null, chunk);

You are going to get your correct Json that way

Upvotes: 0

Steven Fisher
Steven Fisher

Reputation: 44876

For anyone finding this later, potential problems:

  1. The big problem here is that the Content-Type is incorrect. You need to post JSON, not url encoding; while the code does that, it uses the wrong type to tell the server what format it's using. It should be application/json not application/x-www-form-urlencoded.
  2. Base64 sent to Apple's servers must not (as of last week, at least) contain line endings. I don't know that this is a problem here, but it's something I've hit in the past and the .endLineWithLineFeed makes me suspicious.
  3. The algorithm used to switch between servers. Instead of allowing switching between production and sandbox, the method should try to verify the receipt to the production server. If the result object has a status of 21007, the code should then try the same receipt against the sandbox server. Once a receipt has been decoded and verified, the client can check the receipt to see which server it was verified against and (as necessary) ignore sandbox receipts.

Upvotes: 2

user3726962
user3726962

Reputation: 343

Ultimately I solved the problem by following this example and using Python instead of Node.js. I still don't know exactly what was wrong with my code before, but base-64 encoding in Swift and then sending the receipt data and password to a Python Lambda function gave me the correct result.

Upvotes: 0

Related Questions