Firebase Function - Client IP always Internal to Google - firebase

I am writing a Firebase Function, specifically for Dialogflow chatbot fulfillment. I am having trouble getting an accurate client IP address regardless of how I am testing it.
I've seen on various posts the various ways to read client IP, but they are either undefined or an internal Google IP from one of their data centers.
I've tried reading:
"x-forwarded-for" header
req.connection.remoteAddress
req.ip
req.ips (collection of all of them, there is only ever 1 in the collection)
Any help would be much appreciated. I am trying to log analaytics around user interactions, and right now the IPs are all incorrect.

I've tried the following code which is provided here:
const functions = require('firebase-functions');
const util = require('util');
exports.helloWorld = functions.https.onRequest((req, res) => {
// For Firebase Hosting URIs, use req.headers['fastly-client-ip']
// For callable functions, use rawRequest
// Some users have better success with req.headers['x-appengine-user-ip']
const ipAddress = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
const headers = JSON.stringify(req.headers, null, 2);
const message = util.format("<pre>Hello world!\n\nYour IP address: %s\n\nRequest headers: %s</pre>", ipAddress, headers);
res.send(message);
});
When tested (even with mobile data), it returned the public IP of the caller and not a Google Internal IP.
If try this, do you continue getting internal IPs?

Related

How to fix Firebase CORS errors in callable functions? [duplicate]

This question already has answers here:
Firebase Callable Function + CORS
(21 answers)
Closed 1 year ago.
I have a problem with Firebase and CORs, apparently it cannot reach the endpoint with errors like:
Access to fetch at
'https://europe-west2-XXX.cloudfunctions.net/fetchChatToken'
from origin 'https://trato.app' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested
resource. If an opaque response serves your needs, set the request's
mode to 'no-cors' to fetch the resource with CORS disabled.
service.ts:203
POST
https://europe-west2-XXX.cloudfunctions.net/fetchChatToken
net::ERR_FAILED (anonymous) # service.ts:203 ... ...
error.ts:66 Uncaught (in promise) Error: internal
at new t (error.ts:66)
at error.ts:175
at e. (service.ts:276)
at tslib.es6.js:100
at Object.next (tslib.es6.js:81)
at a (tslib.es6.js:71)
I also checked the network tab on dev inspector (chrome) to check if the CORS header is there, i dont see it.
Also, I have been checking firebase functions logs and apparently is not being even invoked, the last line showing is the deployment.
the way that Im using it is this:
Front End side:
const functions = firebaseApp.functions('europe-west2');
export const fetchChatToken = async () => (await functions.httpsCallable('fetchChatToken')()).data;
Functions (Backend) side:
const ensureAuthentication = auth => { if (!auth) throw new HttpsError("unauthenticated", "authentication required"); };
exports.fetchChatToken = functions.region("europe-west2").https.onCall((data, context) => {
ensureAuthentication(context.auth);
try {
const { AccessToken } = twilio.jwt;
const { ChatGrant } = AccessToken;
const grant = new ChatGrant({
serviceSid: conversationsid
});
const token = new AccessToken(accountsid, apikey, apisecret);
token.addGrant(grant);
token.identity = context.auth.uid;
return token.toJwt();
} catch (error) {
console.error(error);
throw new HttpsError("internal", "internal error");
} });
Unfortunately there many reasons possible for this CORS error. If the cloud function returns an "internal" error message it might be due to inconsistent Regions or errors in your cloud function code. My checklist for this error when creating a new cloud function:
Not matched Regions of Firestore-Project, Functions and Client side init cause a CORS Error
internal code errors inside the cloud functions cause this error
new function must be included in cloud function index file (if used)
cloud function name must match the string on client side invocation
delete cloud function in firebase dashboard before deploying new one after error
Make sure the function name referenced in the client is correct, see https://stackoverflow.com/a/62042554/1030246
I got it solved changing it to us, basically removing the region, taking out the 'europe-wes2' region from the function declaration and from the function call it works fine again.
I assume there is some error on the firebase side.

Secure firebase webhook

I have to create a webhook from typeform to firebase. I will create a cloud function listening to events sent from typeform. The typeform is managed by a third party.
The only issue I have, is the authorization part for the webhook. I understood (from reading different post) that anyone can "talk" to the cloud function URL. But I would like to have a secure and exclusive communication between typeform and firebase.
Any hints ?
Thank for your time.
You can definitively connect a Typeform webhook to a Cloud function and push data to Firebase storage.
In addition to authentication pointed by Frank, Typeform also provides a signature mechanism to ensure that the request comes from Typeform webhook.
Typeform lets you define a secret to sign the webhook payload.
When you receive the payload on your end, in the cloud function, you verify first if it's signed correctly, if it's not it means it's not coming from Typeform, therefore, you should not deal with it.
Here is an example to verify the webhook signature:
app.post('/typeform/webhook', async (request, response) => {
console.log('~> webhook received');
// security check, let's make sure request comes from typeform
const signature = request.headers['typeform-signature']
const isValid = verifySignature(signature, request.body.toString())
if (!isValid) {
throw new Error('Webhook signature is not valid, someone is faking this!');
}
//valid signature let's do something with data received
})
And here is the verifySignature function
const crypto = require('crypto')
const verifySignature = function(receivedSignature, payload){
const hash = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('base64')
return receivedSignature === `sha256=${hash}`
}
There are more details on Typeform documentation.
Hope it helps :)
Calling request.body.toString() does not work the way it is described in #Nicolas GreniƩs answer. The result will always be the string "[Object object]", as it only utilizes the default prototype as described here (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString).
A valid approach to stringify req.body would be to use JSON.stringify() which would still not deliver the expected result as you need to hash the original binary data (https://developer.typeform.com/webhooks/secure-your-webhooks/).
The Solution (without Firebase)
Use app.use(bodyParser.raw({ type: 'application/json' })) as specified here (Validate TypeForm Webhook payload in Node) to get the raw binary data and pass the request body directly into the hashing function.
const bodyParser = require("body-parser");
app.use(bodyParser.raw({ type: "application/json" })); // Notice .raw !
app.post("/typeform-handler", (req, res) => {
const hash = crypto
.createHmac('sha256', MY_TYPEFORM_SECRET)
.update(req.body) // Pass the raw body after getting it using bodyParser
.digest('base64')
})
The solution using Firebase
If you are using a Firebase Cloud Function to handle the request, you can't use bodyParser this way as Firebase already takes care of the parsing (https://firebase.google.com/docs/functions/http-events#read_values_from_the_request). Instead, use req.rawBody to access the raw body and pass it to the hash function.
// No need for bodyParser
app.post("/typeform-handler", (req, res) => {
const hash = crypto
.createHmac('sha256', MY_TYPEFORM_SECRET)
.update(req.rawBody) // Notice .rawBody instead of just .body
.digest('base64')
})
Remark for TypeScript users
The default Express Request object does not contain a rawBody property. Be aware that TypeScript therefore might throw an error of no overload matches this call or Property 'rawBody' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'. The actual Request object will however be provided by Firebase and will contain said properties. You can access the actual Request object type using functions.https.Request.

How to use multiple cookies in Firebase hosting + Cloud Run? [duplicate]

i followed the sample of authorized-https-endpoint and only added console.log to print the req.cookies, the problem is the cookies are always empty {} I set the cookies using client JS calls and they do save but from some reason, I can't get them on the server side.
here is the full code of index.js, it's exactly the same as the sample:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const express = require('express');
const cookieParser = require('cookie-parser')();
const cors = require('cors')({origin: true});
const app = express();
const validateFirebaseIdToken = (req, res, next) => {
console.log(req.cookies); //// <----- issue this is empty {} why??
next();
};
app.use(cors);
app.use(cookieParser);
app.use(validateFirebaseIdToken);
app.get('/hello', (req, res) => {
res.send(`Hello!!`);
});
exports.app = functions.https.onRequest(app);
store cookie:
curl http://FUNCTION_URL/hello --cookie "__session=bar" // req.cookies =
{__session: bar}
doesn't store:
curl http://FUNCTION_URL/hello --cookie "foo=bar" // req.cookies =
{}
If you are using Firebase Hosting + Cloud Functions, __session is the only cookie you can store, by design. This is necessary for us to be able to efficiently cache content on the CDN -- we strip all cookies from the request other than __session. This should be documented but doesn't appear to be (oops!). We'll update documentation to reflect this limitation.
Also, you need to set Cache-Control Header as private
res.setHeader('Cache-Control', 'private');
Wow this cost me 2 days of debugging. It is documented (under Hosting > Serve dynamic content and host microservices > Manage cache behavior, but not in a place that I found to be useful -- it is at the very bottom "Using Cookies"). The sample code on Manage Session Cookies they provide uses the cookie name session instead of __session which, in my case, is what caused this problem for me.
Not sure if this is specific to Express.js served via cloud functions only, but that was my use case. The most frustrating part was that when testing locally using firebase serve caching doesn't factor in so it worked just fine.
Instead of trying req.cookies, use req.headers.cookie. You will have to handle the cookie string manually, but at least you don't need to implement express cookie parser, if that's a problem to you.
Is the above answer and naming convention still valid? I can't seem to pass any cookie, to include a session cookie named "__session", to a cloud function.
I setup a simple test function, with the proper firebase rewrite rules:
export const test = functions.https.onRequest((request, response) => {
if (request.cookies) {
response.status(200).send(`cookies: ${request.cookies}`);
} else {
response.status(200).send('no cookies');
}
});
The function gets called every time I access https://www.xxxcustomdomainxxx.com/test, but request.cookies is always undefined and thus 'no cookies' is returned.
For example, the following always returns 'no cookies':
curl https://www.xxxcustomdomainxxx.com/test --cookie "__session=testing"
I get the same behavior using the browser, even after verifying a session cookie named __session was properly set via my authentication endpoint. Further, the link cited above (https://firebase.google.com/docs/hosting/functions#using_cookies) no longer specifies anything about cookies or naming conventions.

How to emulate request parameters when running a Firebase client locally

I've got a Google Dialogflow application I'm putting together that has a number of fulillments I need to process with external REST apis. I've got the client set up and working with firebase serve, enabling me to test locally. My index.js functions have the following siganture:
exports.clientEmployeeServiceCodes = functions.https.onRequest(async (req, res)=>{...});
But when I run locally any request parameter I pass through the url comes up as undefined. Here is an example.
http://localhost:5000/arc-caregiver-f8ec9/us-central1/clientEmployeeServiceCodes?phone=%22%%2b12123003939%22
But I get params when I call through Dialogflow. So my question is whether I'm doing someting wrong or if there's a way to emulate those params in my url call so the method behaves just as it would if I called it from Dialogflow.
Here is the example where I am getting the Dialogflow parameters via the req object. Note where I get the value of pin.
exports.userHours = functions.https.onRequest(async (req, res)=>{
const start = MyUtils.getDatePlus(-14);
const end = MyUtils.getDatePlus(14);
var view = 'API_Pay_Periods';
var form = 'Pay_Periods';
var criteria = 'Pay_Period_Date >= "'+start+'" %26%26 Pay_Period_Date <=
"'+end+'"'
const ret = await callRestAPI(Config,view,form,criteria);
const pp = getPayPeriod(ret);
const pin = req.query.pin
console.log('pin',pin)
const data = await RESTAPI(Config, view,form,criteria);
res.status(200).send(response);
....
res.end();
});
Dialogflow sends its fulfillment information via a POST to your webhook with the JSON object in the body of the request. You cannot duplicate this by sending query parameters in the URL itself.
One typical way to work with this is to setup an ngrok tunnel to your local environment and use it to record the JSON body that is sent. You can then use this same body for testing later.

How to get the user IP address in Meteor server?

I would like to get the user IP address in my meteor application, on the server side, so that I can log the IP address with a bunch of things (for example: non-registered users subscribing to a mailing list, or just doing anything important).
I know that the IP address 'seen' by the server can be different than the real source address when there are reverse proxies involved. In such situations, X-Forwarded-For header should be parsed to get the real public IP address of the user. Note that parsing X-Forwarded-For should not be automatic (see http://www.openinfo.co.uk/apache/index.html for a discussion of potential security issues).
External reference: This question came up on the meteor-talk mailing list in august 2012 (no solution offered).
1 - Without a http request, in the functions you should be able to get the clientIP with:
clientIP = this.connection.clientAddress;
//EX: you declare a submitForm function with Meteor.methods and
//you call it from the client with Meteor.call().
//In submitForm function you will have access to the client address as above
2 - With a http request and using iron-router and its Router.map function:
In the action function of the targeted route use:
clientIp = this.request.connection.remoteAddress;
3 - using Meteor.onConnection function:
Meteor.onConnection(function(conn) {
console.log(conn.clientAddress);
});
Similar to the TimDog answer but works with newer versions of Meteor:
var Fiber = Npm.require('fibers');
__meteor_bootstrap__.app
.use(function(req, res, next) {
Fiber(function () {
console.info(req.connection.remoteAddress);
next();
}).run();
});
This needs to be in your top-level server code (not in Meteor.startup)
This answer https://stackoverflow.com/a/22657421/2845061 already does a good job on showing how to get the client IP address.
I just want to note that if your app is served behind proxy servers (usually happens), you will need to set the HTTP_FORWARDED_COUNT environment variable to the number of proxies you are using.
Ref: https://docs.meteor.com/api/connections.html#Meteor-onConnection
You could do this in your server code:
Meteor.userIPMap = [];
__meteor_bootstrap__.app.on("request", function(req, res) {
var uid = Meteor.userId();
if (!uid) uid = "anonymous";
if (!_.any(Meteor.userIPMap, function(m) { m.userid === uid; })) {
Meteor.userIPMap.push({userid: uid, ip: req.connection.remoteAddress });
}
});
You'll then have a Meteor.userIPMap with a map of userids to ip addresses (to accommodate the x-forwarded-for header, use this function inside the above).
Three notes: (1) this will fire whenever there is a request in your app, so I'm not sure what kind of performance hit this will cause; (2) the __meteor_bootstrap__ object is going away soon I think with a forthcoming revamped package system; and (3) the anonymous user needs better handling here..you'll need a way to attach an anonymous user to an IP by a unique, persistent constraint in their request object.
You have to hook into the server sessions and grab the ip of the current user:
Meteor.userIP = function(uid) {
var k, ret, s, ss, _ref, _ref1, _ref2, _ref3;
ret = {};
if (uid != null) {
_ref = Meteor.default_server.sessions;
for (k in _ref) {
ss = _ref[k];
if (ss.userId === uid) {
s = ss;
}
}
if (s) {
ret.forwardedFor = ( _ref1 = s.socket) != null ?
( _ref2 = _ref1.headers) != null ?
_ref2['x-forwarded-for'] : void 0 : void 0;
ret.remoteAddress = ( _ref3 = s.socket) != null ?
_ref3.remoteAddress : void 0;
}
}
return ret.forwardedFor ? ret.forwardedFor : ret.remoteAddress;
};
Of course you will need the current user to be logged in. If you need it for anonymous users as well follow this post I wrote.
P.S. I know it's an old thread but it lacked a full answer or had code that no longer works.
Here's a way that has worked for me to get a client's IP address from anywhere on the server, without using additional packages. Working in Meteor 0.7 and should work in earlier versions as well.
On the client, get the socket URL (unique) and send it to the server. You can view the socket URL in the web console (under Network in Chrome and Safari).
socket_url = Meteor.default_connection._stream.socket._transport.url
Meteor.call('clientIP', socket_url)
Then, on the server, use the client's socket URL to find their IP in Meteor.server.sessions.
sr = socket_url.split('/')
socket_path = "/"+sr[sr.length-4]+"/"+sr[sr.length-3]+"/"+sr[sr.length-2]+"/"+sr[sr.length-1]
_.each(_.values(Meteor.server.sessions), (session) ->
if session.socket.url == socket_path
user_ip = session.socket.remoteAddress
)
user_ip now contains the connected client's IP address.

Resources