Firebase Cloud Functions execution time is so fluctuate - firebase

I got this from google cloud console. I don't understand why after the function is executed, it should turn hot for some duration. However, after deploying the function and call it, I found cold start and then drop to actual and then increase to cold start again. Please help!
// index.js
import * as functions from 'firebase-functions'
import express from 'express'
import cors from 'cors'
import auth from 'controllers/auth'
const authApp = express()
authApp.use(cors({ origin: true }))
authApp.use(auth)
authApp.use('*', unknownPathHandler, errorMiddleware)
const authApi = functions.https.onRequest(authApp)
exports.auth = authApi
// controllers/auth.js
app.post('/user', verifySecretKey, (req, res, next) => {
const { email, password } = req.body
return appFirebase.auth().signInWithEmailAndPassword(email, password)
.then(() => {
return appFirebase.auth().currentUser.getIdToken().then((token) => {
return res.end(token)
})
})
.catch((err) => next(err))
})
firebase version
"firebase-admin": "5.12.0",
"firebase-functions": "1.0.2",

There is potential for a lot of variance in you function. It's doing all of the following tasks, none of which have guarantees how long they will take:
Using Firebase Auth to effectively sign in a user
Fetching an ID token for that user
Sending a response to the client, wherever in the world they are, with whatever connection speed.
If you want to understand the performance characteristics of your function, you should profile each one of these steps. The sending of the response may not be possible to benchmark, and could be highly variable based on their physical location and connection speed.
If you have solid benchmarks that suggest that Cloud Functions is underperforming compared to expectations, please send those to Firebase support. https://firebase.google.com/support/

Related

Accessing Google Cloud Function from Vercel Serverless Function

I am seeking the best manner in which this should be done.
I have a https based GCF Function such as:
// google function
exports.someFunction = async (req, res) => {
try {
...
// some logic and access
res.status(200).send(data)
}
catch(error) {
res.status(400).send(error.message)
}
}
The API serverless function in Next.js is using axios. Is that the recommended method?
// next.js pages/api/call-google-func.js
async function handler(req, res) {
try {
const url = '....' //https://gcp-zone-project-xx834.cloudfunctions.net/someFunc
const res = await axios.get(url)
const resdata = res.data
res.status(200).send(resdata)
}
catch(error) {
res.status(400).send(error)
}
}
The problem with this method is that the GCF must have public access. How can we set up to access the GCF from Next.js by passing credentials as environment variables. Thanks
I think for this situation where a Vercel Serverless Function must communicate with the outside world, a Google Cloud Function, you'd want to create a JWT token on Vercel's side to pass to Google's side which you would then need to verify. I think Exchanging a self-signed JWT for a Google-signed ID token would be what you need.
Since either side doesn't know about the other, Google's IAM normal cloud privileges for allowing GCG<>GCF communication wouldn't apply here.

How to solve "API resolved without sending a response fetch" when using sentry

I've looked at countless other posts and cannot find the answer to this, why am I continuously getting the API resolved without sending a response for /api/git/latest-commit, this may result in stalled requests. error in next.js? As soon as I disable sentry it goes away, has anyone else struggled with this?
import type { NextApiRequest, NextApiResponse } from 'next'
import { withSentry } from "#sentry/nextjs";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const response = await fetch(`https://api.github.com/repos/####/####/commits?per_page=1`, {
method: 'GET'
});
const data = await response.json();
const commit = data[0]
res.status(200).json({
sha: {
full: commit.sha,
short: commit.sha.substring(0,7)
},
committer: commit.commit.committer.name,
time: commit.commit.committer.date,
html_url: commit.html_url
})
};
export default withSentry(handler);
Running your code produced the following message on my end (next 12.1.4, #sentry/nextjs 6.19.7):
[sentry] If Next.js logs a warning "API resolved without sending a response", it's a false positive, which we're working to rectify.
In the meantime, to suppress this warning, set SENTRY_IGNORE_API_RESOLUTION_ERROR to 1 in your env.
To suppress the nextjs warning, use the externalResolver API route option (see https://nextjs.org/docs/api-routes/api-middlewares#custom-config for details).
To suppress the warning from Sentry, I added this environment variable to an .env.development file:
SENTRY_IGNORE_API_RESOLUTION_ERROR=1
To suppress the warning from the Next.js API route, I added this to latest-commit.ts:
// ...
export const config = {
api: {
externalResolver: true,
},
};
export default withSentry(handler);
Both warnings no longer appear and the data appears to return correctly.
After some digging, this was their explanation as to what's happening:
https://github.com/getsentry/sentry-javascript/pull/4139
In dev, nextjs checks that API route handlers return a response to the client before they resolve, and it throws a warning if this hasn't happened. Meanwhile, in withSentry(), we wrap the res.end() method to ensure that events are flushed before the request/response lifecycle finishes.
As a result, there are cases where the handler resolves before the response is finished, while flushing is still in progress.

Call Cloud Run from Cloud Function: IAM Authentication

I've deployed a small HTTP endpoint via Google Cloud Run. It is working fine when I turn off the authentication.
I now want to turn it on so that it is only callable by my Firebase Cloud Function. If I understand it right, I just have to add the correct service account mail address in the IAM settings of the Cloud Run as "Cloud Run invoker".
But which address is the correct one?
I've tried all addresses that I have found in Firebase Console -> Project Settings -> Service Accounts.
I think you can check the specific firebase function. In the UI, the service account used should be listed.
By default, GCF functions all use <project_id>#appspot.gserviceaccount.com
Thanks to #AhmetB - Google and #whlee's answer I got it working. Basically it is enough adding an Authorization Bearer token to the request, which you can get from a special endpoint: https://cloud.google.com/run/docs/authenticating/service-to-service#nodejs
Then you just have to add the service account of the function to the IAM list of the Cloud Run container: <project_id>#appspot.gserviceaccount.com
The nodejs example is using the deprecated request library, so here is my version using axios:
const getOAuthToken = async (receivingServiceURL: string): Promise<string> => {
// Set up metadata server request
const metadataServerTokenURL = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=';
const uri = metadataServerTokenURL + receivingServiceURL;
const options = {
headers: {
'Metadata-Flavor': 'Google'
}
};
return axios.get(uri, options)
.then((res) => res.data)
.catch((error) => Promise.reject(error));
}
Then you can just use the token in the actual request:
const url = `...`;
const token = await getOAuthToken(url);
axios.post(url, formData, {
headers: {
Authorization: `Bearer ${token}`,
}
}).then(...).catch(...);
#luhu 's answer was really helpful. I'd like to add just one note for those whose are willing to test with the emulators locally first. The metadata server (which is actually http://metadata.google.internal now) as they state
does not work outside of Google Cloud, including from your local machine.
As a workarund, you can use the google-auth-library and then get the token directly if you prefer sticking with axios. Remember to set the GOOGLE_APPLICATION_CREDENTIALS env variable pointing to a service account secret first as it's the only way to make it work (I've tested setting the credential field during admin.initializeApp() but didn't seem to like it).
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();
const url_origin = '....'
const client = await auth.getIdTokenClient(url_origin);
const token = (await client.getRequestHeaders()).Authorization;
const url = '....'
const response = await axios.get(
url,
{
headers: {
Authorization: token,
},
}
);

Call Google Play Developer API from Firebase Functions

I am trying to develop a server-side validation of my users' in-app purchases and subscriptions as recommended, and I want to use Firebase Functions for that. Basically it has to be an HTTP trigger function that receives a purchase token, calls the Play Developer API to verify the purchase, and then does something with the result.
However, calling many of the Google APIs (including Play Developer API) requires non-trivial authorization. Here's how I understand the required setup:
There has to be a GCP project with Google Play Developer API v2 enabled.
It should be a separate project, since there can be only one linked to Play Store in the Google Play Console.
My Firebase Functions project must somehow authenticate to that other project. I figured that using a Service Account is most suitable in this server-to-server scenario.
Finally, my Firebase Functions code must somehow obtain authentication token (hopefully JWT?) and finally make an API call to get a subscription status.
The problem is that absolutely no human-readable documentation or guidance on that is existent. Given that ingress traffic in Firebase is included in the free plan (so I assume they encourage using Google APIs from Firebase Functions), that fact is pretty disappointing. I've managed to find some bits of info here and there, but having too little experience with Google APIs (most of which required simply using an api key), I need help with putting it together.
Here's what I figured out so far:
I got a GCP project linked to the Play Store and with the API enabled. For some reason though, trying to test it in APIs Explorer results in an error "The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console".
I made a Service Account and exported a JSON key, which contains the key to produce a JWT.
I also set up read permissions for that Service Account in Play Console.
I found a Node.JS client library for Google APIs, which is in alpha and has very sparse documentation (e.g. there's no obvious documentation on how to authenticate with JWT, and no samples on how to call the android publisher API). At the moment I'm struggling with that. Unfortunately I'm not super-comfortable with reading JS library code, especially when the editor doesn't provide the possibility to jump to highlighted functions' sources.
I'm pretty surprised this hasn't been asked or documented, because verifying in-app purchases from Firebase Functions seems like a common task. Has anyone successfully done it before, or maybe the Firebase team will step in to answer?
I figured it out myself. I also ditched the heavyweight client library and just coded those few requests manually.
Notes:
The same applies to any Node.js server environment. You still need the key file of a separate service account to mint a JWT and the two steps to call the API, and Firebase is no different.
The same applies to other APIs that require authentication as well — differing only in scope field of the JWT.
There are a few APIs that don't need you to exchange the JWT for an access token — you can mint a JWT and provide it directly in Authentication: Bearer, without a round trip to OAuth backend.
After you've got the JSON file with the private key for a Service Account that's linked to Play Store, the code to call the API is like this (adjust to your needs). Note: I used request-promise as a nicer way to do 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 are the docs I followed.
I think I found a slightly quicker way to do this... or at least... more simply.
To support scaling and keep index.ts from growing out of control... I have all the functions and globals in the index file but all the actual events are handled by handlers. Easier to maintain.
So here's my index.ts (I heart type safety):
//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!
Now... for the show!
// 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);
}
}
}
There are few model classes that help:
export class SubscriptionMessageEvent {
version: string;
packageName: string;
eventTimeMillis: number;
subscriptionNotification: SubscriptionNotification;
testNotification: TestNotification;
}
export class SubscriptionNotification {
version: string;
notificationType: number;
purchaseToken: string;
subscriptionId: string;
}
So that's how we do that thing.

Firebase : How to secure content sent without login?

I'm building a hybrid mobile app with Firebase as my backend. I want to let users post on a wall any message they want without authentication, but I feel concerned about spam possibilities. I mean, if users don't have to be authenticated to be able to post, my security rules are basically empty and anyone who gets the endpoint can post an infinite amount of content. And I don't see what I could do against it.
So I know about anonymous auth, but I'm not sure if it really fix the issue. The endpoint remains open, after all, just behind the necessity to call a method before. It adds a little complexity but not much, I think.
What I wonder is if there is a possibility to check for the call origin, to make sure it comes from my app and nothing else. Or, if you have another idea to get this more secure, I'm open to everything. Thanks!
You can accomplish this using a combination of recaptcha on the client, and firebase cloud functions on the backend.
You send the message you want to add to the store along with the captcha to the cloud function. In the cloud function, we first verify the captcha. If this one is ok, we add the message to the store. This works, because when adding items to the store via a cloud function, firebase authentication rules are ignored.
Here's an example cloud function:
const functions = require('firebase-functions')
const admin = require('firebase-admin')
const rp = require('request-promise')
const cors = require('cors')({
origin: true,
});
admin.initializeApp();
exports.createUser = functions.https.onRequest(function (req, res) {
cors(req, res, () => {
// the body is a json of form {message: Message, captcha: string}
const body = req.body;
// here we verify whether the captcha is ok. We need a remote server for
// for this so you might need a paid plan
rp({
uri: 'https://recaptcha.google.com/recaptcha/api/siteverify',
method: 'POST',
formData: {
secret: '<SECRET>',
response: body.captcha
},
json: true
}).then(result => {
if (result.success) {
// the captcha is ok! we can now send the message to the store
admin.firestore()
.collection('messages')
.add(body.message)
.then(writeResult => {
res.json({result: `Message with ID: ${writeResult.id} added.`});
});
} else {
res.send({success: false, msg: "Recaptcha verification failed."})
}
}).catch(reason => {
res.send({success: false, msg: "Recaptcha request failed."})
})
});
})
And here's some more info: https://firebase.googleblog.com/2017/08/guard-your-web-content-from-abuse-with.html

Resources