@kilnjs/payments-play
v0.1.3
Published
Google Play billing for KilnJS: real time notifications verified by the token that carried them, and purchases read from the Android Publisher API
Readme
@kilnjs/payments-play
Google Play billing for KilnJS: real time developer notifications, and purchases read from the Android Publisher API. No SDK.
yarn add @kilnjs/payments @kilnjs/payments-play// app.config.ts
import { payments } from "@kilnjs/payments";
import "@kilnjs/payments-play";| option | environment | |
| --- | --- | --- |
| packageName | PLAY_PACKAGE_NAME | required. Checked against every notification |
| audience | PLAY_PUSH_AUDIENCE | required. What the push subscription claims as its audience, usually this route's URL |
| pushAccount | PLAY_PUSH_SERVICE_ACCOUNT | required to verify. The service account Pub/Sub pushes as |
| clientEmail | PLAY_CLIENT_EMAIL | for reading purchases, which is a different account |
| privateKey | PLAY_PRIVATE_KEY | as above |
| keysUrl | PLAY_KEYS_URL | where Google publishes its signing keys |
| api | PLAY_API_URL | the Android Publisher API |
| timeout | PLAY_TIMEOUT_MS | 10000 |
Play is the one that carries no signature
This is the difference that gets shipped as a vulnerability. A Play
notification arrives as a plain Pub/Sub message with no signature of its
own: anybody who learns the endpoint can post one. What proves it is the
OIDC token Pub/Sub puts in the Authorization header, signed by Google,
naming the service account the subscription was configured with and the
audience it was configured for.
So an endpoint that reads the message and ignores that header is an endpoint where anybody can grant themselves a subscription. Four things are checked, and each has a forged token in the tests:
- the signature, against Google's published keys, fetched once and held;
- the issuer, which is Google rather than somebody who said so;
- the audience, or a token minted for another service is accepted here;
- the service account, or any Google customer's token is accepted here.
pushAccount is therefore not optional: without it there is nothing to check a
token against, and this package refuses to verify rather than verifying
nothing.
What it maps
| Play | kind |
| --- | --- |
| 4 subscription purchased, 1 one-off purchased | purchased |
| 2 renewed | renewed |
| 3, 20 | cancelled |
| 5, 6 | billing_failed |
| 1, 7, 12 recovered | recovered |
| 10, 11 paused | paused |
| 13 expired | expired |
| any voided purchase | refunded |
| Google's own test notification | other |
A Play notification never says who bought it. event.actor is empty on
purpose: the answer comes from asking the API with the purchase token, which is
why the reference arrives with the entitlement rather than the event.
Google's "canceled" means "will not renew", not "over". A cancelled
subscription is reported as active with renewing: false until its expiry,
because treating it as expired cuts a paying customer off early.
Verifying
yarn verify # ts:check, lint, build, exports:check, testGoogle is never reached: the suite signs its own push tokens with keys it generates, stands in for Google's published certificates, and forges every claim worth forging.
