web-dev-qa-db-ja.com

FirebaseFunctionsからGooglePlay DeveloperAPIを呼び出す

ユーザーのアプリ内購入とサブスクリプションのサーバー側検証を 推奨 として開発しようとしていますが、そのためにFirebaseFunctionsを使用したいと思います。基本的には、購入トークンを受け取り、Play Developer APIを呼び出して購入を確認し、その結果を処理するHTTPトリガー関数である必要があります。

ただし、多くのGoogle API( Play Developer API を含む)を呼び出すには、重要な認証が必要です。必要な設定を理解する方法は次のとおりです。

  1. Google Play Developer APIv2が有効になっているGCPプロジェクトが必要です。
  2. Google PlayコンソールでPlayストアにリンクできるのは1つだけなので、これは別のプロジェクトである必要があります。
  3. 私のFirebaseFunctionsプロジェクトは、他のプロジェクトに対して何らかの方法で認証する必要があります。このサーバー間シナリオでは、サービスアカウントの使用が最適であると考えました。
  4. 最後に、Firebase Functionsコードは何らかの方法で認証トークン(できればJWT?)を取得し、最後にAPI呼び出しを行ってサブスクリプションステータスを取得する必要があります。

問題は、人間が読める形式のドキュメントやガイダンスがまったく存在しないことです。 Firebaseの入力トラフィックが無料プランに含まれていることを考えると(FirebaseFunctionsのGoogleAPIの使用を推奨していると思います)、その事実はかなり残念です。私はあちこちでいくつかの情報を見つけることができましたが、Google APIの経験が少なすぎるため(ほとんどの場合、APIキーを使用するだけで済みます)、それをまとめるのに助けが必要です。

これが私がこれまでに理解したことです:

  1. Playストアにリンクされ、APIが有効になっているGCPプロジェクトを取得しました。ただし、何らかの理由でAPI Explorerでテストしようとすると、「Google Play DeveloperAPIの呼び出しに使用されたプロジェクトIDがGooglePlay DeveloperConsoleにリンクされていません」というエラーが発生します。
  2. サービスアカウントを作成し、JWTを生成するためのキーを含むJSONキーをエクスポートしました。
  3. また、Playコンソールでそのサービスアカウントの読み取り権限を設定しました。
  4. Node.JSクライアントライブラリ のGoogle APIを見つけました。これはアルファ版であり、ドキュメントが非常にまばらです(たとえば、JWTで認証する方法に関する明確なドキュメントがなく、=の呼び出し方法に関するサンプルもありません。 AndroidパブリッシャーAPI)。現時点では、それに苦労しています。残念ながら、特にエディターがジャンプする可能性を提供していない場合は、JSライブラリコードを読むことにあまり慣れていません。強調表示された関数のソース。

Firebase Functionsからのアプリ内購入の確認は一般的な作業のように思われるため、これが質問も文書化もされていないことにかなり驚いています。誰かが以前にそれを成功させたことがありますか、それともFirebaseチームが答えるために介入しますか?

13
Actine

私はそれを自分で理解しました。また、重いクライアントライブラリを捨てて、それらのいくつかのリクエストを手動でコーディングしました。

ノート:

  • 同じことがNode.jsサーバー環境にも当てはまります。 JWTを作成するには別のサービスアカウントのキーファイルが必要であり、APIを呼び出すには2つの手順が必要です。Firebaseも例外ではありません。
  • 同じことが認証を必要とする他のAPIにも当てはまります—JWTのscopeフィールドのみが異なります。
  • いくつかのAPI JWTをアクセストークンと交換する必要がないものがあります— OAuthバックエンドへのラウンドトリップなしで、JWTをミントしてAuthentication: Bearerで直接提供できます。

Playストアにリンクされているサービスアカウントの秘密鍵を含むJSONファイルを取得した後、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);
    });
});

これら は私がフォローしたドキュメントです。

13
Actine

私はこれを行うための少し速い方法を見つけたと思います...または少なくとも...もっと簡単に。

スケーリングをサポートし、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;
}

それが私たちがそのことをする方法です。

1
Brian Burgess