I am using the following code to implement apple server notifications, but it's giving me an error "Invalid JWT Header". I am using storekit2 to purchase subscriptions in the app and I'm using a node js backend to keep track of subscription updates. Can someone help?
When I debug further I get JWT verification failed: Error: Invalid JWT header: Missing kid
I think the issue is that apple has not enabled V2 and is still sending V1 payload because I don't see signedPayload in the response from apple. How can I get it to send V2?
const APPLE_JWKS_URL = ";;
const getApplePublicKey = async (kid) => {
try {
const response = await axios.get(APPLE_JWKS_URL);
const keys = response.data.keys;
const key = keys.find((k) => k.kid === kid);
if (!key) {
throw new Error("Matching key not found");
}
return jwksClient.certToPEM({
kty: key.kty,
n: key.n,
e: key.e,
});
} catch (error) {
console.error("Failed to fetch Apple's public key:", error);
throw new Error("Could not get Apple public key");
}
};
// Function to verify Apple's JWT signature
const verifyAppleJWT = async (token) => {
try {
const decodedHeader = jwt.decode(token, { complete: true });
if (!decodedHeader || !decodedHeader.header.kid) {
throw new Error("Invalid JWT header");
}
const publicKey = await getApplePublicKey(decodedHeader.header.kid);
return jwt.verify(token, publicKey, { algorithms: ["RS256"] });
} catch (error) {
console.error("JWT verification failed:", error);
return null;
}
};
// Apple Server Notification Endpoint
router.post("/apple-server-notifications", async (req, res) => {
try {
const { signedPayload } = req.body;
if (!signedPayload) {
return res.status(400).json({ error: "Missing signedPayload" });
}
// Verify the JWT signature with Apple's public key
const payload = await verifyAppleJWT(signedPayload);
if (!payload) {
return res.status(400).json({ error: "Invalid notification" });
}
console.log("Verified Notification:", JSON.stringify(payload, null, 2));
// Extract relevant data
const eventType = payload.notificationType;
const originalTransactionId = payload.data.originalTransactionId;
// Handle subscription events
switch (eventType) {
case "SUBSCRIBED":
case "DID_RENEW":
await updateSubscription(originalTransactionId, "active");
break;
case "EXPIRED":
await updateSubscription(originalTransactionId, "expired");
break;
case "CANCEL":
await updateSubscription(originalTransactionId, "canceled");
break;
case "DID_REVOKE":
await updateSubscription(originalTransactionId, "revoked");
break;
default:
console.log(`Unhandled event type: ${eventType}`);
}
res.status(200).json({ message: "Notification processed" });
} catch (error) {
console.error("Error handling notification:", error);
res.status(500).json({ error: "Internal Server Error" });
}
});