Позвоните в Google Play Developer API из функций Firebase
Я пытаюсь разработать на стороне сервера проверку покупок и подписок в приложении моих пользователей в соответствии с рекомендациями, и я хочу использовать для этого функции Firebase. По сути, это должна быть триггерная функция HTTP, которая получает токен покупки, вызывает API разработчика Play для проверки покупки, а затем что-то делает с результатом.
Однако для вызова многих API Google (в том числе API Play Developer) требуется нетривиальная авторизация. Вот как я понимаю необходимую настройку:
- Должен быть проект GCP с включенным Google Play Developer API v2.
- Это должен быть отдельный проект, поскольку в Консоли Google Play может быть только один связанный с Play Store.
- Мой проект Firebase Functions должен как-то аутентифицироваться в этом другом проекте. Я подумал, что использование учетной записи службы наиболее подходит в этом сценарии сервер-сервер.
- Наконец, мой код функций Firebase должен каким-то образом получить токен аутентификации (надеюсь, JWT?) И, наконец, сделать вызов API для получения статуса подписки.
Проблема заключается в том, что абсолютно никакой читабельной документации или руководства по этому вопросу не существует. Учитывая, что входящий трафик в Firebase включен в бесплатный план (поэтому я предполагаю, что они поощряют использование API Google из Firebase Functions), этот факт довольно разочаровывает. Мне удалось найти кое-какую информацию здесь и там, но, имея слишком мало опыта работы с API Google (для большинства из них требовалось просто использовать ключ API), мне нужна помощь в его сборке.
Вот что я понял до сих пор:
- Я получил проект GCP, связанный с Play Store и с включенным API. Однако по какой-то причине попытка его протестировать в проводнике API приводит к ошибке "Идентификатор проекта, используемый для вызова API разработчика Google Play, не был связан в консоли разработчика Google Play".
- Я создал служебную учетную запись и экспортировал ключ JSON, который содержит ключ для создания JWT.
- Я также настроил разрешения на чтение для этой учетной записи службы в консоли Play.
- Я нашел клиентскую библиотеку Node.JS для API Google, которая находится в альфа- версии и содержит очень скудную документацию (например, нет очевидной документации о том, как проходить аутентификацию с помощью JWT, и нет примеров того, как вызывать API издателя Android). На данный момент я борюсь с этим. К сожалению, мне не очень удобно читать код библиотеки JS, особенно когда редактор не предоставляет возможность переходить к выделенным источникам функций.
Я очень удивлен, что об этом не спрашивали и не документировали, потому что проверка покупок в приложении из Firebase Functions кажется обычной задачей. Кто-нибудь успешно делал это раньше, или, может быть, команда Firebase вмешается, чтобы ответить?
2 ответа
Я понял это сам. Я также отказался от тяжеловесной клиентской библиотеки и просто закодировал эти несколько запросов вручную.
Заметки:
- То же самое относится к любой серверной среде Node.js. Вам все еще нужен файл ключа отдельной учетной записи службы для создания JWT и два шага для вызова API, и Firebase ничем не отличается.
- То же самое относится и к другим API, которые также требуют аутентификации - отличаются только
scope
поле JWT. - Существует несколько API-интерфейсов, для которых вам не нужно обменивать JWT на токен доступа - вы можете создать JWT и предоставить его непосредственно в
Authentication: Bearer
без обратной поездки в бэкэнд OAuth.
После получения файла JSON с закрытым ключом для учетной записи службы, связанной с Play Store, код для вызова API выглядит следующим образом (подстраивается под ваши потребности). Примечание: я использовал request-promise
как хороший способ сделать http.request
,
const functions = require('firebase-functions');
const jwt = require('jsonwebtoken');
const keyData = require('./key.json'); // Path to your JSON key file
const request = require('request-promise');
/**
* Exchanges the private key file for a temporary access token,
* which is valid for 1 hour and can be reused for multiple requests
*/
function getAccessToken(keyData) {
// Create a JSON Web Token for the Service Account linked to Play Store
const token = jwt.sign(
{ scope: 'https://www.googleapis.com/auth/androidpublisher' },
keyData.private_key,
{
algorithm: 'RS256',
expiresIn: '1h',
issuer: keyData.client_email,
subject: keyData.client_email,
audience: 'https://www.googleapis.com/oauth2/v4/token'
}
);
// Make a request to Google APIs OAuth backend to exchange it for an access token
// Returns a promise
return request.post({
uri: 'https://www.googleapis.com/oauth2/v4/token',
form: {
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion': token
},
transform: body => JSON.parse(body).access_token
});
}
/**
* Makes a GET request to given URL with the access token
*/
function makeApiRequest(url, accessToken) {
return request.get({
url: url,
auth: {
bearer: accessToken
},
transform: body => JSON.parse(body)
});
}
// Our test function
exports.testApi = functions.https.onRequest((req, res) => {
// TODO: process the request, extract parameters, authenticate the user etc
// The API url to call - edit this
const url = `https://www.googleapis.com/androidpublisher/v2/applications/${packageName}/purchases/subscriptions/${subscriptionId}/tokens/${token}`;
getAccessToken(keyData)
.then(token => {
return makeApiRequest(url, token);
})
.then(response => {
// TODO: process the response, e.g. validate the purchase, set access claims to the user etc.
res.send(response);
return;
})
.catch(err => {
res.status(500).send(err);
});
});
Это документы, за которыми я следовал.
Я думаю, что нашел немного более быстрый способ сделать это... или по крайней мере... более просто.
Чтобы поддерживать масштабирование и не допустить выхода index.ts из-под контроля... У меня есть все функции и глобальные переменные в файле индекса, но все фактические события обрабатываются обработчиками. Проще поддерживать.
Так вот мой index.ts (я безопасность типа сердца):
//my imports so you know
import * as functions from 'firebase-functions';
import * as admin from "firebase-admin";
import { SubscriptionEventHandler } from "./subscription/subscription-event-handler";
// honestly not 100% sure this is necessary
admin.initializeApp({
credential: admin.credential.applicationDefault(),
databaseURL: 'dburl'
});
const db = admin.database();
//reference to the class that actually does the logic things
const subscriptionEventHandler = new SubscriptionEventHandler(db);
//yay events!!!
export const onSubscriptionChange = functions.pubsub.topic('subscription_status_channel').onPublish((message, context) => {
return subscriptionEventHandler.handle(message, context);
});
//aren't you happy this is succinct??? I am!
Теперь... для шоу!
// importing like World Market
import * as admin from "firebase-admin";
import {SubscriptionMessageEvent} from "./model/subscription-message-event";
import {androidpublisher_v3, google, oauth2_v2} from "googleapis";
import {UrlParser} from "../utils/url-parser";
import {AxiosResponse} from "axios";
import Schema$SubscriptionPurchase = androidpublisher_v3.Schema$SubscriptionPurchase;
import Androidpublisher = androidpublisher_v3.Androidpublisher;
// you have to get this from your service account... or you could guess
const key = {
"type": "service_account",
"project_id": "not going to tell you",
"private_key_id": "really not going to tell you",
"private_key": "okay... I'll tell you",
"client_email": "doesn't matter",
"client_id": "some number",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "another url"
};
//don't guess this... this is right
const androidPublisherScope = "https://www.googleapis.com/auth/androidpublisher";
// the handler
export class SubscriptionEventHandler {
private ref: admin.database.Reference;
// so you don't need to do this... I just did to log the events in the db
constructor(db: admin.database.Database) {
this.ref = db.ref('/subscriptionEvents');
}
// where the magic happens
public handle(message, context): any {
const data = JSON.parse(Buffer.from(message.data, 'base64').toString()) as SubscriptionMessageEvent;
// if subscriptionNotification is truthy then we're solid here
if (message.json.subscriptionNotification) {
// go get the the auth client but it's async... so wait
return google.auth.getClient({
scopes: androidPublisherScope,
credentials: key
}).then(auth => {
//yay! success! Build android publisher!
const androidPublisher = new Androidpublisher({
auth: auth
});
// get the subscription details
androidPublisher.purchases.subscriptions.get({
packageName: data.packageName,
subscriptionId: data.subscriptionNotification.subscriptionId,
token: data.subscriptionNotification.purchaseToken
}).then((response: AxiosResponse<Schema$SubscriptionPurchase>) => {
//promise fulfilled... grandma would be so happy
console.log("Successfully retrieved details: " + response.data.orderId);
}).catch(err => console.error('Error during retrieval', err));
});
} else {
console.log('Test event... logging test');
return this.ref.child('/testSubscriptionEvents').push(data);
}
}
}
Есть несколько моделей классов, которые помогают:
export class SubscriptionMessageEvent {
version: string;
packageName: string;
eventTimeMillis: number;
subscriptionNotification: SubscriptionNotification;
testNotification: TestNotification;
}
export class SubscriptionNotification {
version: string;
notificationType: number;
purchaseToken: string;
subscriptionId: string;
}
Вот как мы это делаем.