I have a google cloud function providing a Google Chat App. My GCF receives calls from Google Chat when users type input. It doesn't call Chat; it exposes a URL and Chat calls it, it computes for a while, and it sends a response back to Chat. So the GCF does not need authentication to work with Chat.
I seem to need to configure my Google Cloud Function to accept network requests to "Allow All Traffic". Google Chat does not seem to be part of my Google Cloud Project, so I cannot configure the connection into the GCF to be "Allow Internal Traffic Only". So anyone learning the URL of my GCF could flood it with fake traffic.
Ideally, I would like the Google networking system to only permit access to my function from Google Chat, but I cannot find a way to do this using a service account in Google IAM. Can I do this somehow?
Looking at the headers of the access from Chat to my GCF, it does contain an authorization: 'Bearer eyJhbGciO...'
header. That turns out to be a JSON Web Token. That says
headers: {
"alg": "RS256",
"kid": "06ea3d3c9414b34d77d66407580cec7e10c0b7d3",
"typ": "JWT"
}
payload: {
"aud": "939021344830",
"exp": 1680195982,
"iat": 1680192382,
"iss": "chat@system.gserviceaccount.com"
}
so it looks like that is a token generated by Google Chat and signed by Google to demonstrate that the source was indeed Google Chat. The kid
should identify the key I need to validate the JWT. How can I verify that this token is properly signed by Google?
I looked at Secure Google Cloud Functions http trigger with auth but the solutions there do not seem to be applicable because I do not have control over how Google Chat makes the call to my GCF.
Thanks!
Here is my code to check the JWT, where token
is the authorisation header stripped of the bearer
prefix. This seems to be working.
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://www.googleapis.com/service_accounts/v1/metadata/jwk/chat%40system.gserviceaccount.com',
});
const promiseToVerify = (token) => {
return new Promise((resolve, reject) => {
const decoded = jwt.decode(token, {complete: true});
if (!decoded || !decoded.header || !decoded.header.kid) {
return reject(new Error('Invalid token'));
}
client.getSigningKey(decoded.header.kid, (err, key) => {
if (err) {
return reject(err);
}
const signingKey = key.publicKey || key.rsaPublicKey;
jwt.verify(token, signingKey, (err, decoded) => {
if (err) {
return reject(err);
}
resolve(decoded);
});
});
});
};
The aud
which appears in the JWT is the project ID of a folder called system-gsuite
which appears in our google cloud console project selector dropdown. I have not yet been able to find if that ID is unique to our code. aud
needs checking to make sure it is the right value, as well as validating the JWT signature. jwt.verify
above does check the validity time and complains if the JWT has expired.
Using this code, we can be confident that the JWT was signed by google, because the kid
has to be
that of a public key found at a google-controlled URL specific to Google Chat. And the aug
is the ID of a google project
known to our google cloud console. And the token has not yet expired.