Позвоните в Google Play Developer API из функций Firebase

Я пытаюсь разработать на стороне сервера проверку покупок и подписок в приложении моих пользователей в соответствии с рекомендациями, и я хочу использовать для этого функции Firebase. По сути, это должна быть триггерная функция HTTP, которая получает токен покупки, вызывает API разработчика Play для проверки покупки, а затем что-то делает с результатом.

Однако для вызова многих API Google (в том числе API Play Developer) требуется нетривиальная авторизация. Вот как я понимаю необходимую настройку:

  1. Должен быть проект GCP с включенным Google Play Developer API v2.
  2. Это должен быть отдельный проект, поскольку в Консоли Google Play может быть только один связанный с Play Store.
  3. Мой проект Firebase Functions должен как-то аутентифицироваться в этом другом проекте. Я подумал, что использование учетной записи службы наиболее подходит в этом сценарии сервер-сервер.
  4. Наконец, мой код функций Firebase должен каким-то образом получить токен аутентификации (надеюсь, JWT?) И, наконец, сделать вызов API для получения статуса подписки.

Проблема заключается в том, что абсолютно никакой читабельной документации или руководства по этому вопросу не существует. Учитывая, что входящий трафик в Firebase включен в бесплатный план (поэтому я предполагаю, что они поощряют использование API Google из Firebase Functions), этот факт довольно разочаровывает. Мне удалось найти кое-какую информацию здесь и там, но, имея слишком мало опыта работы с API Google (для большинства из них требовалось просто использовать ключ API), мне нужна помощь в его сборке.

Вот что я понял до сих пор:

  1. Я получил проект GCP, связанный с Play Store и с включенным API. Однако по какой-то причине попытка его протестировать в проводнике API приводит к ошибке "Идентификатор проекта, используемый для вызова API разработчика Google Play, не был связан в консоли разработчика Google Play".
  2. Я создал служебную учетную запись и экспортировал ключ JSON, который содержит ключ для создания JWT.
  3. Я также настроил разрешения на чтение для этой учетной записи службы в консоли Play.
  4. Я нашел клиентскую библиотеку 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;
}

Вот как мы это делаем.