4

I have a bot in NodeJS connected to Google Chat using HTTPs endpoints. I am using express to receive requests. I need to verify that all requests come from Google, and want to do this using the Bearer Token that Google Sends with requests.

My problem is that I am struggling to find a way to verify the tokens.

I have captured the token and tried a GET reuqes to https://oauth2.googleapis.com/tokeninfo?id_token=ey... (where ey... is the token start).

Which returns:

    "error": "invalid_token",
    "error_description": "Invalid Value"
}

I have tried what Google recommends:

var token = req.headers.authorization.split(/[ ]+/);
client.verifyIdToken({
    idToken: token[1],
    audience: JSON.parse(process.env.valid_client_ids)
}).then((ticket) => {
    gchatHandler.handleGChat(req.body, res);
}).catch(console.error);

And get the following error:

Error: No pem found for envelope: {"alg":"RS256","kid":"d...1","typ":"JWT"}

Any idea where I should head from here?

Edit: https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com found this, investigating how to use it. The kid matches the one I get.

Jed
  • 121
  • 6

2 Answers2

6

Worked it out, eventually.

You need to hit: https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com to get a JSON file containing the keys linked to their KIDs.

Then when a request arrives, use jsonwebtoken (NPM) to decode the token and extract the KID from the header.

Use the KID to find the matching public key in the response from the website above, then use the verify function to make sure the token matches the public key.

You also need to pass the audience and issuer options to verify, to validate that it is your particular service account hitting the bot.

Jed
  • 121
  • 6
0

The solution above maybe the correct for Google Chat, but in my experience Google services (e.g. Google Tasks) use OIDC tokens, which can be validated with verifyIdToken function.

Adding my solution here, since your question/answer was the closest thing I was able to find to my problem

So, In case if you need to sign a request from your own code

on client, send requests with OIDC token

import {URL} from 'url';
import {GoogleAuth} from 'google-auth-library'; 

// will use default auth or GOOGLE_APPLICATION_CREDENTIALS path to SA file
// you must validate email of this identity on the server!
const auth = new GoogleAuth({});

export const request = async ({url, ...options}) => {
  const targetAudience = new URL(url as string).origin;
  const client = await auth.getIdTokenClient(targetAudience);
  return await client.request({...options, url});
};

await request({ url: 'https://my-domain.com/endpoint1', method: 'POST', data: {} })

on the server, validate OIDC (Id token)


const auth = new OAuth2Client();

const audience = 'https://my-domain.com';


// to validate
const token = req.headers.authorization.split(/[ ]+/)[1];
const ticket = await auth.verifyIdToken({idToken: token, audience });

if (ticket.getPayload().email !== SA_EMAIL) {
 throw new Error('request was signed with different SA');
}
// all good

Read more about Google OpenID Connect Tokens

romanlv
  • 1,432
  • 15
  • 15