Google Storage - signed url Request had insufficient authentication scopes - firebase

I'm using the google cloud nodejs storage library to upload some images to cloud storage. This all works fine. I'm then trying to generate a signed URL immediately after uploading the file, using the same storage object that uploaded the file in the first place but I receive the following error:
Request had insufficient authentication scopes
I'm not sure why this would be happening if it's all linked to the same service account that uploaded in the first place. (For what it's worth it's a firebase app).
The code is below:
const Storage = require('#google-cloud/storage');
storage = new Storage();
storage.bucket(bucketName).upload(event.file.pathName, {
// Support for HTTP requests made with `Accept-Encoding: gzip`
gzip: true,
destination: gcsname,
metadata: {
// Enable long-lived HTTP caching headers
// Use only if the contents of the file will never change
// (If the contents will change, use cacheControl: 'no-cache')
cacheControl: 'public, max-age=31536000'
},
}).then(result => {
let url = `https://storage.googleapis.com/${bucketName}/${gcsname}`;
const options = {
action: 'read',
expires: Date.now() + 1000 * 60 * 60, // one hour
};
// Get a signed URL for the file
storage.bucket(bucketName).file(gcsname).getSignedUrl(options).then(result => {
console.log("generated signed url", result);
}).catch(err => {
console.log("err occurred", err)
})
})
The bucket itself isn't public and neither are the objects, but it's my understanding that I should still be able to generate a signed url. The app itself is running on GCP compute engine, hence not passing any options to the new Storage() - passing options in fact also makes the upload fail.
Can anyone advise on what I'm doing wrong?

With the limited amount of information I have, here's a few things that you could be missing based on the error you are receiving:
The Identity and Access Management (IAM) API must be enabled for the project
The Compute Engine service account needs the iam.serviceAccounts.signBlob permission, available to the "Service Account Token Creator" role.
Additionally, you can find more documentation regarding the topic here.
https://cloud.google.com/storage/docs/access-control/signed-urls
https://cloud.google.com/storage/docs/access-control/signing-urls-manually

Related

Handle Firebase client-side token and access to protected pages

I'm using Firebase auth to login with Facebook, Google and email/pass. Basically, everything runs client-side, I make a call to Firebase and I receive an object containing an access token (that is a JWT ID Token), a customer id and its email. When I get this object, I put it into a persistent store (local storage, I know it's bad) and I perform an API call to one of my sveltekit endpoint that will in turn make another API call to a backend API (written in Go) to get all the user informations: firstname, lastname, phone, address, stats etc. To give a little bit of context, below is a diagram to illustrate what's happening when a user sign-in using Facebook.
Up to now, I just put the Firebase object into a store and just check if the information are there to allow access to a particular page. This check is done in the +layout.svelte page of the directory containing the page to be protected. It looks like something like this:
onMount(() => {
// redirect if not authenticated
if (browser && !$authStore?.uid) goto(`/auth/sign-in`);
});
It's probably not a good thing especially since my store persists in the local storage and therefore is prone to some javascript manipulation.
From my understanding, there's at least 2 things that could be better implemented but I may be wrong:
Set the access token in an httponly cookie straight after receiving it from Firebase. This would avoid storing the access token (that is a JWT) in the local storage and It could be used to authorize access or not to some protected pages...
I receive the Firebase authentication object on client-side buthttp only cookie can only be created from server side. I thought about sending the object as a payload to a POST sveltekit endpoint (like /routes/api/auth/token/+server.js) and set the cookie from here but apparently cookies is not available in a sveltekit endpoint.
So, how and where should I set this cookie ? I see that cookies is available in the load function of a +layout.server.js file, as well as in the handle function of a hooks.server.js file, but I don't see the logic here.
Populate locals.userwith the authenticated user once I've performed a call to my backend. Well, here, it's not obvious to me because I think point 1) would be enough to manage access to protected pages, but I see that a check of locals.user is something I've seen elsewhere.
I tried to set locals.user in the sveltekit endpoint that is making the API call to the backend API:
// /routes/api/users/[uid]/+server.js
import { json } from "#sveltejs/kit";
import axios from "axios";
import { GO_API_GATEWAY_BASE_URL } from "$env/static/private";
export async function GET({ request, locals, params }) {
try {
const config = {
method: "get",
baseURL: GO_API_GATEWAY_BASE_URL,
url: `/users/${params.uid}`,
headers: {
Authorization: `Bearer ${uidToken}`, // <-- the Firebase ID Token
},
withCredentials: true,
};
const res = await axios(config);
// set locals
locals.user = json(res.data); // <--- DOESN'T SEEM TO WORK
return json(res.data);
} catch (error) {...}
}
...but in the +layout.server.js page that I've created I see nothing:
// routes/app/protected_pages/+layout.server.js
import { redirect } from "#sveltejs/kit";
export function load({ locals }) {
console.log(locals); // <----- contains an empty object: {}
if (!locals.user) throw redirect(302, "/auth/sign-in");
}
Thank you so much for your help

403 Error when using generated Sas token to display blobs from Azure blob storage

I've been trying to display images from Azure blob storage on my web app for a while now.
My storage account SAS token is:
?sv=2021-06-08&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2022-12-09T08:03:09Z&st=2022-11-09T08:03:09Z&spr=https&sig=SIGNATURE_HERE
This SAS token includes all permissions and allows all resource types and services.
To generate a SAS token to view a blob, I go through the following steps:
1. Getting the blobService:
const blobService = new
BlobServiceClient(https://${storageAccountName}.blob.core.windows.net/?${storageAccountSasToken});
2. Creating a containerClient:
const containerClient = blobService.getContainerClient(containerName);
3. creating a sasOptions object:
const sasOptions = {containerName: containerName, blobName: blobName, startsOn: sasStartTime, expiresOn: sasExpiryTime, permissions: "racwdt" as unknown as BlobSASPermissions};
4. Generating SAS token with the parameters:
generateBlobSASQueryParameters(sasOptions, sharedKeyCredential).toString();
5. Sending the blobURL (with the SAS token attached) back to the user:
const blobURL = containerClient.getBlockBlobClient(blobName).url;
The problem is, when using the blobURL as src for my Image tag, I get a 403 (forbidden) error:
Server failed to authenticate the request. Make sure the value of
Authorization header is formed correctly including the signature.
the faulty blobURL in question:
https://mywebsite.blob.core.windows.net/container/profilePictures%2Fpicture.png?sv=2021-06-08&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2022-12-09T08:03:09Z&st=2022-11-09T08:03:09Z&spr=https&sig=CITlY0uPxBCGdBeMtIxxJafJM61HQlhooR5ZnDiPHuE%3D
The Error:
AuthenticationFailed
Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature. RequestId:df81f724-f01e-000e-593e-f41f7f000000 Time:2022-11-09T13:24:08.3305270Z
Signature did not match. String to sign used was STORAGE_ACCOUNT_NAME racwdt bfqt sc 2022-11-09T12:31:47Z 2022-12-09T20:31:47Z https 2021-06-08
Additional information:
The sasToken env variable includes "?" at the start of the string
All containers are PRIVATE.
My storage account is only accessible through a specific virtual network
My website's domain is listed on "Allowed Origins" in CORS tab, as well as localhost:3000
Uploading to Blob storage works, So Its safe to assume that the problem is solely related to the generated SAS token
Any assistance would be gladly appreciated :)
I tried in my environment and got below results:
Code:
var storage = require("#azure/storage-blob")
const accountname ="storage13261";
const key = "< Account key >";
const cred = new storage.StorageSharedKeyCredential(accountname,key);
const blobServiceClient = new storage.BlobServiceClient(`https://${accountname}.blob.core.windows.net`,cred);
const containerName="test";
const client =blobServiceClient.getContainerClient(containerName)
const blobName="nature.png";
const blobClient = client.getBlobClient(blobName);
const blobSAS = storage.generateBlobSASQueryParameters({
containerName,
blobName,
permissions: storage.BlobSASPermissions.parse("racwdt"),
startsOn: new Date(),
expiresOn: new Date(new Date().valueOf() + 86400)
},
cred
).toString();
const sasUrl= blobClient.url+"?"+blobSAS;
console.log(sasUrl);
Console:
The problem is in your SAS token where storage service is uses racwdt but in you SAS has rwdlacupiytfx that may cause to display an image.
I checked the Url + SAS token in the browser it perfectly worked.
Reference:
Grant limited access to data with shared access signatures (SAS) - Azure Storage | Microsoft Learn
Updated:
You can get both SAS and SAS-URL manually with check the permission by refer the below image.

Creating new document with Firestore REST API and Local Emulator Suite, Returning Error 404: Problem with Path Parameter

I’m just getting acquainted with Firebase/Firestore as a beginner coder, and I'm attempting to create an integration test for a set of callable functions a friend had written for their project. I am writing a test to automate testing using the Firebase local emulator suite.
Right now, I'm attempting to write a POSt request using Axios that will create a document in a given collection in my local emulator suite, after having received an Id Token from generating an authorized user.
The project id is called okane-crud-dev. I’ve created a collection
called test.
I have created an authenticated user with a given email and password, and generated the unique Id Token from an initial post request:
interface createPostRequest {
url: string;
data: Object;
config: Object;
};
//create an instance of a user
const createUserInstance : createPostRequest = {
url: 'http://localhost:9099/identitytoolkit.googleapis.com/v1/accounts:signUp?key=hi',
data: {
'email': 'myemail#email.com',
'password': 'mypassword',
'returnSecureToken': true
},
config: {
'headers':
{'Content-Type': 'application/json'}
},
};
const createUserResponse = await axios.post(createUserInstance.url, createUserInstance.data, createUserInstance.config);
const userIdToken = createUserResponse.data.idToken;
const userLocalId = createUserResponse.data.localId;
Up to this point, I have had no issues.
As for the second POST request to create a document, this is my code. I used this post as a reference:
Creating new collection and document with Firestore REST API returning HTTP 400
const createDocumentInstance : createPostRequest = {
url: "https://firestore.googleapis.com/v1beta1/projects/'localhost:8080/okane-crud-dev'/databases/(default)/documents/test",
data: {
"fields": {
"localId": userLocalId,
'budget': '2000',
}
},
//directly pasted IdToken as using the variable resulted in problem with ' ' error
config: {
'headers':
{
'Content-Type': 'application/json',
'Authorization': `Bearer ${userIdToken}`,
}
}};
console.log(createDocumentInstance);
const createDocument = await axios.post(createDocumentInstance.url, createDocumentInstance.data, createDocumentInstance.config);
const docReference = createDocument.data;
console.log(docReference);
When I attempted to run this, the following error was returned:
Request failed with status code 404
at createError (../../node_modules/axios/lib/core/createError.js:16:15)
at settle (../../node_modules/axios/lib/core/settle.js:17:12)
at IncomingMessage.handleStreamEnd (../../node_modules/axios/lib/adapters/http.js:293:11)
I'm a beginner and am just starting to learn how to code, so bear with me if this is an easy answer as I'm still figuring out how to debug.
I know that a 404 error means an issue with locating the resource -> and after making some adjustments to the headers, I figured the issue must be in my URL. I’ve tried looking around for other posts that use local emulator suite and POST requests to figure out if there was something wrong with how I wrote the path.
"https://firestore.googleapis.com/v1beta1/projects/'localhost:8080/okane-crud-dev'/databases/(default)/documents/test"
I've been looking at the Firebase documentation closely for creating a document; https://firebase.google.com/docs/firestore/reference/rest/v1beta1/projects.databases.documents/createDocument#path-parameters
Borrowing from the other post, I’ve tried different variations of where to include the emulator suite port: localhost:8080 and the project id “okane-crud-dev”. But haven’t seemed to figure out. I made sure that the project id was connected to my local emulator suite. Does anyone have any suggestions?
If you're using the Firestore Emulator with the REST API, you should change the base URL https://firestore.googleapis.com/v1 to your localhost http://localhost:8080/v1 then proceed with the path of your Firestore database.
http://localhost:8080/v1/projects/okrane-crud-dev/databases/(default)/documents/test

Securing Firebase Cloud Function HTTP Endpoint

I have a pretty simple use-case for a front facing website. It has a contact form and the details of the contact form need to be saved to the firebase database for further processing. The website is built using NextJS. I understand that the api functionality of NextJS is not usable using Firebase Hosting so as a result, I'm inclined to use Cloud Functions to set up a HTTP endpoint that accepts form data as a POST request and save it to the realtime database / Firestore.
However, I'm unable to figure out a way to secure this endpoint. How do I prevent a normal user from extracting the endpoint URL from website source code and sending multiple requests to that URL? Can I keep this endpoint responsive only for that particular domain? Or how do I resolve this?
Alternatively, I could use the Firebase SDK directly in-app and save the data to the database but that would require me to keep the contacts collection as public for anyone to read/write, which is again a security risk.
What would be a better way to solve this issue whilst keeping the security intact? Note that since its a public website, I cannot have authenticated users with Firebase.
That is not possible. Users can see all the URLs they are making calls to in the networks tab. Your serverless functions must be ready to handle spam (like reject malicious or badly formatted requests), though you will still be charged for the CPU usage. That is one of the biggest cons of being serverless. But you will be saving a lot of time setting up servers and all that hassle.
The best you can do is enable CORS which still won't prevent spam but will reject requests after the pre-flight request. Though only browsers follow CORS and API clients like postman or insomnia don't
This cannot be considered as a security threat as it all depends on your code's logic, however you'll be charged for the usage and that's the risk involved. There are services like Cloudflare API Shield but again, Firebase has it's own SDK so that can be bypass somehow.
Coming to the reCaptcha case which involves verifying the reCaptcha token on the backend, you may get rid of bots to some extents. But if someone just keeps spamming your server without valid token, your functions are still going to charge you for time taken for validating the token.
Please let me know if you have any more questions.
Both of your options can work.
Using the rest api with clloud functions you could integrate the Google Captcha.
Using directly the database you can write the database rules in a way that everyone can only add a new contact and can't read or edit it. This is still less secure because someone could fill up your database. But with a quite good field validation and duplicate restriction that would be a "lesser" problem.
Here is how we did it with our website:
The cloud functions that handle the captcha and contact POST:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const rp = require("request-promise");
const nodemailer = require("nodemailer");
const gmailEmail = encodeURIComponent(functions.config().gmail.email);
const gmailPassword = encodeURIComponent(functions.config().gmail.password);
const mailTransport = nodemailer.createTransport(
`smtps://${gmailEmail}:${gmailPassword}#smtp.gmail.com`
);
exports.checkRecaptcha = functions
.region("europe-west1")
.https.onRequest((req, res) => {
const response = req.query.response;
console.log("recaptcha response", response);
rp({
uri: "https://recaptcha.google.com/recaptcha/api/siteverify",
method: "POST",
formData: {
secret: "TOP_SECRET",
response: response,
},
json: true,
})
.then((result) => {
console.log("recaptcha result", result);
if (result.success) {
res.send("You're good to go, human.");
} else {
res.send("Recaptcha verification failed. Are you a robot?");
}
})
.catch((reason) => {
console.log("Recaptcha request failure", reason);
res.send("Recaptcha request failed.");
});
});
exports.triggerEmail = functions
.region("europe-west1")
.https.onRequest((req, res) => {
if (req.method !== "POST") {
res.status(400).send("Please send a POST request");
return;
}
const values = JSON.parse(req.body);
const email = "email#company.com";
const bcc = "email#company.com";
const mailOptions = {
subject: "Kontakt von Website",
text: `Datum: ${values.dateTime}\n
Name: ${`${values.gender} ${values.firstname} ${values.lastname}`}
Firmenname: ${values.company ? values.company : ""}
Strasse: ${values.street ? values.street : ""}
Ort: ${values.place ? values.place : ""}
Land: ${values.country ? values.country : ""}
PLZ: ${values.zip ? values.zip : ""}
E-Mail: ${values.email ? values.email : ""}\n\n
${values.text ? values.text : ""}`,
to: email,
bcc: bcc,
};
console.log(req.headers.origin);
if (
req.headers.origin == "https://www.your_company.com" ||
req.headers.origin == "http://localhost:3000"
) {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
}
return mailTransport.sendMail(mailOptions).then(() => {
res.status(200).send("OK");
});
});
The Captcha in our side usinge react:
<ReCAPTCHA
ref='recaptcha'
sitekey='SECRET_KEY'
onChange={response => this.setState({ response: response })}
/>
And the contact POST call:
fetch('https://URL_TO_YOUR_FUNCTION_THAT_SENDS_THE_EMAIL', {
method: 'POST',
body: JSON.stringify({
dateTime: new Date().toString(),
gender: this.state.gender,
firstname: this.state.firstname,
lastname: this.state.lastname,
company: this.state.company,
street: this.state.street,
place: this.state.place,
country: this.state.country,
zip: this.state.zip,
email: this.state.email,
text: this.state.text,
isLogistik: isLogistik
})
})
Make sure that your Captcha settings are setup to us the second cloud function for verification.

Firebase service account to generate authentication token for client-side use with Google Apps Script

I am having difficulty using the FirebaseApp (a 3rd party API) to generate an authentication token that can be passed to a sidebar and used by the client to login and access my Firebase Database client-side.
I'm trying to use this tutorial but cannot get it working without using a database secret (which is being depreciated) in makeToken(). I'd prefer to use a service account as reflected in this tutorial. When I look at the difference between the tokens generated, the first 2 pieces separated by a '.' are identical, the last piece after the final '.' is different. The lengths are the same as well. eg:
//Example Generated by Database Secret: TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBv.ZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=.dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2U=
//Example Generated by Service Account: TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBv.ZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=.IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbml=
I can generate the OAuth access token, pass it to FirebaseApp and generate an authentication token, but when it is passed client-side and I attempt to authenticate I get an error: Login Failed! Error: INVALID_TOKEN: Failed to validate MAC.
It seems like there is a lot of misinformation and conflicting information on how this should be done.
I have a getFirebaseService() function server-side that uses Apps Script OAuth2 Library to get an access token.
function getFirebaseService() {
return OAuth2.createService('Firebase')
// Set the endpoint URL.
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the private key and issuer.
.setPrivateKey(fb_PRIVATE_KEY) //Service account private key
.setIssuer(fb_SERVICE_EMAIL) //Service account email
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scopes.
.setScope('https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/firebase.database');
}
I have a makeToken() function server-side that gets an authentication token from Firebase using the OAuth access token. I am able to use the service.getAccessToken() OAuth token server-side to access and store data. So that works, I guess my issue is creating a client auth token that's more restrictive.
function makeToken(){
var service = getFirebaseService();
if (service.hasAccess()) {
return FirebaseApp.getDatabaseByUrl(fb_URL, service.getAccessToken()) //Database Secret Works: "AAslhfi3MYACCESSTOKEN2930hf03ah4th8" but is being depreciated.
.createAuthToken(Session.getActiveUser().getEmail());
} else {
Logger.log("makeToken: " + service.getLastError());
}
}
Then client-side, from the sidebar, I try to authenticate with a custom auth token retrieved server-side from makeToken().
var userAuthToken;
google.script.run.withSuccessHandler(function (requestAuthToken) {
userAuthToken = authenticateClient(requestAuthToken)
}).makeToken();
function authenticateClient(userRequestToken) {
var ref = new Firebase(fb_URL);
ref.authWithCustomToken(userRequestToken, function (error, authData) {
if (error) {
console.log("FB Login Failed!", error); //Error below come from here.
}
else {
console.log("FB Login Succeeded!", authData);
}
});
return ref.authData.auth;
}
This results in Login Failed! Error: INVALID_TOKEN: Failed to validate MAC..
Edit: Is it possible FirebaseApp is incorrectly generating the JWT Authentication Token?
Edit2: I think the above edit is unlikely as I attempted to use the GSApp library and had the same issue. It only seems to want the depreciated database secret, not a service account OAuth.
Alright, so after a very long day I figured it out. I'm going to lay out what I ended up using for libraries and what the issue was (see the third library). The main problem was essentially that the tutorial was outdated and no a lot of people use Firebase in apps script.
OAuth2 (Server-side)
Link
I didn't have to change anything here! It was working fine and never an issue.
FirebaseApp (Server-side)
Link
This is a nice library and I stuck with it because it worked well (once I got it there). I had to make a change to my original code that came from the tutorial I mentioned. My code ended up like this and worked:
if (service.hasAccess()) {
return FirebaseApp.getDatabaseByUrl(fb_URL, service.getAccessToken()) //get OAuth Token
.createAuthToken(Session.getEffectiveUser().getEmail(), null, serviceAccount.client_email, serviceAccount.private_key);
//... Added the null, private key, and service email parameters.
Firebase (Client-side)
Link
Alright, so this is where my main issue was -- The tutorial I followed for client-side setup was old. I had to upgrade the code on my own to use the new 3.x version:
<script src="https://www.gstatic.com/firebasejs/5.8.2/firebase.js"></script>
// Initialize Firebase
var config = {
apiKey: "<Web API Key>",
authDomain: "<Project ID>.firebaseapp.com",
databaseURL: "https://<DB URL>.firebaseio.com/"
};
firebase.initializeApp(config);
With this firebase instance I was able to update my original authenticateClient() method:
function authenticateClient(userRequestToken) {
firebase.auth().signInWithCustomToken(userRequestToken).catch(function(error) {
// Handle Errors here.
console.error("authClient: ", error.code, error.message);
});
return {
uid: firebase.auth().currentUser.uid,
metadata: {
lastSignInTime: firebase.auth().currentUser.lastSignInTime
}
};
}
That's it! I now have a firebase instance with a signed in user via JWT Custom Token! I came across a few people with similar issues an I hope this helps.

Resources