Google Sign-In: backend verification - node.js

I have Google Sign-in working on my app: the relevant code is roughly:
var acc = await signInService.signIn();
var auth = await acc.authentication;
var token = auth.idToken;
This gives me a nice long token, which I then pass to my backend with an HTTP POST (this is working fine), and then try to verify. I have the same google-services.json file in my flutter tree and on the backend server (which is nodejs/restify). The backend code is roughly:
let creds = require('./google-services.json');
let auth = require('google-auth-library').OAuth2Client;
let client = new auth(creds.client[0].oauth_client[0].client_id);
. . .
let ticket = await client.verifyIdToken({
idToken: token,
audience: creds.client[0].oauth_client[0].client_id
});
let payload = ticket.getPayload();
This consistently returns my the error "Wrong recipient, payload audience != requiredAudience".
I have also tried registering separately with GCP console and using those keys/client_id instead, but same result. Where can I find the valid client_id that will properly verify this token?

The problem here is the client_id that is being used to create an OAuth2Client and the client_id being used as the audience in the verifyIdToken is the same. The client_id for the audience should be the client_id that was used in your frontend application to get the id_token.
Below is sample code from Google documentation.
const {OAuth2Client} = require('google-auth-library');
const client = new OAuth2Client(CLIENT_ID);
async function verify() {
const ticket = await client.verifyIdToken({
idToken: token,
audience: CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
const payload = ticket.getPayload();
const userid = payload['sub'];
// If request specified a G Suite domain:
//const domain = payload['hd'];
}
verify().catch(console.error);
And here is the link for the documentation.
Hope this helps.

Another quick solution might be change the name of your param "audience" to "requiredAudience". It works to me. If you copied the code from google, maybe the google documentation is outdated.
client.verifyIdToken({
idToken,
requiredAudience: GOOGLE_CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});

It has already been mentioned above that requiredAudience works instead of audience, but I noticed requiredAudience works for both {client_id : <CLIENT_ID>} and <CLIENT_ID>. So maybe you were referencing creds.client[0].oauth_client[0] instead of creds.client[0].oauth_client[0].client_id? I have not been able to find any docs on the difference between requiredAudience and audience, however make sure you are sending just the <CLIENT_ID> instead of {client_id : <CLIENT_ID>}.
Google doc: link

verifyIdToken()'s call signature doesn't require the audience parameter. That's also stated in the changelog. So you can skip it, and it'll work. The documentation is kinda misleading on that.
It's also the reason why using requiredAudience works because it actually isn't being used by the method, so it's the same as not providing it.

I've been faceing this issue with google-auth-library version 8.7.0 and came across a workaround only if you have a single CLIENT_ID to verify.
Once you create your OAuth2Client like this:
const googleClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
You don't need to pass the CLIENT_ID in verifyIdToken function as it uses your googleClient object to create auth url.

Related

How do I call Google Analytics Admin API (for GA4) using an OAuth2 client in node.js?

I've noticed that all the node.js code samples for Google Analytics Admin and Google Analytics Data assume a service account and either a JSON file or a GOOGLE_APPLICATION_CREDENTIALS environment variable.
e.g.
const analyticsAdmin = require('#google-analytics/admin');
async function main() {
// Instantiates a client using default credentials.
// TODO(developer): uncomment and use the following line in order to
// manually set the path to the service account JSON file instead of
// using the value from the GOOGLE_APPLICATION_CREDENTIALS environment
// variable.
// const analyticsAdminClient = new analyticsAdmin.AnalyticsAdminServiceClient(
// {keyFilename: "your_key_json_file_path"});
const analyticsAdminClient = new analyticsAdmin.AnalyticsAdminServiceClient();
const [accounts] = await analyticsAdminClient.listAccounts();
console.log('Accounts:');
accounts.forEach(account => {
console.log(account);
});
}
I am building a service which allows users to use their own account to access their own data, so using a service account is not appropriate.
I initially thought I might be able to use the google-api-node-client -- Auth would be handled by building a URL to redirect and do the oauth dance...
Using google-api-nodejs-client:
const {google} = require('googleapis');
const oauth2Client = new google.auth.OAuth2(
YOUR_CLIENT_ID,
YOUR_CLIENT_SECRET,
YOUR_REDIRECT_URL
);
// generate a url that asks permissions for Google Analytics scopes
const scopes = [
"https://www.googleapis.com/auth/analytics", // View and manage your Google Analytics data
"https://www.googleapis.com/auth/analytics.readonly", // View your Google Analytics data
];
const url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes
});
// redirect to `url` in a popup for the oauth dance
After auth, Google redirects to GET /oauthcallback?code={authorizationCode}, so we collect the code and get the token to perform subsequent OAuth2 enabled calls:
// This will provide an object with the access_token and refresh_token.
// Save these somewhere safe so they can be used at a later time.
const {tokens} = await oauth2Client.getToken(code)
oauth2Client.setCredentials(tokens);
// of course we need to handle the refresh token too
This all works fine, but is it possible to plug the OAuth2 client from the google-api-node-client code into the google-analytics-admin code?
👉 It looks like I need to somehow call analyticsAdmin.AnalyticsAdminServiceClient() with the access token I've already retrieved - but how?
The simple answer here is don't bother with the Node.js libraries for Google Analytics Admin & Google Analytics Data.
Cut out the middleman and build a very simple wrapper yourself which queries the REST APIs directly. Then you will have visibility on the whole of the process, and any errors made will be your own.
Provided you handle the refresh token correctly, this is likely all you need:
const getResponse = async (url, accessToken, options = {}) => {
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response;
};
I use Python but the method could be similar. You should create a Credentials object based on the obtained token:
credentials = google.auth.credentials.Credentials(token=YOUR_TOKEN)
Then use it to create the client:
from google.analytics.admin import AnalyticsAdminServiceClient
client = AnalyticsAdminServiceClient(credentials=credentials)
client.list_account_summaries()

Endpoint to fetch Subreddits of a Reddit Account

I have completed the oauth flow for my third party app against a Reddit account and I've gotten the access token for the account.
Now my next issue is
How can I fetch the subreddits for an account using the access token
I can't seem to figure out the endpoint for that.
Does anyone know the endpoint for that?
Thank you
The Reddit OAuth Docs say that for the /subreddits/mine/(where) endpoint, the subreddits OAuth scope is necessary.
Once that scope is acquired for a user, you can use the following snippets of code to access the list of subscribed subreddits for the user:
View a users subreddits                                                                                
View in Fusebit
// Demonstrate using snooclient and Fusebit
const subscriptions = await redditClient.getSubscriptions().fetchAll();
// OR fetch the first page using a raw HTTP request
// - the User-Agent is necessary, don't forget it!
const access_token = redditClient.fusebit.credentials.access_token;
const httpSubs = await superagent.get(
'https://oauth.reddit.com/subreddits/mine/subscriber')
.set('Authorization', `Bearer ${access_token}`)
.set('User-Agent', 'Fusebit Integration');
const length = httpSubs.body.data.children.length;
ctx.body = {
usingSnoo: `User has ${subscriptions.length} subreddits`,
usingHttp: `The first page has ${length} subreddits`,
};
});

How to specify a domain name while verifying google id token

I am writing a function to verify a Google id token. I'm strictly following the documentation but I'm stuck on one last step. The token must be from a particular G-suite domain. The code snippet below is from the documentation but I don't know how to interpret it. Do I replace hd with mydomain.com? How do I specify my domain name?
// If request specified a G Suite domain:
// const domain = payload['hd'];
Kindly let me know if there is any additional detail I omitted in this question. There is no issue with my current code but I'll just add a little snippet here for context:
const token = getToken(event);
const client = new OAuth2Client(CLIENT_ID);
async function verify() {
const ticket = await client.verifyIdToken({
idToken: token,
audience: CLIENT_ID
});
const payload = ticket.getPayload();
const userid = payload['sub'];
}
// const domain = payload['hd'];
verify().catch(console.error);
'hd' is the key for the domain value in the payload ... if it's set, then domain = payload['hd'] will give you the user's domain as a string and you can check that it matches yourdomain.com

Verify JWT from Google Chat POST request

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.
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.
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

DocuSign Node SDK not returning loginInfo in Production

I've built out an integration using DocuSign's Node SDK. While testing using a DocuSign sandbox account, the authentication flow works just fine using the example in the docs.
I'm now trying to do the same within a live DocuSign production account using the Integrator Key that was promoted from the sandbox account. authApi.login() seems to work just fine, I get no error and the status code of the response is 200. However, the value of loginInfo comes back as exports {} with no account info included.
I've made sure to change the base path from https://demo.docusign.net/restapi to www.docusign.net/restapi and as far as I can tell from the docs, there doesn't seem to be anything else I need to make the switch to production. Here is the code I am using:
apiClient.setBasePath('www.docusign.net/restapi');
apiClient.addDefaultHeader('Authorization', 'Bearer ' + token);
docusign.Configuration.default.setDefaultApiClient(apiClient);
const authApi = new docusign.AuthenticationApi();
const loginOps = {
apiPassword: true,
includeAccountIdGuid: true
};
authApi.login(loginOps, function (err, loginInfo, response) {
if (err) {
console.log(err);
}
if (loginInfo) {
// loginInfo returns 'exports {}' so the variables below cannot be set.
const loginAccounts = loginInfo.loginAccounts;
const loginAccount = loginAccounts[0];
const baseUrl = loginAccount.baseUrl;
const accountDomain = baseUrl.split('/v2');
const accountId = loginAccount.accountId;
apiClient.setBasePath(accountDomain[0]);
docusign.Configuration.default.setDefaultApiClient(apiClient);
www.docusign.net endpoint will only work if your PROD account is in NA1, if your PROD Account is in NA2, then you need to use na2.docusign.net and if it is in NA3 then na3.docusign.net. This is the main reason you should use /oauth/userinfo call with OAUTH2 Access Token to know your base URL, and then call all APIs with this baseURL. You can find more details at https://docs.docusign.com/esign/guide/authentication/userinfo.html

Resources