My current Ionic / Angular / Firebase + a very simple Node server app has security issue when using Braintree to charge user credit card. The problem, according to @RaymondBerg is because client can post any customerId and create a braintree token and charge that customer. Since all my user authorization happened in Firebase / Angular - client side. So when user do a $HTTP.post from my AngularJS/Ionic to my Node server, I don't want to authorize them again (as I don't even know how to do that so I use Firebase).
So what is the strategy here to setup Firebase and my Node server to work with payment system like braintree?
One thing I can think off, is to first create a node in my firebase before http request and then pass in the client $id for request in client side (Ionic app):
$scope.getToken = function () { var ref = new Firebase('[FirebaseURL]/braintreePaymentToken'); var tokenObj = $firebaseObject(ref.child(posterId)); tokenObj.tokenGenerated = true; tokenObj.$save().then(function(){ $http({ method: 'POST', url: 'http://localhost:3000/api/v1/token', data: { //user $id from Firebase userId: snapshot.key(), } })}
In Firebase, I set up a security rule as:
"braintreePayment": {
".read": false,
".write": false,
},
"braintreePaymentToken": {
"$uid": {
".read": "auth != null",
".write": "auth != null && auth.uid == $uid",
}
},
This way, the temp node braintreePaymentToken can ONLY be written by current login user in the app. Other login user (nefarious user) can not write on this node b/c their auth.uid will not equal to posterId, which posterId is the user who need to pay.
On server end, I use once to see if I can find the value:
var ref = new Firebase('[FirebaseURL]');
app.post('/api/v1/token', jsonParser, function (request, response) {
var userId = request.body.userId;
console.log (userId);
//customerId from braintree is stored here so no one except the server can read it
ref.child('braintreePayment').child(userId).once("value", function(snapshot){
var exists = (snapshot.val() !== null);
console.log (exists);
if (exists) {
console.log ("using exsiting customer!");
//If braintreePaymentToken with userId child exsited, it mean this request is come from my Ionic client, not from anywhere else.
ref.child('braintreePaymentToken').child(userId).once("value", function(snap) {
if (snap.val()) {
gateway.clientToken.generate({
customerId: snapshot.val().customerId
}, function (err, res) {
if (err) throw err;
response.json({
"client_token": res.clientToken
});
//After I return the clientToken, I delete the braintreePaymentToken node. It is like using Firebase to send email with Zaiper. More secue I guess?
ref.child('braintreePaymentToken').child(userId).remove();
});
else {
response.json({
"client_token": "Unauthorized Access!"
});
}
} else {
console.log ("using no customer!");
gateway.clientToken.generate({}, function (err, res) {
if (err) throw err;
response.json({
"client_token": res.clientToken
});
});
}
});
});
And when user hit pay button on my client(ionic app), I do the Firebase Once request again to see if the customerId already in my firebase/braintreePayment. If not, we save one with the return transaction customerId created by braintree.
app.post('/api/v1/process', jsonParser, function (request, response) {
var transaction = request.body;
ref.child('braintreePayment').child(transaction.userId).once("value", function(snapshot){
var exists = (snapshot.val() !== null);
console.log (exists);
if (exists) {
console.log ("Return customer!");
gateway.transaction.sale({
amount: transaction.amount,
paymentMethodNonce: transaction.payment_method_nonce,
options: {
submitForSettlement: true
},
}, function (err, result) {
if (err) throw err;
response.json(result);
});
} else {
console.log ("First time customer!");
gateway.transaction.sale({
amount: transaction.amount,
paymentMethodNonce: transaction.payment_method_nonce,
options: {
store_in_vault_on_success: true,
submitForSettlement: true
},
}, function (err, result) {
if (err) throw err;
console.log ("Customer Id: " + result.transaction.customer.id);
var customerId = result.transaction.customer.id;
ref.child('braintreePayment').child(transaction.userId).update({customerId: customerId});
response.json(result);
});
}
});
});
As you see, this is REALLY COMPLICATED. But I do not know a better, secue way to do this... Is this the best way to structure between Firebase, Node, and Braintree? Is this address the OWASP security concern? Is there way to improve this code to be better or there is a better way to do it?