从 Firebase 函数调用 Google Play Developer API
Call Google Play Developer API from Firebase Functions
我正在尝试为用户的应用内购买和订阅开发服务器端验证 recommended,我想为此使用 Firebase Functions。基本上它必须是一个接收购买令牌的 HTTP 触发函数,调用 Play Developer API 来验证购买,然后对结果做一些事情。
但是,调用许多 Google API(包括 Play Developer API)需要重要的授权。以下是我对所需设置的理解:
- 必须有一个启用了 Google Play Developer API v2 的 GCP 项目。
- 它应该是一个单独的项目,因为在 Google Play 控制台中只能有一个链接到 Play 商店。
- 我的 Firebase Functions 项目必须以某种方式向另一个项目进行身份验证。我认为使用服务帐户最适合这种服务器到服务器的场景。
- 最后,我的 Firebase Functions 代码必须以某种方式获取身份验证令牌(希望是 JWT?)并最终进行 API 调用以获取订阅状态。
问题是绝对没有人类可读的文档或指南存在。鉴于 Firebase 中的入口流量包含在免费计划中(所以我假设他们鼓励使用来自 Firebase Functions 的 Google APIs),这个事实非常令人失望。我设法在这里和那里找到了一些信息,但是对 Google APIs 的经验太少(其中大部分只需要使用 api 键),我需要帮助把它放在一起。
这是我到目前为止的发现:
- 我有一个 GCP 项目链接到 Play 商店并启用了 API。但出于某种原因,尝试在 APIs Explorer 中测试它会导致错误 "The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console"。
- 我创建了一个服务帐户并导出了一个 JSON 密钥,其中包含生成 JWT 的密钥。
- 我还在 Play 管理中心为该服务帐户设置了读取权限。
- 我找到了 Google APIs 的 Node.JS client library,它处于 alpha 阶段并且文档非常稀疏(例如,没有关于如何使用 JWT 进行身份验证的明显文档,也没有有关如何调用 android 发布者 API 的示例)。目前我正在努力解决这个问题。不幸的是,我不太喜欢阅读 JS 库代码,尤其是当编辑器不提供跳转到突出显示的函数源代码的可能性时。
我很惊讶这没有被询问或记录,因为从 Firebase Functions 验证应用内购买似乎是一项常见任务。以前有没有人成功过,或者 Firebase 团队会介入回答?
我自己想出来了。我还放弃了重量级客户端库,只是手动编写了那几个请求。
备注:
- 这同样适用于任何 Node.js 服务器环境。您仍然需要单独服务帐户的密钥文件来创建 JWT 和调用 API 的两个步骤,而 Firebase 也不例外。
- 这同样适用于其他需要身份验证的 API——不同之处仅在于 JWT 的
scope
字段。
- 有 a few APIs 不需要您将 JWT 换成访问令牌 — 您可以创建 JWT 并直接在
Authentication: Bearer
中提供它,而无需往返 OAuth 后端.
在您获得 JSON 文件以及链接到 Play 商店的服务帐户的私钥后,调用 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);
});
});
These 是我遵循的文档。
我想我找到了一种稍微更快的方法...或者至少...更简单。
为了支持缩放并防止 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;
}
这就是我们做那件事的方式。
我正在尝试为用户的应用内购买和订阅开发服务器端验证 recommended,我想为此使用 Firebase Functions。基本上它必须是一个接收购买令牌的 HTTP 触发函数,调用 Play Developer API 来验证购买,然后对结果做一些事情。
但是,调用许多 Google API(包括 Play Developer API)需要重要的授权。以下是我对所需设置的理解:
- 必须有一个启用了 Google Play Developer API v2 的 GCP 项目。
- 它应该是一个单独的项目,因为在 Google Play 控制台中只能有一个链接到 Play 商店。
- 我的 Firebase Functions 项目必须以某种方式向另一个项目进行身份验证。我认为使用服务帐户最适合这种服务器到服务器的场景。
- 最后,我的 Firebase Functions 代码必须以某种方式获取身份验证令牌(希望是 JWT?)并最终进行 API 调用以获取订阅状态。
问题是绝对没有人类可读的文档或指南存在。鉴于 Firebase 中的入口流量包含在免费计划中(所以我假设他们鼓励使用来自 Firebase Functions 的 Google APIs),这个事实非常令人失望。我设法在这里和那里找到了一些信息,但是对 Google APIs 的经验太少(其中大部分只需要使用 api 键),我需要帮助把它放在一起。
这是我到目前为止的发现:
- 我有一个 GCP 项目链接到 Play 商店并启用了 API。但出于某种原因,尝试在 APIs Explorer 中测试它会导致错误 "The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console"。
- 我创建了一个服务帐户并导出了一个 JSON 密钥,其中包含生成 JWT 的密钥。
- 我还在 Play 管理中心为该服务帐户设置了读取权限。
- 我找到了 Google APIs 的 Node.JS client library,它处于 alpha 阶段并且文档非常稀疏(例如,没有关于如何使用 JWT 进行身份验证的明显文档,也没有有关如何调用 android 发布者 API 的示例)。目前我正在努力解决这个问题。不幸的是,我不太喜欢阅读 JS 库代码,尤其是当编辑器不提供跳转到突出显示的函数源代码的可能性时。
我很惊讶这没有被询问或记录,因为从 Firebase Functions 验证应用内购买似乎是一项常见任务。以前有没有人成功过,或者 Firebase 团队会介入回答?
我自己想出来了。我还放弃了重量级客户端库,只是手动编写了那几个请求。
备注:
- 这同样适用于任何 Node.js 服务器环境。您仍然需要单独服务帐户的密钥文件来创建 JWT 和调用 API 的两个步骤,而 Firebase 也不例外。
- 这同样适用于其他需要身份验证的 API——不同之处仅在于 JWT 的
scope
字段。 - 有 a few APIs 不需要您将 JWT 换成访问令牌 — 您可以创建 JWT 并直接在
Authentication: Bearer
中提供它,而无需往返 OAuth 后端.
在您获得 JSON 文件以及链接到 Play 商店的服务帐户的私钥后,调用 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);
});
});
These 是我遵循的文档。
我想我找到了一种稍微更快的方法...或者至少...更简单。
为了支持缩放并防止 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;
}
这就是我们做那件事的方式。