I built a Firebase HTTP Event function with Node and Express. The function is working, but when I invoke the function on the client side I get 403 Forbidden. The first time I invoked the function I was asked to sign in with a Google account. I signed in with the same account I use for Firebase, but when I invoked the function I got:
Screenshot of 403 error
I looked at the use roles on Google cloud platform and the permission to invoke the function is set to allUsers. I signed out and back in again in the Firebase CLI.
Here is the index.js in the functions folder:
const functions = require('firebase-functions');
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const port = process.env.port || 5600
const nodemailer = require('nodemailer');
app.use(express.static('Public'));
app.use(bodyParser.urlencoded({ extended: true }));
const urlencodedParser = bodyParser.urlencoded({extended: true});
app.post("/api/user", urlencodedParser, (req, res) => {
res.sendFile('../Public/bedankt.html', {root: __dirname})
const persGegevens = req.body
const string = JSON.stringify(persGegevens, (key, value) => {
if (typeof value === "string"){
return value.toUpperCase();
} else {
return value
}
}, 1);
var transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'gietvloermakers#gmail.com',
pass: 'Gietvloermakers2020!'
}
});
var mailOptions = {
from: 'gietvloermakers#gmail.com',
to: 'gvbeusekom84#hotmail.com',
subject: 'Nieuwe bestelling op Gietvloermakers',
html: string
};
transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
});
exports.app1 = functions.https.onRequest(app);
app.listen(port);
console.log(port);
Here is the html:
<form id="controlleer-form" action="/api/user" method="post" enctype="application/x-www-form-urlencoded">
<div class="controleer-div">
<h2>Uw bestelling</h2>
<p>Aantal m2</p>
<input class="controle-input" type="text" name="aantalM2" id="aantalM2" readonly>
<p>Kleur</p>
<input class="controle-input" type="text" name="kleur" id="kleur" readonly>
<p>Assistentie</p>
<input class="controle-input" type="text" name="assistentie" id="assistentie" readonly>
<p>Gereedschappen</p>
<input class="controle-input" type="text" name="gereedschappen" id="gereedschappen" readonly>
<p>Totale prijs</p>
<input class="controle-input" type="text" name="totale-prijs" id="totale-prijs" readonly>
<p id="andere-kleur">Bestelling aanpassen</p>
</div>
<div class="controleer-div">
<h2>Uw gegevens</h2>
<p>Voornaam</p>
<input type="text" name="voornaam" placeholder="Voornaam">
<p>Achternaam</p>
<input type="text" name="Achternaam" placeholder="Achternaam">
<p>Straatnaam en huisnummer</p>
<input type="text" name="Achternaam" placeholder="Straatnaam en huisnummer">
<p>Postcode</p>
<input type="text" name="Achternaam" placeholder="Postcode">
<p>Telefoonnummer</p>
<input type="tel" name="telefoonnummer" placeholder="Telefoonnummer">
<p>Emailadres</p>
<input type="email" name="email" placeholder="Emailadres"><br>
<input id="verzenden" type="submit">
</div>
</form>
Here is the firebase.json:
{
"hosting": {
"public": "Public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [{
"source": "**",
"function": "app1"
}]
}
}
I tried but I exhausted all possible solutions I've found online so far.
I encountered this recently. It turns out that as of January 15, 2020 new functions require authentication by default.
See the docs here for details.
The solution was to manually add the Cloud Functions Invoker permission to the allUsers user in the Cloud Functions page in the Google Cloud Console.
If you are getting 403 forbidden error like below
Error: Forbidden Your client does not have permission to get URL
/api/test from this server.
Please follow below steps to grant access to all users. Basically this is to allow unauthenticated clients to access your api endpoint.
Go to https://console.cloud.google.com/functions/list
Select the function to which you want to give public access
Click on PERMISSIONS
Click on ADD MEMBER
Type allUsers
Select role Cloud Functions -> Cloud Functions Invoker
Save
That's it, now test your api.
This has to do with permission access to your cloud functions http requests and cloud function events, you need to edit your cloud function IAM permission.
https://cloud.google.com/functions/docs/securing/managing-access-iam#allowing_unauthenticated_function_invocation
Had the same problem (was asked to login with my Google Account, then denied access). It turned out that functions do currently not work outside the default region. In my case, I had to make a change here:
exports.app = functions
.region('europe-west6') // does not work, delete this line
.https.onRequest(app);
Your code exports the express application as the Cloud Function app1 on this line:
exports.app1 = functions.https.onRequest(app);
In your screenshot, you have tried to access the non-existent app Cloud Function instead resulting in the 403 Forbidden response.
This means the correct URL to call from your client is
http://us-central1-gietvloermakers.cloudfunctions.net/app1/api/user
^^^^
(or you could change the name of the export to app)
Having a closer look at your source code, you should also remove the following lines. If you wanted to test your code you would instead use firebase serve.
const port = process.env.port || 5600
/* ... */
app.listen(port);
On the following lines, you also inject the body parser twice.
app.use(bodyParser.urlencoded({ extended: true })); // use this
const urlencodedParser = bodyParser.urlencoded({extended: true}); // or this, not both
app.post("/api/user", urlencodedParser, ...
In your code, you also have:
app.post("/api/user", urlencodedParser, (req, res) => {
res.sendFile('../Public/bedankt.html', {root: __dirname})
/* do some other stuff */
})
This is invalid for a Cloud Function, because as soon as the Cloud Function handler (your code) calls end(), redirect() or send(), the Cloud Function is allowed to be terminated at any time which means that your email may never be sent. To fix this you need to send the file last.
app.post("/api/user", urlencodedParser, (req, res) => {
/* do some other stuff */
res.sendFile('../Public/bedankt.html', {root: __dirname})
});
My last observation, is that the error may be caused by the folder Public not existing on the server. Based on your sendFile call, you are expecting that the folder "Public" is available to your deployed function but as it is not inside the functions folder, it will not be deployed with your code.
res.sendFile('../Public/bedankt.html', {root: __dirname})
As this file would also be accessible at your-domain.com/bedankt.html, we'll redirect to it. If you wanted to send the HTML content of this file instead, move it inside your deployed functions directory.
res.redirect('/bedankt.html')
Because you appear to be trying to use your express function behind Firebase hosting, we can trim your index.js file to the following:
const functions = require('firebase-functions');
const express = require('express');
const bodyParser = require('body-parser');
const nodemailer = require('nodemailer');
const apiApp = express();
apiApp.use(bodyParser.urlencoded({ extended: true }));
apiApp.post("/api/user", (req, res) => {
const persGegevens = req.body
const string = JSON.stringify(persGegevens, (key, value) => {
if (typeof value === "string"){
return value.toUpperCase();
} else {
return value
}
}, 1);
var transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'gietvloermakers#gmail.com',
pass: 'Gietvloermakers2020!'
}
});
var mailOptions = {
from: 'gietvloermakers#gmail.com',
to: 'gvbeusekom84#hotmail.com',
subject: 'Nieuwe bestelling op Gietvloermakers',
html: string
};
transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
res.redirect('/bedankt.html?success=0');
} else {
console.log('Email sent: ' + info.response);
res.redirect('/bedankt.html?success=1');
}
});
});
// note rename to api
exports.api = functions.https.onRequest(apiApp);
which requires updating your firebase.json file to:
{
"hosting": {
"public": "Public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [{
"source": "/api/**",
"function": "api"
}]
}
}
This configuration will first attempt to find a matching file in your Public directory. If it can't find a match, it will check if the requested path starts with /api and if so, launch your Cloud Function. If it still can't find a match, it will show your 404 page (or the built in one if it doesn't exist).
Related
The firebase Sveltekit client app and server api use a google cloud run hosting container. This works fine when I use the cloud run url: https://app...-4ysldefc4nq-uc.a.run.app/
But when I use firebase rewriting the client works fine using: https://vc-ticker.web.app/... but receives 502 and 504 responses from the API service. The cloud run log does not show any errors, receives the client fetch POST request and returns a Readablestream response.
But this API service response stream never arrives when using rewrites.
firebase.json
{
"hosting": {
"public": "public", !! NOT used, cloud run hosts the app
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"run": {
"serviceId": "vc-ticker-app",
"region": "us-central1"
}
}
]
}
}
+page.svelte client API request:
const logging = true;
const controller = new AbortController();
let reader = null;
const signal = controller.signal;
async function streamer(params) {
console.log("stream with logging:", logging, JSON.stringify(params));
try {
const response = await fetch("api/my-ticker", {
method: "POST",
body: JSON.stringify(params),
headers: {
"content-type": "application/json",
},
signal: signal,
});
const stream = response.body.pipeThrough(new TextDecoderStream("utf-8"));
reader = stream.getReader();
while (true) {
const { value, done } = await reader.read();
if (done || response.status !== 200) {
console.log("done response", response.status, done, value);
await reader.cancel(`reader done or invalid response: ${response.status}`);
reader = null;
break;
}
// response ok: parse multi json chunks => array => set store
const quotes = {};
JSON.parse(`[${value.replaceAll("}{", "},{")}]`).forEach((each, idx) => {
quotes[each.id] = [each.price, each.changePercent];
console.log(`quote-${idx}:`, quotes[each.id]);
});
positions.set(quotes);
}
} catch (err) {
console.log("streamer exception", err.name, err);
if (reader) {
await reader.cancel(`client exception: ${err.name}`);
reader = null;
}
}
}
$: if ($portfolio?.coins) {
const params = {
logging,
symbols: Object.values($portfolio.symbols),
};
streamer(params);
}
onDestroy(async () => {
if (reader) await reader.cancel("client destroyed");
controller.abort();
console.log("finished");
});
I use the Sveltekit adapter-node to build the app.
With rewrite rules, you can direct requests that match specific patterns to a single destination.Check your firebase.json file and verify if the rewrite configuration in the hosting section has the redirect serviceId name same as that from the deployed container image,as per below example
"hosting": {// ...
// Add the "rewrites" attribute within "hosting"
"rewrites": [ {
"source": "/helloworld",
"run": {
"serviceId": "helloworld", // "service name" (from when you [deployed the container image][3])
"region": "us-central1" // optional (if omitted, default is us-central1)
}
} ]
}
It is important to note that Firebase Hosting is subject to a 60-second request timeout. If your app requires more than 60 seconds to run, you'll receive an HTTPS status code 504 (request timeout). To support dynamic content that requires longer compute time, consider using an App Engine flexible environment.
You should also check the Hosting configuration page for more details about rewrite rules. You can also learn about the priority order of responses for various Hosting configurations.
I made it work with an external link to the cloud run api service (cors).
But I still do not understand why It can't be done without cors using only firebase rewrites.
+page.svelte client API request update:
Now using GET and an auth token to verify the api request on the endpoint server
const search = new URLSearchParams(params);
const apiHost = "https://fs-por....-app-4y...q-uc.a.run.app/api/yahoo-finance-streamer";
const response = await fetch(`${apiHost}?${search.toString()}`, {
method: "GET",
headers: {
"auth-token": await getIdToken(),
},
signal: signal,
});
And a handle hook to verify the auth token and handle cors:
const logging = true;
const reqUnauthorized = { status: 403, statusText: 'Unauthorized!' };
/** #type {import('#sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
let response;
if (event.request.method !== "OPTIONS") {
if (event.url.pathname.startsWith('/api')) {
const authToken = event.request.headers.get("auth-token")
const { error = null, decodedToken } = await decodeIdToken(logging, authToken)
if (error) return new Response(error.message, reqUnauthorized);
if (verifyUser(logging, decodedToken) === false) {
return new Response(`user auth failed for: ${decodedToken.email}`, reqUnauthorized);
}
}
response = await resolve(event);
} else { // handle cors preflight OPTIONS
response = new Response("", { status: 200 });
}
response.headers.append('Access-Control-Allow-Headers', "*");
response.headers.append('Access-Control-Allow-Origin', "*");
return response;
}
From firebase support:
I got an answer from the engineering team. Unfortunately Firebase Hosting does not support streaming responses at the moment. I’ve created a feature request so they will consider implementing it.
Please be informed that submitting a feature request doesn’t guarantee that it will be implemented. Keep an eye on the release notes.
I realize that this is not the answer you expected from me, but unfortunately there is nothing I can do about it.
I'm having some trouble with serving some dynamic content from Firebase Hosting.
I've written an http.onRequest() cloud function that returns an image (content-type: image/jpeg) as its response. The function works as expected if I access it directly at its url:
https://us-central1-my-project-id.cloudfunctions.net/hosting-getPartnerImg
Per the documentation, I am using the us-central1 region.
I would like to be able to invoke this function using Firebase Hosting as well, which I've configured as follows:
firebase.json (snippet)
"rewrites" : [
{
"source" : "/pimg",
"function" : "hosting-getPartnerImage"
}
],
"headers": [
{
"source": "/pimg",
"headers": [ {
"key": "Cache-Control",
"value": "max-age=60"
},
{
"key": "Access-Control-Allow-Origin",
"value": "*"
}
]
}]
index.js (snippet)
const functions = require('firebase-functions');
const admin = require('firebase-admin');
let firebaseDefaultConfig = JSON.parse(process.env.FIREBASE_CONFIG);
admin.initializeApp(firebaseDefaultConfig);
const fn = process.env.FUNCTION_NAME;
if(!fn || fn === 'hosting-getPartnerImg'){
exports.hosting = require('./hosting.js');
}
hosting.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
exports.getPartnerImg = functions.region("us-central1").https.onRequest((req, res) => {
const partnerId = req.query.partner;
const fileName = req.query.file;
res.set("content-type", "image/jpeg");
const bucket = admin.storage().bucket();
let file = bucket.file("partnerImgs/" + partnerId + "/" + fileName);
let readStream = file.createReadStream();
readStream.pipe(res);
});
This one has me stumped. Navigating to a URL like:
https://my-project-id.web.app/pimg?partner=BLAH&file=foo.jpg does not generate a Page Not Found as other URLs, so I'm reasonably confident that the rewrite is taking hold as it should. Be that the case though, why am I immediately taken to:
appengine.google.com/_ah/loginform... with the message:
** An application is requesting permission to your Google Account **
Can't HTTP onRequest cloud functions be used anonymously via Firebase Hosting? Why does the function work when I hit it directly, yet requests permission when I access it via the Firebase Hosting rewrite.
Any ideas would be appreciated.
The AppEngine login page behavior is what happens when the function being called doesn't exist (regardless of if it is via a rewrite or not).
Your problem is this rewrite:
"function" : "hosting-getPartnerImage"
vs the actual function name:
if(!fn || fn === 'hosting-getPartnerImg'){
or
exports.getPartnerImg = ...
Notably, the rewrite doesn't call the correct function.
You should change the rewrite to call hosting-getPartnerImg instead.
I'm trying to call functions from app but it doesn't work and I'm getting the following error from the console:
index.esm.js:402 OPTIONS https://us-central1-undefined.cloudfunctions.net/addMessage 404 ()
Failed to load https://us-central1-undefined.cloudfunctions.net/addMessage: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://MYWEBADDRESS' is therefore not allowed access. The response had HTTP status code 404. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
firebase.json:
{
"database": {
"rules": "database.rules.json"
},
"hosting": {
"public": "public",
"rewrites": [
{
"source": "**",
"function": "addMessage"
}
]
},
"functions": {
"predeploy": [
"npm --prefix $RESOURCE_DIR run lint"
],
"source": "functions"
}
}
index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.addMessage = functions.https.onCall((data, context) => {
// Message text passed from the client.
const text = data.text;
// Checking attribute.
if (!(typeof text === 'string') || text.length === 0) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError('invalid-argument', 'The function must be called with ' +
'one arguments "text" containing the message text to add.');
}
// Checking that the user is authenticated.
if (!context.auth) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
'while authenticated.');
}
// Saving the new message to the Realtime Database.
return admin.database().ref('/messages').push({
text: text
}).then(() => {
console.log('New Message written');
// Returning the sanitized message to the client.
return { text: sanitizedMessage };
}).catch((error) => {
// Re-throwing the error as an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError('unknown', error.message, error);
});
});
my script in index.html
var addMessage = firebase.functions().httpsCallable('addMessage');
addMessage({text: "messageText"}).then(function(result) {
var message = result.data.text;
console.log(message);
});
How I initialize Firebase:
<script src="https://www.gstatic.com/firebasejs/5.0.4/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.0.4/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.0.4/firebase-database.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.0.4/firebase-functions.js"></script>
<script>
// Initialize Firebase
var config = {
apiKey: "**",
authDomain: "***",
databaseURL: "***",
storageBucket: "***",
};
firebase.initializeApp(config);
var functions = firebase.functions();
</script>
I was having the same exact problem, found your question without an answer, but managed to figure it out in the end.
As #Doug Stevenson mentioned above in the comments, the problem is that the Cloud Functions URL you are seeing has undefined instead of your project ID as the last piece of the url subdomain.
The reason it is undefined is because your project ID is not part of your initial Firebase config object. Like me, you likely copied and pasted the starter snippet from Firebase for the JS SDK, but you did it before they started including the Project ID as part of it. For some reason, even though the project ID is now needed to construct the cloud functions URL, the SDK doesn't error / warn if you don't include it.
All you need to do is add the following field to your config object:
projectId: <YOUR_PROJECT_ID_HERE>
Then you should see the requests no longer 404.
I'm trying to add open graph meta tags dynamically to my index.html using firebase functions. I'm following this tutorial but because I'd like to allow users to share on their facebook I need to add open graph meta tags on-demand when user wants to share content.
So, it seems to me that running the function via rewrite in firebase.json is not best solution here and I'd like to call the function when user wants to share content to Facebook.
I try to use CORS but it seems I get the "Access-Control-Allow-Origin" error in the browser when I am sending my response back. If I redirect I have no issue. But I need to send the index.html that's been updated. How can I resolve this issue?
Here's my function:
const cors = require('cors')({ origin: true });
const fs = require('fs');
exports.handler = function(req, res) {
cors(req, res, () => {
let indexHTML = fs.readFileSync('./hosting/index.html').toString();
console.log(`#share-fb-formfeed inside cors`, indexHTML);
const ogPlaceholder = '<meta name="functions-insert-dynamic-og">';
res.set('Cache-Control', 'public, max-age=300, s-maxage=600');
res.status(200).send(indexHTML); //results in Access-Control-Allow-Origin
// res.redirect(`https://thekasis.com/feed`); //this works
});
}
Using Rewrites
I also removed cors in my function and here is my firebase.json but I'm not sure if running the Firebase function all the time would be a good idea. It would be ideal to run the function only on-demand when the user is sharing something and so I really like to be able to call the function client side on-demand.
{
"database": {
"rules": "database.rules.json"
},
"hosting": {
"public": "build/es5-bundled",
"rewrites": [{
"source": "**",
"function": "shareContentG-shareToFb"
}]
}
}
I have setup a custom domain with Firebase Hosting (eg. myapp.domain.com).
How can one redirect (or turn off) the default Firebase Hosting URL (eg. myapp.firebaseapp.com) so that the app is only accessible from the custom domain?
You cannot turn off the subdomain. Your app will always be available on https://myapp.firebaseapp.com and whatever custom domain you've set up.
To redirect people, you can add a canonical link to your HTML:
<link rel="canonical" href="http://myapp.domain.com/" />
Read more about that in Specify your canonical on the Google Webmaster Central Blog.
You could use Firebase Functions.
Free for 125K invocations/month - https://firebase.google.com/pricing
An example using Express middleware:
// functions/index.js
const functions = require('firebase-functions');
const express = require('express');
const url = require('url');
const app = express();
// Allowed domains
let domains = ['localhost:5000', 'example.com'];
// Base URL to redirect
let baseurl = 'https://example.com/';
// Redirect middleware
app.use((req, res, next) => {
if (!domains.includes(req.headers['x-forwarded-host'])) {
return res.status(301).redirect(url.resolve(baseurl, req.path.replace(/^\/+/, "")));
}
return next();
});
// Dynamically route static html files
app.get('/', (req, res) => {
return res.sendFile('index.html', { root: './html' });
});
// ...
// 404 middleware
app.use((req, res) => {
return res.status(404).sendFile('404.html', { root: './html' });
});
// Export redirect function
exports.redirectFunc = functions.https.onRequest(app);
The exported function name must be added to rewrites in firebase.json e.g.:
{
"hosting": {
"public": "public",
"rewrites": [
{
"source": "**",
"function": "redirectFunc"
}
]
}
}
In addition to specifying canonical link as mentioned in Frank van Puffelen's answer. We can also add front end JavaScript code to do the actual redirect like this without disclosing default url.
if (location.hostname.indexOf('custom.url') === -1) {
location.replace("https://custom.url");
}