I wish to automate all of the steps involved in setting up a new Firebase project without any user interaction. I've accomplished most of the steps via either the gCloud CLI, Firebase CLI or the GoogleApis NodeJS library.
Authentication has been done via the CLI tools or via service accounts.
The only thing I haven't been able to do so far is adding Google Analytics to the newly created Firebase project. I have found this Google Api which should accomplish this, but I'm having problems authenticating the request.
How would I authenticate a request to this API without any user interaction? The API is not available via the CLI tools, so my best guess would be to use a service account with the owner IAM-role, but the request keeps failing.
My steps so far have been:
Ensuring that the management API is enabled
Add a service account to the GCloud project with owner privileges
Download the service account
Run the following code
import { google } from 'googleapis';
import * as fetch from 'node-fetch';
async function addGoogleAnalytics {
const token = await getJWTAcessToken();
await addAnalyticsFetch(token);
};
async function addAnalyticsFetch(accessToken) {
const url = `https://firebase.googleapis.com/v1beta1/projects/<my-project-id>:addGoogleAnalytics`;
const fetchResult = await fetch(url, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
json: true,
body: JSON.stringify({ analyticsAccountId: '<my-analytics-account-id>' }),
});
const fetchResultText = await fetchResult.text();
console.log('Fetch result: ', fetchResultText);
}
function getJWTAcessToken() {
const SCOPES = ['https://www.googleapis.com/auth/cloud-platform'];
const key = require('../../serviceAccount.json');
return new Promise((resolve, reject) => {
const jwtClient = new google.auth.JWT(key.client_email, null, key.private_key, SCOPES, null);
jwtClient.authorize((err, tokens) => {
if (err) {
reject(err);
return;
}
resolve(tokens.access_token);
});
});
}
The result of the API call is a simple 403 - "The caller does not have permission".
I've also attempted this using the GoogleApis NodeJS library with similar results.
If being a project owner doesn't give enough privileges, how do I permit this service account to perform this API call? I have a suspicion that I'm failing because the service account is in no way associated with the Google Analytics account, but the documentation doesn't list that as a requirement. It is also not listed as a step in Google's own guide.
It turns out that the above code is 100 % valid. The problem was indeed that the service account had enough privileges to edit the Firebase-project, but it had no authorization to create a new property for the Google Analytics account.
After giving the service account edit privileges for the Google Analytics account, the connection between Firebase and Google Analytics was successfully established. This process can be automated via this API.
Related
I'm having troubles to authenticate from Firebase Cloud Functions to a custom Node server.
Background:
I use a custom node + express server to receive calls from authenticated clients coming from our website. I can successfully verify their tokens using something like this:
const bearerHeader = req.headers['authorization'];
const bearerToken = bearerHeader.split(' ')[1];
await admin.auth().verifyIdToken( bearerToken );
What I need now:
I need to call one of this endpoints but from a Cloud Function (not from an authenticated web client).
So, on the cloud function I'm calling:
const admin = require('firebase-admin');
admin.initializeApp(); // regular initialization
const token = await admin.app().options.credential?.getAccessToken();
const config = { headers: { Authorization: `Bearer ${token.id_token}` } };
await axios.post(url, body, config);
The problem:
The token.id_token field is missing from getAccessToken():
// token:
{
access_token: 'ya29. ... ... ',
expires_in: 3599,
token_type: 'Bearer'
}
When locally run with other credentials (my user credentials when running firebase functions:shell, for example, or when setting GOOGLE_APPLICATION_CREDENTIALS to a service account credentials file) I do get an extra property id_token that I can use to authenticate. But when deployed and run on the Cloud Function, id_token property is empty.
Any ideas?
Thanks in advance.
PS: an extra problem... the local test with a service account do include id_token, but when authenticating to the server getAccessToken() fails with:
Firebase ID token has incorrect "aud" (audience) claim. Expected "<project-id>" but got "<some-hash>.apps.googleusercontent.com".
Maybe both problems solve the same way. But the server does work properly to verify user tokens coming from a website.
EDIT:
I had to exchange the access_token for an id_token, for which I followed something similar to this, with the projectId as audience.
Now the problem is about the issuer (iss) instead of the audience (aud):
Firebase ID token has incorrect "iss" (issuer) claim. Expected "https://securetoken.google.com/<project id>" but got "https://accounts.google.com".
I guess I could verify the token on the server using the same library, but then my web clients would have the same issuer problem
I'm trying to authenticate an API call made to my server (on Cloud Run) from a Cloud Scheduler cron job.
I'm trying to use a service account to this.
Note: this is all happening inside the same Project.
References:
https://cloud.google.com/scheduler/docs/http-target-auth
This is what I'm doing:
STEP 1 - Create the service account
I went to https://console.cloud.google.com/apis/credentials and created a new service account. I've assigned the role as owner.
STEP 2 - Create the cron job.
I went to https://console.cloud.google.com/cloudscheduler to create the cron job like I always do.
In the service account field I've put my service account e-mail. In the Audience field, I've put my project id because at some point I got an error saying that it was expecting it to be the name of my project id.
This was the error:
Firebase ID token has incorrect "aud" (audience) claim. Expected "PROJECT_ID"
STEP 3 - Running the job and identify decoding the token:
This is the code on my server:
import * as admin from "firebase-admin";
admin.initializeApp({
credential: admin.credential.cert(
// THIS IS THE DEFAULT FIREBASE-ADMIN SERVICE ACCOUNT
// THAT IS AUTOMATICALLY CREATED BY FIREBASE
SERVICE_ACCOUNT as admin.ServiceAccount
)});
// THIS IS THE CODE THAT IS INSIDE MY SERVER TRYING TO VERIFY THE SERVICE ACCOUNT
try {
const authHeader = req.headers.authorization;
console.log(`authHeader: ${authHeader}`);
if (authHeader) {
const idToken = authHeader.split(" ")[1]; // GETS THE USER ID TOKEN
const decodedToken = await admin.auth().verifyIdToken(idToken);
console.log(`decodedToken: ${decodedToken}`);
}
}
And this is the error I'm currently getting:
Firebase ID token has incorrect "iss" (issuer) claim. Expected "https://securetoken.google.com/"my-project-id" but got "https://accounts.google.com". Make sure the ID token comes from the same Firebase project as the service account used to authenticate this SDK. See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.
Is there anything wrong with the way I'm doing it? Should I not be using firebase-admin to this?
Should I be using google-auth-library to verify the token?
https://github.com/googleapis/google-auth-library-nodejs#verifying-id-tokens
https://developers.google.com/identity/sign-in/web/backend-auth
After a morning in hell trying to debug this, here is what I've found.
It seems that firebase-admin's admin.auth().verifyIdToken() only works for tokens generated from the firebase SDK.
I got it to work by using the google-auth-library directly.
I did the following.
NOTE: The rest of the code is the same (using the same service account as described in the question):
import { OAuth2Client } from "google-auth-library";
export const apiExpressRouteHandler: RequestHandler = async (req, res) => {
try {
const PROJECT_ID = process.env.PROJECT_ID;
const authHeader = req.headers.authorization;
if (authHeader) {
const client = new OAuth2Client(PROJECT_ID);
const ticket = await client.verifyIdToken({
idToken: authHeader.split(" ")[1],
audience: PROJECT_ID
});
// LOGGING ticket PROPERTIES
console.log(`userId: ${JSON.stringify(ticket.getUserId())}`);
console.log(`payload: ${JSON.stringify(ticket.getPayload())}`);
console.log(`envelope: ${JSON.stringify(ticket.getEnvelope())}`);
console.log(`attributes: ${JSON.stringify(ticket.getAttributes())}`);
}
// REST OF THE CODE
}
}
catch(err) {
// ...
}
I'm not sure if the PROJECT_ID is necessary to initialize the client with new OAuth2Client(PROJECT_ID); but it is working like this.
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,
},
}
);
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.
We have a large SPA using Firebase v2. We would like to upgrade to the new API, but we experience the following problem:
As the app is quite large, we have developed many integration tests, and for these tests we always need to reset the database and initialize it to a state, where some users exist. However, we found out there really is no such thing as creating a user on server anymore ( Firebase createUserWithEmailAndPassword method is undefined in node.js ), and we are quite unsure, how to upgrade the API and yet be able to reset and initialize the database from server.
Moreover, we are quite forced to do this upgrade, because we noticed that the Firebase v2, is still using the deprecated Graph API v2.0 for Facebook OAuth, and is not recommended for use after 8.8.2016. We understand that the Firebase v2 will probably not upgrade the calls to the Graph API, as the v2 is legacy. This, however, leaves us quite cornered for now.
Any help on this topic, please?
As of Firebase v3.3.0 you are able to create user accounts using Node, but the documentation isn't great on how to expose these methods.
In order to use the user management methods, you need to initialize an application in node using your Web API key, and not the Service Account config that is walked through in the setup guide.
// The Usual Service Account Init
// This will not contain any user management methods on firebase.auth()
this.app = firebase.initializeApp(
{
serviceAccount: 'path/to/serviceaccount/file.json',
databaseURL: 'https://mydbfb.firebaseio.com'
},
'MyAppName');
// Web Client Init in Node.js
// firebase.auth() will now contain user management methods
this.app = firebase.initializeApp(
{
"apiKey": "my-api-key",
"authDomain": "somedomain.firebaseapp.com",
"databaseURL": "https://mydbfb.firebaseio.com",
"storageBucket": "myfbdb.appspot.com",
"messagingSenderId": "SomeId"
},
'MyAppName');
You can grab your client api key from your Firebase console from the Web Setup guide
https://firebase.google.com/docs/web/setup
This is the only reference I could find that explicitly referenced the need to init with api key to get this to work.
https://groups.google.com/forum/#!msg/firebase-talk/_6Rhro3zBbk/u8hB1oVRCgAJ
Given below is a working example of creating Firebase user through Node.js
exports.addUser = function(req, res) {
var wine = req.body;
var email = req.body.email;
console.log(req.body);
var password = req.body.password;
var name = req.body.name;
console.log(“Creating user for -“+email+”-“+password);
var defaultAuth = admin.auth();
admin.auth().createUser({
email: email,
emailVerified: false,
password: password,
displayName: name,
disabled: false
})
.then(function(userRecord) {
console.log(“Created Firebase User successfully with id :”, userRecord.uid);
var wine = req.body;
wine.userId = userRecord.uid;
wine.timestamp = Date.now();
delete wine.password;
status = “201”;
var reply = JSON.stringify(wine);
db.collection(‘collname’, function(err, collection) {
collection.insert(wine, {safe:true}, function(err, result) {
if (err) {
wine.status = “200”;
wine.message = “An error occured”;
reply.set(‘status’,”201″);
res.status(201).send(wine);
} else {
console.log(‘Success: ‘ + JSON.stringify(result[0]));
status= “200”;
wine.status = “200”;
wine.message = “Account created Successfully”;
res.status(200).send(wine);
}
});
});
})
.catch(function(error) {
wine.message = “An error occured—“;
wine.status = “201”;
console.log(“User Creation onf Firebase failed:”, error);
res.status(201).send(wine);
});
}
For details you can see the following blog post
http://navraj.net/?p=53
Thanks