I'm setting up App Store Notifications for my app. Having trouble verifying even the TEST notification, through.
I'm generating JWT-token and sending it via Postman. I get a successful notification UUID as a response. But my Node.JS endpoint says it can't verify it. Here's the endpoint:
const fs = require('fs');
const path = require('path');
const { SignedDataVerifier, Environment } = require('@apple/app-store-server-library');
module.exports = function (sqlexec) {
function loadRootCAs() {
// const gPath = path.resolve(__dirname, "AppleIncRootCertificate.cer");
const g3Path = path.resolve(__dirname, "AppleRootCA-G3.cer");
// const g2Path = path.resolve(__dirname, "AppleRootCA-G2.cer");
const loadedCerts = [];
try {
// loadedCerts.push(fs.readFileSync(gPath));
loadedCerts.push(fs.readFileSync(g3Path));
// loadedCerts.push(fs.readFileSync(g2Path));
if (loadedCerts.length === 0) {
throw new Error("No Apple Root CA certificates were loaded.");
}
console.log("[APPLE NOTIFICATIONS2] Apple Root CA certificates loaded successfully.");
return loadedCerts;
} catch (error) {
console.error("❌ CRITICAL: Error loading Apple Root CA certificate(s):", error.message);
console.error("Ensure 'AppleRootCA-G3.cer' (and others if specified) are present at the expected path and readable.");
throw new Error("Failed to load essential Apple Root CA certificates. Notification processing will fail.");
}
}
const appleRootCAs = loadRootCAs();
const enableOnlineChecks = true;
const environment = Environment.SANDBOX;
const bundleId = "SomeBundleID";
const appAppleId = undefined; // Set if you're in PRODUCTION
const verifier = new SignedDataVerifier(
appleRootCAs,
enableOnlineChecks,
environment,
bundleId,
appAppleId
);
router.post('/notifications_refund', async function (req, res) {
const signedPayload = req.body.signedPayload;
try {
const notificationVerificationResult = await verifier.verifyAndDecodeNotification(signedPayload);
if (!notificationVerificationResult.isValid) {
console.error(`[APPLE NOTIFICATIONS2] Failed to verify notification. Status: ${notificationVerificationResult.verificationStatus}, Error: ${notificationVerificationResult.errorMessage || 'N/A'}`);
return res.status(400).json({ status: "error", message: "Invalid notification signature or payload." });
}
const decodedNotification = notificationVerificationResult.payload;
const notificationType = decodedNotification.notificationType;
const subtype = decodedNotification.subtype;
if (notificationType === 'TEST') {
console.log(`[APPLE NOTIFICATIONS2] Received TEST notification. Subtype: ${subtype}`);
// The TEST notification's data.signedTransactionInfo is a JWS representing a sample transaction.
} else if (notificationType === 'REFUND') {
console.log(`[APPLE NOTIFICATIONS2] Received REFUND notification. Subtype: ${subtype}`);
} else {
console.log(`[APPLE NOTIFICATIONS2] Received notificationType: ${notificationType}, Subtype: ${subtype}. Skipping non-refund/test type for this endpoint.`);
return res.status(200).json({ status: "success", message: "Notification received, but not a type processed by this refund endpoint." });
}
// Ensure data and signedTransactionInfo exist
if (!decodedNotification.data || !decodedNotification.data.signedTransactionInfo) {
console.error("[APPLE NOTIFICATIONS2] Notification payload is missing data or signedTransactionInfo.");
return res.status(400).json({ status: "error", message: "Notification missing transaction info." });
}
const transactionInfoJWS = decodedNotification.data.signedTransactionInfo;
const transactionVerificationResult = await verifier.verifyAndDecodeTransaction(transactionInfoJWS);
if (!transactionVerificationResult.isValid) {
console.error(`[APPLE NOTIFICATIONS2] Failed to verify signedTransactionInfo. Status: ${transactionVerificationResult.verificationStatus}, Error: ${transactionVerificationResult.errorMessage || 'N/A'}`);
return res.status(400).json({ status: "error", message: "Invalid signedTransactionInfo in notification." });
}
const verifiedTransactionPayload = transactionVerificationResult.payload;
const transactionId = verifiedTransactionPayload.originalTransactionId || verifiedTransactionPayload.transactionId;
console.log(`[APPLE NOTIFICATIONS2] Successfully decoded Transaction ID: ${transactionId} for notification type ${notificationType}`);
// This is where my refund logic starts in case the notif is refund, but fow now I'm just trying to verify a TEST notif
return res.status(200).json({
status: "success",
message: "Refund (or TEST) notification processed successfully and validated."
});
} catch (error) {
console.error("[APPLE NOTIFICATIONS2] Critical error processing notification:", error);
// Check if the error is from the verifier or elsewhere
if (error.name === 'SignedDataVerificationError') { // Example, check actual error type from library
return res.status(400).json({status: "error", message: `Notification verification failed: ${error.message}`});
}
return res.status(500).json({
status: "error",
message: "Failed to process notification due to an internal server error."
});
}
});
return router;
};
I tried different root certs, only G3 works, other two give errors. Also tried adding G3 intermediate (WWDRCAG3), but it doesn't seem to help.
What am I doing wrong?