Verifying reCAPTCHA v3 in Firebase Function causes CORS Issue - firebase

I have the following codes that verify Google reCAPTCHA v3 in my Firebase Function that caused the CORS issue:
const functions = require('firebase-functions');
const nodemailer = require("nodemailer");
const express = require("express");
const cors = require("cors");
const request = require('request');
const serverApi = express();
api.use(cors({ origin: true }));
function verifyCaptcha(token, returnData) {
// Put your secret key here.
var secretKey = functions.config().recaptcha.secretkey;
var verificationUrl = "https://www.google.com/recaptcha/api/siteverify?secret=" + secretKey + "&response=" + token;
// Note here: External network call to google.com
request(verificationUrl, function (error, response, body) {
body = JSON.parse(body);
// Success will be true or false depending upon captcha validation.
if (!body.success) {
body['status'] = false;
body['errSource'] = "recaptcha";
body['message'] = "Failed to pass captcha verification.";
} else {
body['status'] = true;
body['message'] = "Successfully passed captcha verification!";
};
console.log(`Google returns: ${JSON.stringify(body)}`);
returnData(body);
});
};
api.post("/api/service-name", (req, res) => {
if (!req.body['g-recaptcha-response']) {
return res.send({ "status": false, "errSource": "recaptcha", "message": "Client-side reCAPTCHA token not found." });
};
const recaptchaToken = req.body['g-recaptcha-response'];
verifyCaptcha(recaptchaToken, function (result) {
if (result.status == false) {
return res.send(result);
};
// My business logics here.
});
});
exports.api = functions.https.onRequest(api);
I noticed that after removing the reCAPTCHA v3 verification request in within my Firebase Function, no more CORS issue for my localhost to call "/api/service-name" using $.ajax(). This is because the following Firebase Function log reminded me of the "External network is not accessible":
Billing account not configured. External network is not accessible and quotas are severely limited.
Configure billing account to remove these restrictions
My question is: Is there a way to get my server-side reCAPTCHA verification to work without causing this CORS issue, which could be prevented by "Billing account not configured"? Thanks!
UPDATE:
After catching the request() error that does the verification, I get the following error:
{errno: "EAI_AGAIN", code: "EAI_AGAIN", syscall: "getaddrinfo", hostname: "www.google.com", host: "www.google.com", …}
Also, after handling this error, no more CORS issue, but reCAPTCHA still cannot be verified. Any idea what causes this? Thanks again!

It's now confirmed that the above issue has been resolved after Enable Billing at the Google Cloud Console. It is NOT actually the CORS issue between the localhost and Firebase Functions/Hosting (although the Chrome browser returned as CORS related error message), it's actually the HTTP Request from the Firebase Function to the Google reCAPTCHA api during token verification process. Due to billing account not linked to the Firebase Project where the function sits in, any requests from any Firebase Functions to any External Network Resources, including Google reCAPTCHA, will be rejected with the following errors:
HTTP Request Error:
{errno: "EAI_AGAIN", code: "EAI_AGAIN", syscall: "getaddrinfo", hostname: "www.google.com", host: "www.google.com", …}
After enabling billing at GCP and linking the billing account to the specific Firebase Project, the request to Google reCAPTCHA verification will be successful (if the token is valid) without the above error. However, your FREE Spark Tier Firebase account will be AUTOMATICALLY UPGRADED to Blaze Plan -- Pay as you go.

Related

Authenticate *from* Firebase Cloud Functions to custom Node Server

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

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,
},
}
);

Programmatically add Google Analytics to Firebase project

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.

Firebase client error: Custom token corresponds to a different audience

I'm using the Firebase Python AdminSDK to generate a custom token which a Javascript client uses to sign in to Firebase. When the JS client tries to authenticate with the custom token it gets the error "Custom token corresponds to a different audience".
The code given with the error: 'auth/custom-token-mismatch'.
Many Google'd answers regarding the "audience" mismatch reference Analytics. But I'm doing a Web project, not iOS or Android, so I can't use Analytics to manage audiences.
The SO answers I've read are listed at the end, below.
I captured the custom token and plugged it in to https://jwt.io/ and both the values and the instanciation/expiration times (an hour apart) look good:
Decoded custom token on jwt.io:
{
"claims": {},
"uid": "<myuniqueID",
"sub": "firebase-adminsdk-1knpr#firebase-<myproject>.iam.gserviceaccount.com",
"iss": "firebase-adminsdk-1knpr#firebase-<myproject>.iam.gserviceaccount.com",
"iat": 1540153710,
"aud": "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
"exp": 1540157310
}
Python server:
def getFirebaseInstance(): # For Firebase Python SDK
try:
currentDir_path = os.path.dirname(os.path.realpath(__file__))
cred = credentials.Certificate(currentDir_path + '/includeFirebaseServiceAccounts/firebase-<myprojectname>-firebase-adminsdk-1knpr-e1244dd261.json')
firebaseAdmin = firebase_admin.initialize_app(cred, { 'databaseURL': 'https://<myprojectname>.firebaseio.com', 'databaseAuthVariableOverride': {'uid':'<myuniqueServerID>'}})
if firebaseAdmin:
return(firebaseAdmin)
except:
raise
def firebaseClientToken(request):
try:
uid = "<myuniqueClientID>" # case sensitive
additional_claims = { }
token = auth.create_custom_token(uid,additional_claims)
return HttpResponse(token)
except Exception as err:
return HttpResponse("System error:" + str(err), status=406)
Javascript client:
(
function authClient2Firebase() {
$.ajax({
url: "firebaseClientToken/",
method: "POST",
success: function(response) { step2(response); },
error: function(xhr) { alert("There was an error loading a security check. Have you lost your internet connection? Detail:" + xhr.responseText); }
});
function step2(customToken) {
try {
firebase.auth().signInWithCustomToken(customToken).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
alert("There was an error with the secure login. \n\nDetail: " + errorMessage + '\nCode: ' + errorCode);
});
}
catch(err) {
alert(err);
}
console.log("authClient2Firebase.js: Firebase login succeeded!");
}
}
)();
My project under the Console "Settings" page does have a Web API key, but I don't see anywhere that it's used.
There's only one user, me, under the Console's "Settings"->"Users and Permissions" page.
There's only one service account listed on the Console "Settings"->"Service Accounts" page. I tried deleting all secrets on that page, generating a new one, then generating and installing a new blue-button "secret" (bad name, actually it generates a whole json credential object).
These are the domains listed in Console "Authentication" -> "Sign-in Method":
localhost Default
<myproject>.firebaseapp.com Default
127.0.0.1 Custom
auth.firebase.com Custom
The actual domain I'm using is localhost:8000, which can't be entered here.
SO answers consulted unsuccessfully:
The custom token corresponds to a different audience
(I'm not using a key, except what's stored in the ServiceAccount
credentials.)
Firebase token error, "The custom token corresponds to a different audience."
Firebase custom auth issue token different audienceenter
link description here (Close, but I'm not using a Node server and
not sure what he means by server "must belong to the same project"
since the Python server isn't registered in any way except through the
ServiceAccount credentials which I downloaded.)
Embarrassing but true, it turned out to be a simple oversight. When the JS client initialized itself as a Firebase app, before authenticating, it was using old credentials from a test environment.
// Initialize Firebase
var config = {
apiKey: "<WebAPI from Firebase console, 'Project Settings'>",
authDomain: "<myproject>.firebaseapp.com",
databaseURL: "https://<myproject>.firebaseio.com",
projectId: "<myproject>",
storageBucket: "<myproject>.appspot.com",
messagingSenderId: "<id from Console Project Settings>" // optional
};
firebase.initializeApp(config);

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