How to parse and validate Cloud Task token in Firebase HTTP function - firebase

I have a node.js application that creates Cloud HTTP tasks with authentication. I'd like to handle these tasks viaFirebase HTTP function (also in JS). I understand that I need to use oidcToken when creating a task, but I don't understand how to validate such a token on the Firebase HTTP function end. Docs are not very helpful. I was expecting to find some utility in #google-cloud/tasks or in googleapis/google-auth-library-nodejs, but nothing jump out at me.

All you have to do is assosiacte your Cloud Function with a service account. This is called Function Identity. Please note that whenever you deploy a Cloud Function either in GCP or Firebase, the same function appears in both platforms.
You can create a new service account for this purpose with:
gcloud iam service-accounts create [YOUR_NEW_SERVICE_ACCOUNT_NAME] \
--display-name "Service Account Test"
And assign the required IAM role with:
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member serviceAccount:[YOUR_NEW_SERVICE_ACCOUNT_NAME]#${PROJECT_ID}.iam.gserviceaccount.com \
--role roles/cloudfunctions.invoker
Your function deployment should look like something like this:
gcloud functions deploy hello_world \
--trigger-http \
--region us-central1 \
--runtime nodejs14 \
--service-account [YOUR_NEW_SERVICE_ACCOUNT_NAME]#${PROJECT_ID}.iam.gserviceaccount.com \
--no-allow-unauthenticated
Once you have all that, your Cloud HTTP task should use this service account as the OICD token, but I believe you already know how to do so.
You can find more information in this guide (although it uses the Cloud Scheduler instead of Cloud Tasks, the idea is pretty much the same).

You don't need to validate the token in the function, when you create the function so only authenticated users can call it this verification will automatically be done on Google's side.
So, to make what you want you need to:
1. Create the Task Queue
Detailed steps in this how-to.
2. Create the service account
You will need to create a SA with these IAM permissions:
Cloud Functions Invoker
Cloud Tasks Enqueuer
Service Account User
"Cloud Functions Invoker" is necessary to be able to call the function and "Cloud Tasks Enqueuer" to add tasks to the queue. If you want to use a different SA for each of those steps you can separate these permissions.
3. Create the Firebase Functions function
When creating the function, make sure that it required authentication.
4. Create the task using the SA
There's a section for this in the documentation, which you can find here. The code below is copied below:
// Imports the Google Cloud Tasks library.
const {CloudTasksClient} = require('#google-cloud/tasks');
// Instantiates a client.
const client = new CloudTasksClient();
async function createHttpTaskWithToken() {
const project = '[PROJECT_NAME]';
const queue = '[QUEUE_NAME]';
const location = '[LOCATION]';
const url = '[FUNCTION_TREIGGER_URL]';
const serviceAccountEmail = '[SA]';
const payload = 'Hello, World!';
// Construct the fully qualified queue name.
const parent = client.queuePath(project, location, queue);
const task = {
httpRequest: {
httpMethod: 'POST',
url,
oidcToken: {
serviceAccountEmail,
},
},
};
if (payload) {
task.httpRequest.body = Buffer.from(payload).toString('base64');
}
console.log('Sending task:');
console.log(task);
// Send create task request.
const request = {parent: parent, task: task};
const [response] = await client.createTask(request);
const name = response.name;
console.log(`Created task ${name}`);
}
createHttpTaskWithToken();

Related

How to test otp login in cypress?

How does one E2E test OTP login?
I have set up an OTP login, I want to write a Cypress test for it where the user enters the OTP and gets it in email. How do I write a test for this, given that the OTP changes every time I send an email?
The current solutions I have are:
To create a test account and hardcode a static OTP for it on the server.
To create a mock API with static responses and use that for testing (currently I'm using the actual deployed API for testing)
If I'm understanding your requirement, you can use otplib to bypass the email reading/parsing stage and directly generate the token that would otherwise be sent to the user in an email.
The package cypress-otp is a thin wrapper around otplib, but unfortunately it's not up-to-date and is awfully noisy and hard to follow for such a simple task.
This is how I unraveled the code and updated for Cypress ver 10.10.0:
Install otplib
yarn add -D otplib or npm install otplib --save-dev
Add a task to call otplib from you test
This takes the place of reading a mail and parsing out the token, which you don't need to test because normally a user does that and enters the token into your app under test.
In cypress.config.js
const { defineConfig } = require("cypress");
const otplib = require("otplib");
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
on('task', {
generateToken(secret) {
return otplib.authenticator.generate(secret);
}
})
},
},
});
Example test taken from cypress-otp (simplified)
describe('Example of generating a token for use in OTP tests', () => {
let secret; // in this example, secret is taken from the app page,
// but normally you will pass it in from a fixture
// or an environment variable
beforeEach(() => {
cy.visit('https://otplib.yeojz.dev'); // we use this page as a sample app
// Get the secret
cy.contains("Demo Secret")
.parent().parent()
.find('input').invoke('val')
.should('not.eq', 'Loading...') // simpler than waitUntil()
.then(value => secret = value)
})
it('tests the token entry', () => {
cy.task('generateToken', secret)
.then(token => {
cy.contains('Verify Token').click();
cy.contains('Please input a token')
.parent()
.find('input')
.type(token);
cy.contains('The token is valid in this current window')
.should('be.visible')
})
})
})
This test is the one given in cypress-otp, a simplified version of it which is more easily understood with moderate programming skills.
It's a bit contrived, because the app provides both the secret and the token, and then also verifies it.
The essential part is the cy.task('generateToken', secret) which makes receiving the token very easy.
If your application is sending OTP via emails then I have a solution for you.
Mailhog.
You can point the SMTP to mailhog and all the outbound emails will appear in Mailhog's management console. Something like this.
After that it's a piece of cake. You can access those email inside cypress tests using this plugin.
https://www.npmjs.com/package/cypress-mailhog
It's been two months but I hope this helps.

How to get the current https functions endpoint for firebase when using/not using emulator

I have a graphql api running on a firebase function and I develop locally for the most part.
Before when I called the firebase functions directly I would use the connectFunctionsEmulator function to conditionally connect to my local emulator and when I called the functions using getFunctions it would know to call my localhost endpoint.
However I'm trying to use Apollo client and I'm not sure how to determine when the local emulators have been connected and when they have not as well as how to determine the absolute url when calling the function like the way firebase does when I call it using the firebase functions helpers.
I went snooping through the code and when I call the connectFunctionsEmulator function it updates a variable called functionsInstance.emulatorOrigin.
This is not present in the functions typing but I'm still able to access the value by doing getFunctions().emulatorOrigin.
If the value is not set to null that means firebase will be talking to my emulators and not to the live endpoint.
function getGraphQLUri(): string {
const functions = getFunctions();
const app = getApp();
const { projectId } = app.options;
const { region } = functions;
// emulatorOrigin is not defined in the typings but it exists
// #ts-ignore
const emulator = functions.emulatorOrigin;
let url: string = "";
if (emulator) {
url = `${emulator}/${projectId}/${region}/graphql`;
} else {
url = `https://${region}-${projectId}.cloudfunctions.net/graphql`;
}
return url;
}
I would appreciate if someone at Google can confirm whether this approach might continue to be supported in the future.

Firebase Cloud Functions Deployment Error with error code "NoSuchKey"

I'm trying to deploy one of my functions from firebase CLI (version 8.12.1) and it keeps failing.
The function hasn't changed in weeks, so I am a bit confused as to why it's failing now.
Error from the CLI
functions[http-api-(europe-west1)]: Deployment error.
Build failed: Build error details not available. Please check the logs at https://console.cloud.google.com/logs/viewer?project=&advancedFilter=resource.type%3Dbuild%0Aresource.labels.build_id%3Dfeb2697d-29b4-4ab7-9b84-90d9f847be42%0AlogName%3Dprojects%2Fvestico-dev%2Flogs%2Fcloudbuild
Logs from the cloud console
Step #3 - "restorer": Restoring data for "google.nodejs.functions-framework:functions-framework" from cache
Step #3 - "restorer": \u001b[31;1mERROR: \u001b[0mfailed to restore: restoring data: GET https://storage.googleapis.com/eu.artifacts..appspot.com/containers/images/sha256:484d08dfc6a8f356c34a86fa4440fedf86f4fc398967eea66e4aab4e9ee81e3d?access_token=REDACTED: unsupported status code 404; body: NoSuchKeyThe specified key does not exist.No such object: eu.artifacts..appspot.com/containers/images/sha256:484d08dfc6a8f356c34a86fa4440fedf86f4fc398967eea66e4aab4e9ee81e3d
Finished Step #3 - "restorer"
ERROR: build step 3 "eu.gcr.io/fn-img/buildpacks/nodejs10/builder:nodejs10_20201005_20_RC00" failed: step exited with non-zero status: 46
The interesting piece is probably the error from above:
<Error>
<Code>NoSuchKey</Code>
<Message>The specified key does not exist.</Message>
<Details>No such object: eu.artifacts.<project-id>.appspot.com/containers/images/sha256:484d08dfc6a8f356c34a86fa4440fedf86f4fc398967eea66e4aab4e9ee81e3d</Details>
</Error>
What key is builder referring to? <project-id>#appspot.gserviceaccount.com has accesss to the cloud function with roles Cloud Functions Admin and Editor.
EDIT
Deploying through firebase deploy --only functions:<my-api>
That function uses #google-cloud/storage to get a public url for a storage resource.
I'm loading the service account configs like this:
const devServiceAccount = require("../../service-accounts/dev.json");
const prodServiceAccount = require("../../service-accounts/prod.json");
export const getAdminConfig = (): (AppOptions | undefined) => {
const baseConfigEnv = process.env.FIREBASE_CONFIG;
if (!baseConfigEnv) {
console.error("no firebase config environment");
return undefined;
}
const app = functions.config().app;
if (app === undefined) {
console.error("no firebase app config");
return undefined;
}
const serviceAccount = app.environment === 'dev' ? devServiceAccount : prodServiceAccount;
const adminConfig = JSON.parse(baseConfigEnv) as AppOptions;
adminConfig.credential = credential.cert(serviceAccount);
return adminConfig;
}
The cloud storage is used here.
const options = {
action: 'read',
expires: Date.now() + 1000 * 60 * 60 //1 hour
} as GetSignedUrlConfig;
const file = bucket.file(path);
filePathPromises.push(file.getSignedUrl(options))
});
My folder structure is as follows.
+ functions
+ lib
+ function.js
+ service-accounts
+ dev.json
+ prod.json
+ src
+ function.ts
I was ruling out that the service account files are the issue given that the files are loaded in getAdminConfig() for all functions in the project.
Update 10/13/20
I've verified the files uploaded to the GCF storage container. The JSON keys are there and in the right location. The paths match, so they should be found when the GCF is running.
Adding a hint for the next soul running into this problem. It seems to be caused by missing/inaccessible file in the restore/rollback process.
I was successfully removing the problem by simply:
Deleting my functions using the web firebase console.
Deploying normally again >firebase deploy
It seems there was an intermittent issue in Firebase Cloud Functions or GCF. I just ran firebase deploy --only functions again and it deployed successfully.

Securing Cloud Functions auth onDelete trigger

How do we secure the auth().onDelete handler so it's not exposed as a function (so user code cannot call it)?
I define the handler as a Firebase Cloud function:
exports.deleteUser = functions.auth.user().onDelete((user, context) => { ... }
It seems like this function is executable in client code by calling:
var f = config.getFunctions().httpsCallable('deleteUser')
const rc = await f({user: 'abc'})
I want that code to fail as if the function does not exist, but it doesn't. Running "firebase serve" I see the function being executed when I call it directly using the httpsCallable approach.
I want to ensure it's automatically called when a user is deleted but never otherwise.

How Firebase Cloud functions handle HTTP post method?

I have created Firebase Cloud Functions app,
I created function with https.onRequest.
and get data with req.body but there is not data there.
Can Firebase Cloud Functions can handle HTTP POST method?
This is my sample code:-
var functions = require('firebase-functions');
exports.testPost = functions.https.onRequest((req, res) => {
console.log(req.body);
});
I tested by postman with POST method but didn't show result in Firebase log.
Functions built on Firebase can also use Express.js routers for handling GET/POST/PUT/DELETE, etc... is fully supported by Google, and is the recommended way to implement these types of functions.
More documentation can be found here:
https://firebase.google.com/docs/functions/http-events
Here's a working example built on Node.js
const functions = require('firebase-functions');
const express = require('express');
const cors = require('cors');
const app = express();
// Automatically allow cross-origin requests
app.use(cors({ origin: true }));
app.get('/hello', (req, res) => {
res.end("Received GET request!");
});
app.post('/hello', (req, res) => {
res.end("Received POST request!");
});
// Expose Express API as a single Cloud Function:
exports.widgets = functions.https.onRequest(app);
Then, run firebase deploy, and that should compile your code and create the new "widgets" function. Note: You can rename widgets to anything you want. Ultimately, it will generate a URL for calling the function.
I am planning to do the same thing. What I reckon the approach should be is to check the request.method in the function body. A probable approach can be:
if (request.method != "POST") {
respond.status(400).send("I am not happy");
return;
}
// handle the post request
Here's some reference to the details regarding what the request object holds: https://firebase.google.com/docs/functions/http-events
Firebase functions support GET, POST, PUT, DELETE, and OPTIONS method, and you can check what kind of methods that trigger your function.
// Check for POST request
if(request.method !== "POST"){
res.status(400).send('Please send a POST request');
return;
}
Then to get data from POST request (for example JSON type) will be in the header of your request.
const postData = request.body;
// for instance
const format = req.body.format;
// query string params
let format = req.query.format;
Maybe your project hasn't been setup to communicate with your firebase database. Try the following from your terminal:
npm install -g firebase-tools
Then inside your project folder, run the following and login using your credentials
firebase login
Then
firebase init functions
This will create a folder with index.js, package.json and node_modules
If you are using Postman correctly the rest of your code should work.

Resources