How to invoke "authenticated" functions on GCP cloud functions - google-cloud-platform

On GCP cloud functions if I unchecked the "Allow unauthenticated invocations", I can access the HTTP API only via an access token provided gcloud auth print-access-token command, which is a JWT token, how can I get similar access token via postman, so that my mobile app can get similar token and be able to invoke cloud function? Should I set up my own OAuth server which is on GCP, if yes how?
PS: Please refer this question here

This should do the trick:
curl -i https://[REGION]-[PROJECT_ID].cloudfunctions.net/[FUNCTION_NAME] -H "Authorization: bearer $(gcloud auth print-identity-token)"
I also suggest checking the Authenticating Developers, Functions, and End-users documentation for more ways to authenticate with Google Cloud Functions.

That's how you can generate the token programmatically -
// const url = 'https://TARGET_URL';
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();
async function request() {
if (!targetAudience) {
// Use the request URL hostname as the target audience for requests.
const {URL} = require('url');
targetAudience = new URL(url).origin;
}
console.info(`request ${url} with target audience ${targetAudience}`);
const client = await auth.getIdTokenClient(targetAudience);
const res = await client.request({url});
console.info(res.data);
}
request().catch(err => {
console.error(err.message);
process.exitCode = 1;
});
Once token is generated you can pass it in header as -
Authorization: Bearer ${myToken}
Read further documentations here - https://cloud.google.com/functions/docs/securing/authenticating#functions-bearer-token-example-nodejs

Related

How to import Google Cloud credentials into a Firebase Cloud Function?

I'm trying to set up Google Cloud Translation in a Firebase Cloud Function. I'm using the demo code provided by Google Cloud Translation:
// Instantiates a client
const translationClient = new TranslationServiceClient();
const projectId = 'languagetwo-cd94d';
const location = 'global';
const text = 'Hello, world!';
async function translateText() {
// Construct request
const request = {
parent: `projects/${projectId}/locations/${location}`,
contents: [text],
mimeType: 'text/plain', // mime types: text/plain, text/html
sourceLanguageCode: 'en',
targetLanguageCode: 'es',
};
// Run request
const [response] = await translationClient.translateText(request);
for (const translation of response.translations) {
console.log(`Translation: ${translation.translatedText}`);
}
}
translateText();
This demo tutorial makes a second file called key.json:
{
"type": "service_account",
"project_id": "myAwesomeApp",
"private_key_id": "1234567890",
"private_key": "-----BEGIN PRIVATE KEY-----\noPeeking=\n-----END PRIVATE KEY-----\n",
"client_email": "translation-quickstart#myAwesomeApp.iam.gserviceaccount.com",
"client_id": "1234567890",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/translation-quickstart%40myAwesomeApp.iam.gserviceaccount.com"
}
I uploaded my credentials from the CLI:
gcloud auth login
gcloud iam service-accounts create translation-quickstart --project myAwesomeApp
gcloud projects add-iam-policy-binding myAwesomeApp
gcloud iam service-accounts keys \
create key.json --iam-account \
translation-quickstart#myAwesomeApp.iam.gserviceaccount.com
export GOOGLE_APPLICATION_CREDENTIALS=key.json
I then entered node app.js at the CLI and it runs perfectly. ¡Hola Mundo!
How do I import my credentials into a Firebase Cloud Function? I tried this:
exports.ENtranslateES = functions.firestore.document('Users/{userID}/English/Translation_Request').onUpdate((change) => { // triggers when browser writes a request word to the database
// Google Cloud
const { TranslationServiceClient } = require('#google-cloud/translate');
// Instantiates a client
const translationClient = new TranslationServiceClient();
const projectId = 'languagetwo-cd94d';
const location = 'global';
const text = 'Hello, world!';
async function translateText() {
// Construct request
const request = {
parent: `projects/${projectId}/locations/${location}`,
contents: [text],
mimeType: 'text/plain', // mime types: text/plain, text/html
sourceLanguageCode: 'en',
targetLanguageCode: 'es',
};
// Run request
const [response] = await translationClient.translateText(request);
for (const translation of response.translations) {
console.log(`Translation: ${translation.translatedText}`);
}
}
return translateText()
});
I added only a return at the bottom because Firebase Cloud Functions require that something has to be returned.
The result is that the function triggers and translateText() fires. Then I get an error message:
Error: 7 PERMISSION_DENIED: Cloud IAM permission
That looks like the credentials weren't imported. How do I import the key.json credentials into the Firebase Cloud Function?
Normally, you do not import a service account into a Google compute service such as Cloud Functions. Those services have an attached service account. There are methods of securely storing a service account using services like Google Cloud Secret Manager. In your case there is a better solution.
The following line in your source code uses the Cloud Function attached service account, which defaults to the App Engine default service account PROJECT_ID#appspot.gserviceaccount.com.
const translationClient = new TranslationServiceClient();
Since you did not specify a credential when creating the translationClient, ADC (Application Default Credentials) searches for credentials. In your example, the search found valid credentials from the Cloud Function service account.
The solution is to add the required role to that service account.
If you want to use the service account that you created, then attach the service account identity (email address) to the Cloud Function link.
Access control with IAM
I got Google Cloud Translate to work in Postman. This is a step towards an answer, not an answer. (Google has its own version of Postman, called Google Cloud API, but it doesn't work with Translate,)
I followed this blog post to set up Google Cloud API in Postman. I started at my Google Cloud Console. I selected my project. I clicked APIs & Services, then Credentials, then + Create Credentials, then OAuth client ID. Under Application type I selected Web application. I named the client ID Postman. Lastly I added an Authorized redirect URI: https://console.cloud.google.com/. Now when I click on my Postman API in my Google Cloud Console I see a Client ID and a Client secret.
In Postman, I changed GET to POST and entered the URL from the Google Cloud Translation page:
https://cloud.google.com/translate/docs/reference/rest/v2/translate
Under the Authorization tab I put in:
Token Name: GCP Token
Grant Type: Authorization Code
Callback URL: https://www.getpostman.com/oauth2/callback
Auth URL: https://accounts.google.com/o/oauth2/auth
Access Token URL: https://accounts.google.com/o/oauth2/token
Client ID: 1234567890abc.apps.googleusercontent.com
Client Secret: ABCDE-NoPeeking-1234567890
Scope: https://www.googleapis.com/auth/cloud-platform
State:
Client Authorization: Send as Basic Auth header
I then clicked Get New Access Token and an access token appeared at the top of all this. The token is good for one hour.
Under Params I entered:
q: rain
target: es
source: en
Google Cloud Translate returned lluvia.
Now I know what the auth properties and query parameters are. I don't know how to put them into a Firebase Cloud Function.

Generate JWT for service account using compute metadata from cloud function

I'm trying to generate a JWT for a given service account serviceA from a Google/Firebase Cloud function. Service account serviceB is running the function.
I got it working by using the account keys from JSON.
Given that the CF is running within Google Cloud, I want to leverage compute metadata to not having to store the private key with the functions.
I've been trying to access the metadata server for serviceA while serviceB is executing the CF. I deliberately don't want serviceA to run the CF.
The code
const request = require('request-promise');
const serviceAccountEmail = 'serviceA#<projectA>.iam.gserviceaccount.com';
const metadataServerTokenURL = `http://metadata/computeMetadata/v1/instance/service-accounts/${serviceAccountEmail}/identity?audience=<audience>`;
const tokenRequestOptions = {
uri: metadataServerTokenURL,
headers: {
'Metadata-Flavor': 'Google'
}
};
const token = await request(tokenRequestOptions);
The error
I'm currently getting a 404 not found error for the email provided
I guess it's
a) not possible what I'm trying to do, or
b) I'm missing some IAM permissions for serviceA
You can do this with the metadata server because they can only generate ID Token for the service account loaded with your instance (in this case the serviceB).
You can use another API for this: Service Account Credentials API, especially the generateIdToken method
In your case, you can do something like this (in python here)
import google.auth
from google.auth.transport.requests import AuthorizedSession
import json
# IAP audience is the ClientID of IAP-App-Engine-app in
# the API->credentials page
# Cloud Function and Cloud Run need the base URL of the service
audience = 'YOUR AUDIENCE'
# #1 Get the default credential to generate the access token
credentials, project_id = google.auth.default(
scopes='https://www.googleapis.com/auth/cloud-platform')
# #2 To use the current service account email
service_account_email = credentials.service_account_email
# Don't work with user account, so define manually the email
# service_account_email = 'MY SERVICE ACCOUNT EMAIL'
# #3 prepare the call the the service account credentials API
sa_credentials_url = f'https://iamcredentials.googleapis.com/' \
f'v1/projects/-/serviceAccounts/' \
f'{service_account_email}:generateIdToken'
headers = {'Content-Type': 'application/json'}
# Create an AuthorizedSession that includes
# automatically the access_token based on your credentials
authed_session = AuthorizedSession(credentials)
# Define the audience in the request body
# add the parameter "'includeEmail':true" for IAP access
body = json.dumps({'audience': audience})
# Make the call
token_response = authed_session.request('POST',sa_credentials_url,
data=body, headers=headers)
jwt = token_response.json()
id_token = jwt['token']
I wrote an article on this, this week
I adopted #guillaume blaquiere's solution to Typescript:
import { GaxiosOptions, Headers } from 'gaxios';
import { GoogleAuth } from 'google-auth-library';
interface TokenRequestResponse {
token: string
}
const service_account_email = 'MY SERVICE ACCOUNT EMAIL'
const audience: string = 'MY AUDIENCE';
const sa_credentials_url = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${service_account_email}:generateIdToken`
const headers: Headers = { 'Content-Type': 'application/json' }
const body = {
'audience': audience
};
const options: GaxiosOptions = {
method: 'POST',
url: sa_credentials_url,
body: JSON.stringify(body),
headers: headers,
};
const auth = new GoogleAuth({
scopes: 'https://www.googleapis.com/auth/cloud-platform'
});
const client = await auth.getClient();
const tokenResponse = await client.request(options);
const tokenRequestResponse = tokenResponse.data as TokenRequestResponse;
const token = tokenRequestResponse.token;

Executing AWS Signv4 HTTP API from Javascript returns 403 error

I'm developing a Javascript (browser) client for HTTP API's in AWS API Gateway.The API's use an IAM authorizer. In my Javascript App I log in through a Cognito Identity Pool (developer identity). Next I convert the OpenID token into an access key id, secret access key and session token using AWS.CognitoIdentityCredentials.
I then want to use these credentials to make the API call, using the code below. I see the call being executed, but I get a HTTP/403 error back. The reply does not contain any further indication of the cause. I'd appreciate all help to understand what is going wrong. When disabling the IAM authorizer, the HTTP API works nicely.
I also tried the JWT authorizer, passing the OpenID token received from the Cognito Identity Pool (using http://cognito-identity.amazon.com as provider). When doing so I get the error: Bearer scope="" error="invalid_token" error_description="unable to decode "n" from RSA public key" in the www-authenticate response header.
Thanks a lot.
// Credentials will be available when this function is called.
var accessKeyId = AWS.config.credentials.accessKeyId;
var secretAccessKey = AWS.config.credentials.secretAccessKey;
var sessionToken = AWS.config.credentials.sessionToken;
let test_url = 'https://xxxxxxx.execute-api.eu-central-1.amazonaws.com/yyyyyy';
var httpRequest = new AWS.HttpRequest("https://" + test_url, "eu-central-1");
httpRequest.method = "GET";
AWS.config.credentials = {
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
sessionToken: sessionToken
}
var v4signer = new AWS.Signers.V4(httpRequest, "execute-api");
v4signer.addAuthorization(AWS.config.credentials, AWS.util.date.getDate());
fetch(httpRequest.endpoint.href , {
method: httpRequest.method,
headers: httpRequest.headers,
//body: httpRequest.body
}).then(function (response) {
if (!response.ok) {
$('body').html("ERROR: " + JSON.stringify(response.blob()));
return;
}
$('body').html("SUCCESS: " + JSON.stringify(response.blob()));
});
After some debugging and searching the web, I found the solution. Generating a correct signature seems to require a 'host' header:
httpRequest.headers.host = 'xxxxxxx.execute-api.eu-central-1.amazonaws.com'
After adding this host header the API call succeeds.

Google Cloud Build fetch Identity token

in my scenario, I would like to trigger an Google Cloud Function based on HTTP endpoint during a Google Cloud Build. The HTTP request is done using a step with a python:3.7-slim container.
Based on this and this examples from the documentation, I wanted to use the following code:
REGION = 'us-central1'
PROJECT_ID = 'name-of-project'
RECEIVING_FUNCTION = 'my-cloud-function'
function_url = f'https://{REGION}-{PROJECT_ID}.cloudfunctions.net/{RECEIVING_FUNCTION}'
metadata_server_url = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience='
token_full_url = metadata_server_url + function_url
token_headers = {'Metadata-Flavor': 'Google'}
token_response = requests.get(token_full_url, headers=token_headers)
jwt = token_response.text
print(jwt)
r = requests.post(url=function_url, headers=function_headers, json=payload)
Surprisingly, the code fails because jwt is Not Found (according to the print statement).
I already tested the code and IAM settings by hard coding a valid identity token and also tested the exact same fetching mechanism on a test VM inside the same project.
The problem seems to be that the meta data fetching some is not working inside cloud build.
Am I missing something?
Thank you for any help!
The solution is to use a new IAM api to generate an ID_TOKEN, on a service account with an access token, if the requester (this one who generate the access token) has the role Service Account Token Creator on the service account (or widely on the project).
This first example use direct API calls
- name: gcr.io/cloud-builders/gcloud
entrypoint: "bash"
args:
- "-c"
- |
curl -X POST -H "content-type: application/json" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-d '{"audience": "YOUR AUDIENCE"}' \
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/YOUR SERVICE ACCOUNT:generateIdToken"
# Use Cloud Build Service Account
# service_account_email=$(gcloud config get-value account)
And here the Python code version
- name: python:3.7
entrypoint: "bash"
args:
- "-c"
- |
pip3 install google-auth requests
python3 extract-token.py
And extract-token.py content the following code
REGION = 'us-central1'
PROJECT_ID = 'name-of-project'
RECEIVING_FUNCTION = 'my-cloud-function'
function_url = f'https://{REGION}-{PROJECT_ID}.cloudfunctions.net/{RECEIVING_FUNCTION}'
import google.auth
credentials, project_id = google.auth.default(scopes='https://www.googleapis.com/auth/cloud-platform')
# To use the Cloud Build service account email
service_account_email = credentials.service_account_email
#service_account_email = "YOUR OWN SERVICE ACCOUNT"
metadata_server_url = f'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{service_account_email}:generateIdToken'
token_headers = {'content-type': 'application/json'}
from google.auth.transport.requests import AuthorizedSession
authed_session = AuthorizedSession(credentials)
import json
body = json.dumps({'audience': function_url})
token_response = authed_session.request('POST',metadata_server_url, data=body, headers=token_headers)
jwt = token_response.json()
print(jwt['token'])
Don't hesitate if you need more details.
I think I will write an article on this on Medium, if you want I name you, let me know
The best here is to create a Feature Request (FR) in the Public Issue Tracker. There is a difference between filing an issue and a FR. The FR gives visibility to the Engineering team of the real needs; according to the number of users which are being affected by that, they prioritize the development of them. I suggest also to create a guthub repo so they can easily replicate it and make reference to the issues aforementioned.
On the other hand as a workaround, you can create a topic in Pub/Sub to receive build notifications:
gcloud pubsub topics create cloud-builds
Each time you submit a build, a message will be pushed to the topic, then you can create a PubSub Cloud Function and call your HTTP CF from there.
I used this example from github, mentioned in the docs Authenticating Function to function
const {get} = require('axios');
// TODO(developer): set these values
const REGION = 'us-central1';
const PROJECT_ID = 'YOUR PROJECTID';
const RECEIVING_FUNCTION = 'FUNCTION TO TRIGGER';
// Constants for setting up metadata server request
// See https://cloud.google.com/compute/docs/instances/verifying-instance-identity#request_signature
const functionURL = `https://${REGION}-${PROJECT_ID}.cloudfunctions.net/${RECEIVING_FUNCTION}`;
const metadataServerURL =
'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=';
const tokenUrl = metadataServerURL + functionURL;
exports.helloPubSub = async (event, context) => {
// Fetch the token
const message = event.data
? Buffer.from(event.data, 'base64').toString()
: 'Hello, World';
const tokenResponse = await get(tokenUrl, {
headers: {
'Metadata-Flavor': 'Google',
},
});
const token = tokenResponse.data;
// Provide the token in the request to the receiving function
try {
const functionResponse = await get(functionURL, {
headers: {Authorization: `bearer ${token}`},
});
console.log(message);
} catch (err) {
console.error(err);
}
};
Finally, when the ClouBuild submits, your PubSub CF will be triggered and you can call your CF inside it.

Google function HTTP trigger - authentication problem server to server with service account

What i want to do: To call a google function from my server/machine & limit it usage with a (simple) authentication.
What i use: Node.js, google-auth-library library for authentication.
What have I done/tried:
1) Created a project in Google Cloud Functions
2) Created a simple google function
exports.helloWorld = (req, res) => {
let message = req.query.message || req.body.message || 'Hello World!';
res.status(200).send(message);
};
3) Set my custom service account
4) Enabled api:
- Cloud Functions API
- IAM Service Account Credentials API
- Cloud Run API
- Compute Engine API
- IAM Service Account Credentials API
5) Given to my server account necessary authorization (project owner, cloud function admin, IAM project admin... (need more?)
6) Generated key from my service account and saved it in json format
NB: with allUser permission (without authorization required), i can call my endpoint without problem
7) From my project i tried to auth my function in this way
const { JWT } = require('google-auth-library');
const fetch = require('node-fetch');
const keys = require('./service-account-keys.json');
async function callFunction(text) {
const url = `https://europe-west1-myFunction.cloudfunctions.net/test`;
const client = new JWT({
email: keys.client_email,
keyFile: keys,
key: keys.private_key,
scopes: [
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/iam',
],
});
const res = await client.request({ url });
const tokenInfo = await client.getTokenInfo(client.credentials.access_token);
try {
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${client.credentials.access_token}`,
},
});
if (response.status !== 200) {
console.log(response);
return {};
}
return response.json();
} catch (e) {
console.error(e);
}
}
ℹ️ if i try to pass at client.request() url without name of function (https://europe-west1-myFunction.cloudfunctions.net), i not received error, but when use the JWT token obtained in fetch call, i received the same error.
RESULT:
Error:
<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>401 Unauthorized</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Unauthorized</h1>
<h2>Your client does not have permission to the requested URL <code>/test1</code>.</h2>
<h2></h2>
</body></html>
❓ How do I call a google function with any protection to prevent anyone from using it? (I don't need specific security, just that random users don't use it)
Thanks in advance for any help
When you call a private function (or a private Cloud Run) you have to use a google signed identity token.
In your code, you use an access token
headers: {
Authorization: `Bearer ${client.credentials.access_token}`,
},
Access token work when you have to request Google Cloud API, not your services
And the google signed is important, because you can easily generate a self signed identity token with the google auth lib, but it won't work
You have code sample here and I wrote a tool in Go if you want to have a try on it
** EDIT **
I worked on an example, and, even if I never liked Javascript, I have to admit that I'm jealous!! It's so simple in Node!!
Here my working example
const {GoogleAuth} = require('google-auth-library');
async function main() {
// Define your URL, here with Cloud Run but the security is exactly the same with Cloud Functions (same underlying infrastructure)
const url = "https://go111-vqg64v3fcq-uc.a.run.app"
// Here I use the default credential, not an explicit key like you
const auth = new GoogleAuth();
//Example with the key file, not recommended on GCP environment.
//const auth = new GoogleAuth({keyFilename:"/path/to/key.json"})
//Create your client with an Identity token.
const client = await auth.getIdTokenClient(url);
const res = await client.request({url});
console.log(res.data);
}
main().catch(console.error);
Note: Only Service account can generate and identity token with audience. If you are in your local computer, don't use your user account with the default credential mode.