Calling (private) cloud function from cloud function - node.js

I keep getting a 403 with the testcode below. Is it just me or is it overly complicated to call a function within the same project? I did some research here and here.
I've set the cloud function invoker on the default service account for both functions. And the allow internal traffic
So i have tried both codes below. The token is printed to the logs in the first function, so why do i still get a 403?
Script1:
const axios = require("axios");
/**
* Responds to any HTTP request.
*
* #param {!express:Request} req HTTP request context.
* #param {!express:Response} res HTTP response context.
*/
exports.helloWorld = async (req, res) => {
console.log(JSON.stringify(process.env));
const sample_api_url = `https://someRegionAndSomeProject.cloudfunctions.net/sample-api`;
const metadataServerURL =
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=";
const tokenUrl = metadataServerURL + sample_api_url;
// Fetch the token
const tokenResponse = await axios(tokenUrl, {
method: "GET",
headers: {
"Metadata-Flavor": "Google",
},
});
const token = tokenResponse.data;
console.log(token);
const functionResponse = await axios(sample_api_url, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = functionResponse.data;
console.log(data);
res.status(200).json({ token, data });
};
Script2:
const {GoogleAuth} = require('google-auth-library');
/**
* Responds to any HTTP request.
*
* #param {!express:Request} req HTTP request context.
* #param {!express:Response} res HTTP response context.
*/
exports.helloWorld = async (req, res) => {
const url = 'https://someRegionAndSomeProject.cloudfunctions.net/sample-api';
const targetAudience = url;
const auth = new GoogleAuth();
const client = await auth.getIdTokenClient(targetAudience);
const response = await client.request({url});
res.status(200).json({data: response.data})
};

There is 2 types of security on Cloud Functions
Identity based security: if you deploy your Cloud Functions without the allow-unauthenticated parameter, you have to send a request with an Authorization: bearer <token> header, with token is an identity token which has, at least, the cloud functions invoker role. You can also add the allUsers user with the cloud functions invoker role to make the function publicly reachable (no security header required)
Network based security: this time, only the request coming from your project VPCs or your VPC SC are allowed to access the Cloud Functions. If you try to reach the cloud functions from an unauthorized network, you get a 403 (your error).
You can combine the 2 security solutions if you want. In your code, you correctly add the security header. However, your request is rejected by the network check.
The solution is not so simple, not free, and I totally agree with you that this pattern should be simpler.
To achieve this, you must create a serverless VPC connector and attach it on your functions that perform the call. You also have to set the egress to ALL on that functions. That's all
The consequence are the following: The traffic originated from your function will be routed to your VPC thanks to the serverless VPC connector. The Cloud Functions URL being always a public URL, you have to set the egress to ALL, to route the traffic going to public URL through the serverless VPC connector.

Based on your post, I created a sample that worked for me. I created two GCP functions "func1" and "func2". I then determined how to call func2 from func1 where func2 is only exposed to be invoked by the service account identity which func1 runs as.
The final code for func1 is as follows:
const func2Url = 'https://us-central1-XXX.cloudfunctions.net/func2';
const targetAudience = func2Url;
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();
async function request() {
console.info(`request ${func2Url} with target audience ${targetAudience}`);
const client = await auth.getIdTokenClient(targetAudience);
const res = await client.request({url: func2Url});
console.info(res.data);
return res;
}
exports.func1 = async (req, res) => {
let message = `Hello from func1`;
try {
let response = await request();
res.status(200).send(`${message} + ${response.data}`);
}
catch(e) {
console.log(e);
res.status(500).send('failed');
}
};
The primary recipe used here is as described in the Google Docs here. I also found this medium article helpful.

Related

Can I call Google cloud run with id token client?

My backend service is deployed on cloud run.
It can only access by authorized request with IAM service account.
So i use google-auth-libray to request from frontend.
const client = await auth.getIdTokenClient(process.env.NEXT_PUBLIC_CALL_URL!);
const data = await client.request({
url: process.env.NEXT_PUBLIC_CALL_URL!,
});
this is my source.
But I want call with my login token.
So i attempt like this.
const client = await auth.getIdTokenClient(process.env.NEXT_PUBLIC_CALL_URL!);
const header = await client.getRequestHeaders();
const data = await axios.get(process.env.NEXT_PUBLIC_CALL_URL, {
header: {
Authorization: `${header}, Bearer mytoken`
}
})
But It's not working.
Can i call request from front-end like this?

Why is Azure KeyVault getSecret returning Promise <pending>?

I followed the Microsoft tutorial on this link: https://learn.microsoft.com/en-us/azure/key-vault/secrets/quick-create-node but I want to have them as separate functions instead of putting them into one main function.
This is what I have, setSecret works fine but getSecret is returning Promise ?
Both getSecret and setSecret return a Promise because they are asynchronous methods that need to make an HTTP request to the Key Vault service.
If you were to try the following:
const secretPromise = client.setSecret(secretName, secretValue);
You'll notice that secretPromise is a Promise<KeyVaultSecret> as per the API documentation
This allows you to wait for the secret to be set and get back the newly set secret:
const secret = await client.setSecret(secretName, secretValue);
Be mindful that by not waiting for the setSecret call to succeed you will be unable to:
Get the newly created secret (unless you get lucky with timing)
Be notified and handle any errors if setSecret fails (you can verify this by creating the secret client with an invalid Key Vault URL and running both functions - azureSetSecret will claim success but azureGetSecret will throw an error)
I'm not sure if you are trying to find a way which allows you to obtain key vault secrets in nodejs. As the quick start sample you mentioned above, microsoft provides a async method await client.getSecret(secretName) for nodejs.
Here I'd like to recommend you using rest api to access key vault secret. When calling the api, you need to generate an access token as the request header, you can refer to this sample or take a look at my test project below.
calling api:
const axios = require('axios');
const auth = require('./credenticalflow');
let vaultName = 'key vault name';
let keyName = 'key name';
let accesstoken = '';
let secret = '';
init();
async function init(){
const authResponse = await auth.getToken(auth.tokenRequest);
accesstoken = authResponse.accessToken;
getsecret(vaultName,keyName,accesstoken);
console.log("22222222:"+secret);
}
function getsecret(vaultName,keyName,token){
console.log('the token is :'+ token);
axios.get('https://'+vaultName+'.vault.azure.net/secrets/'+keyName+'/7d4b682f5c9a41578602aa5b86611aa7?api-version=7.1',{
headers: {
'Authorization': 'Bearer '+token
}
})
.then(function (response) {
// handle success
secret = response.data.value;
console.log("1111111:"+secret);
})
.catch(function (error) {
// handle error
console.log('error');
});
}
generate access token:
const msal = require('#azure/msal-node');
const msalConfig = {
auth: {
clientId: 'azure ad app cilent id',
authority: 'https://login.microsoftonline.com/<your tenant name such as xx.onmicrosoft.com>',
clientSecret: 'client secret for the azure ad app',
}
};
const tokenRequest = {
scopes: ['https://vault.azure.net/.default'],
};
const cca = new msal.ConfidentialClientApplication(msalConfig);
async function getToken(tokenRequest) {
return await cca.acquireTokenByClientCredential(tokenRequest);
}
module.exports = {
tokenRequest: tokenRequest,
getToken: getToken
};

Cloud Function to Authenticate a User

I am attempting to authenticate a user to access various scopes in the user Gsuite. I can run the code locally but I cannot seem to get it accepted as a cloud function.
I have tried deploying with firebase and with gcloud. I have checked my eslint settings.
This code is coming from https://github.com/googleapis/google-api-nodejs-client/blob/master/README.md#oauth2-client
'use strict';
const fs = require('fs');
const path = require('path');
const http = require('http');
const url = require('url');
const opn = require('open');
const destroyer = require('server-destroy');
const {google} = require('googleapis');
/**
* To use OAuth2 authentication, we need access to a a CLIENT_ID, CLIENT_SECRET, AND REDIRECT_URI. To get these credentials for your application, visit https://console.cloud.google.com/apis/credentials.
*/
const keyPath = path.join(__dirname, 'credentials.json');
let keys = {redirect_uris: ['']};
if (fs.existsSync(keyPath)) {
keys = require(keyPath).web;
}
/**
* Create a new OAuth2 client with the configured keys.
*/
const oauth2Client = new google.auth.OAuth2(
keys.client_id,
keys.client_secret,
`http://localhost:3000/oauth2callback`
);
/**
* This is one of the many ways you can configure googleapis to use authentication credentials. In this method, we're setting a global reference for all APIs. Any other API you use here, like google.drive('v3'), will now use this auth client. You can also override the auth client at the service and method call levels.
*/
google.options({auth: oauth2Client});
const scopes = ['https://www.googleapis.com/auth/documents'];
/**
* Open an http server to accept the oauth callback. In this simple example, the only request to our webserver is to /callback?code=<code>
*/
async function authenticate(){
// grab the url that will be used for authorization
const authorizeUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes
});
const server = http.createServer(async (req, res) => {
try {
if (req.url.indexOf('/oauth2callback') > -1) {
const qs = new url.URL(req.url, 'http://localhost:3000').searchParams;
res.end('Authentication successful! Please return to the console.');
server.destroy();
const {tokens} = await oauth2Client.getToken(qs.get('code'));
oauth2Client.credentials = tokens; // eslint-disable-line require-atomic-updates
resolve(oauth2Client);
}
} catch (e) {
reject(e);
}
})
.listen(3000, () => {
// open the browser to the authorize url to start the workflow
opn(authorizeUrl, {wait: false}).then(cp => cp.unref())
.catch(
error => {
console.log(error);
});
});
destroyer(server)
.then(client => runSample(client)).catch(
error => {
console.log(error);
});
};
module.exports.authenticate=authenticate;
async function runSample(client) {
// retrieve user profile
console.log(client);
const docs = google.docs({
version: 'v1',
auth: client
});
const createResponse = await docs.documents.create({
requestBody: {
title: 'Your new document!',
},
});
}
I expect it to load as a cloud function to firebase or gcloud.
However:
Firebase returns "Deploy complete" but it never shows in the functions.
gcloud returns "SyntaxError: Unexpected token function" with the word function indicated in "async function authenticate(){"
I'm new to node.js and may be missing something really obvious to others.
You will never get User Credentials (Client ID/Client Secret) to work
in Cloud Functions (meaning authenticate and create credentials).
OAuth requires a web browser and a human. Neither one exists in Cloud
Functions. Use a Service Account instead. – John Hanley

Auth0 "service not found" error

I'm attempting to use Auth0 to issue JWT tokens for accessing my API (so that Auth0 handles all the OAuth and security concerns, etc., and my API just needs to check the token). When I try to test the Authorization Code flow for clients to receive an access token (using Node + Express), the following happens:
The authorization code request works fine, and the client is redirected back to my redirect_uri with the code appended to the query. All good.
The token request then always fails. If I include the audience parameter, the request returns an access_denied error with the following details: Service not found: {the audience parameter}, regardless of what value I set for the audience parameter.
If I don't include the audience parameter, I get a server_error with the message Service not found: https://oauth.auth0.com/userinfo.
I've checked every Auth0 setting and read every documentation page thoroughly, and so far nothing has worked. I've also tested the Authorization Code flow in Auth0's API debugger, and it worked fine. My test follows exactly the same parameters, and yet still receives an error requesting the token. I'm testing on localhost. The client credentials and implicit flows are working fine.
Here is a test endpoint I created which retrieves the authorization code from Auth0:
const qs = require('querystring');
const getCode = (req, res) => {
const params = {
audience, // the value of the API Audience setting for the client
client_id, // the client ID
redirect_uri, // the redirect_uri, which is also listed in the Allowed Callback URLs field
response_type: `code`,
scope: `offline_access open` // ask to return ID token and refresh token,
state: `12345`,
};
const authDomain = `mydomain.auth0.com/oauth`;
res.redirect(`${authDomain}/oauth/authorize?${qs.stringify(params)}`);
};
The redirect_uri then redirects to the following endpoint, where I make the request for the access token:
const https = require('https');
const callback = (req, res) => {
const body = {
client_id,
client_secret,
code: req.query.code,
grant_type: `authorization_code`,
redirect_uri, // same value as provided during the code request
};
const opts = {
headers: { 'Content-Type': `application/json` },
hostname: `mydomain.auth0.com`,
method: `POST`,
path: `/oauth/token`,
};
const request = https.request(opts, response => {
let data = ``;
response.on(`data`, chunk => { data += chunk; });
response.on(`error`, res.send(err.message));
response.on(`end`, () => res.json(JSON.parse(data))); // this executes, but displays the error returned from Auth0
});
request.on(`error`, err => res.send(err.message));
request.end(JSON.stringify(body), `utf8`);
};
Any suggestions as to what I might be doing wrong?
The issue was that I was calling the incorrect URL at Auth0. I mistakenly thought that both the authorization and token endpoints began with /oauth, when in fact the authorization endpoint is just /authorize, while the token endpoint is /oauth/authorize. Correcting the URLs in my code fixed the problem.
My solution was the identifier of the api was not found. If it is not exact it won't find it. I had an extra backslash on my 'audience' where the identifier didnt have one. pretty easy mistake but the error is not very clear in Auth0.
In my case, I was using auth0 react hooks. So the example code looked like this:
const getUserMetadata = async () => {
const domain = process.env.REACT_APP_AUTH0_DOMAIN
try {
const accessToken = await getAccessTokenSilently({
audience: `https://${domain}/api/v2/`,
scope: 'read:current_user',
})
console.log('accessToken', accessToken)
localStorage.setItem('access_token', accessToken)
setUserAuthenticated(true)
} catch (e) {
console.log('error in getting access token', e.message)
}
}
My solution to this was using by default Auth0 Audience value in audience field
const getUserMetadata = async () => {
const auth0audience = process.env.REACT_APP_AUTH0_AUDIENCE
try {
const accessToken = await getAccessTokenSilently({
audience: auth0audience,
scope: 'read:current_user',
})
console.log('accessToken', accessToken)
localStorage.setItem('access_token', accessToken)
setUserAuthenticated(true)
} catch (e) {
console.log('error in getting access token', e.message)
}
}
Because its stated in auth0 docs of configuring custom domains that, you need to use by default API audience
Source - https://auth0.com/docs/brand-and-customize/custom-domains/configure-features-to-use-custom-domains

Cloud Functions for Firebase: how to issue a request to my Cloud Endpoint

I'm trying to issue a request to my cloud endpoint project when a certain value is written in the firebase database. I can't find any example of how perform a request to Endpoints in Node.js. Here's what I come up with so far:
"use strict";
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const gapi = require('googleapis');
admin.initializeApp(functions.config().firebase);
exports.doCalc = functions.database.ref('/users/{uid}/calc').onWrite(event => {
return gapi.client.init({
'apiKey': 'AIzxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'clientId': '1234567890-xxx.apps.googleusercontent.com',
'scope': 'donno what to put here'
}).then(function() {
return gapi.client.request({
'path': 'https://myproj.appspot.com/_ah/api/myApi/v1',
'params': {'query': 'startCalc', uid: event.params.uid }
})
}).then(function(response) {
console.log(response.result);
}, function(reason) {
console.log('Error: ' + reason.result.error.message);
});
});
When triggered, Functions' log spouts: TypeError: Cannot read property 'init' of undefined. i.e. doesn't even recognize gapi.client.
First, what is the right package to use for this request? googleapis? request-promise?
Second, am I setting up the correct path and parameters for a call to an endpoint? Assume the endpoint function is startCalc(int uid).
Update
It seems that Cloud Functions for Firebase blocks requests to their App Engine service - at least on the Spark plan (even though they're both owned by Google - so you'd assume "on the same network"). The request below, works on a local machine running Node.js, but fails on the Functions server, with a getaddrinfo EAI_AGAIN error, as described here. Evidently, it is not considered accessing Google API when you perform a request to your server running on Google's App Engine.
Can't explain why Firebase advocates here steer clear of this question like from fire.
Original Answer
Figured it out - switched to 'request-promise' library:
"use strict";
const functions = require('firebase-functions');
const request = require('request-promise');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.doCalc = functions.database.ref('/users/{uid}/calc').onWrite(event => {
return request({
url: `https://myproj.appspot.com/_ah/api/myApi/v1/startCalc/${event.params.uid}`,
method: 'POST'
}).then(function(resp) {
console.log(resp);
}).catch(function(error) {
console.log(error.message);
});
});

Resources