unable to get a response with people.connections.list - node.js

⚠️ I forgot a process.exit(0) in the main thread, so the app was terminated before the callback was executed. This code sample works like a charm.
Here is the code from googleapis nodejs client I have issue on:
First thing first, I would like to get the list of contacts for one user using a nodejs application.
Set up a OAuth2Client
So I set up a OAuth2Client with this code:
const {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
} = require("./keys.json");
const REDIRECT_URL = "http://localhost:3000/oauth2callback";
const oAuth2Client = new OAuth2Client(CLIENT_ID, CLIENT_SECRET, REDIRECT_URL);
Then, using a temporary server, I ask for token using the user's credentials:
function getGoogleCode() {
return new Promise((resolve, reject) => {
// Open an http server to accept the oauth callback. In this simple example, the
// only request to our webserver is to /oauth2callback?code=<code>
const server = http
.createServer(async (req, res) => {
if (req.url.indexOf("/oauth2callback") > -1) {
// acquire the code from the querystring, and close the web server.
const { code } = querystring.parse(url.parse(req.url).query);
res.end(
`Authentication successful! Please return to the console. [code: ${code}]`
);
server.close();
resolve(code);
}
reject(new Error("oops", req, res));
})
.listen(3000, () => {
// open the browser to the authorize url to start the workflow
// Generate the url that will be used for the consent dialog.
const authorizeUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
scope: ["https://www.googleapis.com/auth/contacts.readonly"]
});
opn(authorizeUrl);
});
});
}
Then I finish to set up my client:
const code = await getGoogleCode();
const { tokens } = await oAuth2Client.getToken(code);
oAuth2Client.setCredentials(tokens);
When everything's fine
I managed to get a response with the low level API:
const personFields = ["emailAddresses", "names"];
const url = `https://people.googleapis.com/v1/people/me/connections?personFields=${personFields.join(
","
)}`;
const res = await oAuth2Client.request({ url });
console.log(chalk.gray(JSON.stringify(res.data.connections, null, 2)));
Everything is working like a charm, but, I would like to use the high level API from the same library
google.people API
As described in API Explorer, I build the code below:
const personFields = ["emailAddresses", "names"];
people.people.connections.list(
{ resourceName: "people/me", personFields },
(res, err) => {
console.log("error: ", err);
console.log("res1: ", res);
}
);
No error, no res1, nothing.
⚠️ I forgot a process.exit(0) in the main thread, so the app was terminated before the callback was executed. This code sample works like a charm.

Not sure why you aren't getting logging. But you probably should return the res in the callback, otherwise calling await on the callback will return undefined.
Also make sure to set personFields in the people.people.connections.list call.

Related

How can i check if google API is authenticated/authorized with OAuth2 using Node?

Sorry if this isn't worded correctly I am very new to Node and to GoogleAPI.
I have an app with a button to login/authorize GoogleAPI through OAuth 2.0 so that I can add events to the calendar.
This all works perfectly fine. I can successfully login and add events to the calendar. I will add my code for this at the end in case it is relevant.
My trouble is that I would like to run a check on the GET request of the page that has the button to check if the GoogleAPI is connected/authorized so that I can show the button and give a warning to log in/authorize if it is not, and to hide the button if it is.
For the life of me, I have not been able to figure this out, I've been stuck on it for days now and have tried many many different approaches.
I have read the documentation, but it is not clear and I don't understand it too well.
I thought maybe it has something to do with validating an ID token? But I'm not sure, and couldn't work out how to do that.
If you have any advice it would be greatly appreciated!!!
Here is the code for the initial authorization that works fine in case it is helpful:
const oAuth2Client = new google.auth.OAuth2(
CLIENT_ID,
CLIENT_SECRET,
REDIRECT_URL,
);
exports.googleAuthCode = (req, res) => {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
console.log('authURL: ',authUrl)
res.redirect(authUrl);
}
exports.googleRedirect = async (req, res) => {
const code = req.query.code;
const {tokens} = await oAuth2Client.getToken(code)
oAuth2Client.setCredentials(tokens);
req.session.tokens = tokens;
res.redirect('/jobs?message=Success')
}
In response to #DaImTo it does not request authorizaiton if it is not already authorized it simply throws the error An error occurred: Error: No access, refresh token, API key or refresh handler callback is set. and refreshes the page.
Here is the code for the GET request that adds the events, maybe I have set it up wrong?
exports.scheduleEvent = async (req, res) => {
const date = req.query['date'];
let events = await Events.findOne({}).sort({_id: -1});
events = events.events
events.forEach(async (event) => {
try {
const result = await calendar.events.insert({
auth: oAuth2Client,
calendarId: 'primary',
resource: event,
});
console.log(`Event created: ${result.data.htmlLink}`);
} catch (err) {
console.error(`An error occurred: ${err}`);
}
});
res.send({
msg: "Done"
})
}

MSAL Node / Electron - PublicClientApplication.acquireTokenByCode() accessing Tokens despite clearing accounts

I'm trying to clear all access Tokens from Main.Ts in an Electron App without success.
On the Node side of the App, I have a login function that implements the Auth Code Flow with PKCE - which works fine:
let login = async () => {
try {
const authResult = await getTokenInteractive(authCodeUrlParams);
mainWindow.webContents.send(IpcMessages.XXX-XXXX, authResult);
} catch (error) {
console.log(error);
}
};
let getTokenInteractive = async (
tokenRequest: AuthorizationUrlRequest
): Promise\<AuthenticationResult\> =\> {
try {
...
// Generate PKCE Challenge and Verifier before request
const cryptoProvider = new CryptoProvider();
const { challenge, verifier } = await cryptoProvider.generatePkceCodes();
...
// Get Auth Code URL
const authCodeUrl = await clientApplication.getAuthCodeUrl(
localAuthCodeUrlParams
);
const authCode = await listenForAuthCode(authCodeUrl, authWindow);
...
// This works fine
const authResult: AuthenticationResult =
await clientApplication.acquireTokenByCode({
...authCodeRequest,
code: authCode,
codeVerifier: verifier,
});
return authResult;
} catch (error) {
throw error;
}
};
The logout function also clears the accounts:
const logout = async () =\> {
try {
console.log("LOGOUT RECEIVED");
const accounts: AccountInfo\[\] = await clientApplication
.getTokenCache()
.getAllAccounts();
if (accounts && accounts.length \> 0) console.log(accounts);
await clientApplication.getTokenCache().removeAccount(accounts\[0\]);
// Test to see if accounts are cleared returns '[]'
const checkAccounts: AccountInfo[] = await clientApplication
.getTokenCache()
.getAllAccounts();
console.log(checkAccounts);
mainWindow.webContents.send(IpcMessages.USER_LOGGED_OUT);
} catch (err) {
console.log("Error in main.logout ", err);
}
};
Unfortunately, acquireTokenByCode still accesses the Token (?? How) when the user is redirected to a login page.
ps I've also checked local storage, Cookies etc in the browser and they are never populated (ie always remain empty).
I've seen numerous answers suggesting using a logout URL:
https://learn.microsoft.com/en-us/answers/questions/263335/msal-sign-out-does-not-appear-to-clear-cache.html
https://learn.microsoft.com/en-us/answers/questions/994601/logout-does-not-clear-session.html
However, these solutions down't work with Azure B2C. Also, redirecting using the logout endpoint using Electron.shell(openExternal([url])) also does nothing.
It's unbelievable that MS hasn't implemented a simple logout solution.
Question: Does anyone know how to stop tokens from being acquired WITHOUT having to open a native browser window or populate the Browser Window with a "Successfully signed out. You can now close this window" prompt ?
Thanks in advance.

Oauth2 with Google docs API Node.js (trying to programmatically write a new google doc)

I have a typical web app with a client and a node.js server. When a user selects an option on the client, I want to export (create) a google doc in their drive.
I am half way there following this tutorial https://developers.google.com/identity/protocols/oauth2/web-server
With my current set up, after the user authenticates, the authentication token is sent to a web hook (server side), but I don't have any of the data for creating the google doc there.
If I did, I could create the doc from there. Otherwise, I need to send the token itself back to the client so I can create the doc with the necessary payload from there.
In that case, I don't know how to signal to the client that the user has been authenticated. Should I use a web socket?
Something tells me that my general set up might not be correct, and that I should be doing it a different way in my use case.
This is my client side code that brings the user to the google auth page after getting the auth url (not sure if this really needs to be done server side, but since I have some user credentials I thought it might be better).
export async function exportToGoogleDoc() {
const response = await POST(`${API_URL}export/gdoc`, {
'hello': 'world'
});
if (response.status == 200){
window.location.href = response.authUrl;
}
}
And then the endpoint (just returns the autheticationUrl)
api.post('/export/gdoc', express.raw({ type: 'application/json' }), async (req, res, next) => {
try {
const scopes = [
'https://www.googleapis.com/auth/drive'
];
const oauth2Client = new google.auth.OAuth2(
credentials.web.client_id,
credentials.web.client_secret,
credentials.web.redirect_uris[0]
);
const authorizationUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes,
include_granted_scopes: true
});
res.json({ 'status': 200, authUrl : authorizationUrl } );
} catch (err){
next(err);
}
});
But then after the user agrees and authenticates with their google account, the auth token is sent to this web hook. At the bottom I am able to write an empty google doc to the authenticated google drive, but I don't have the data I need to create the real doc.
api.get('/auth/google', express.raw({ type: 'application/json' }), async (req, res, next) => {
const q = url.parse(req.url, true).query;
const oauth2Client = new google.auth.OAuth2(
credentials.web.client_id,
credentials.web.client_secret,
credentials.web.redirect_uris[0]
);
if (q.error) {
console.log('Error:' + q.error);
} else {
const { tokens } = await oauth2Client.getToken(q.code.toString());
oauth2Client.setCredentials(tokens);
const drive = google.drive('v3');
const requestBody = {
'name': 'Dabble',
'mimeType': 'application/vnd.google-apps.document',
};
drive.files.create({
requestBody: requestBody,
fields: 'id',
auth: oauth2Client
}, function (err, file) {
if (err) {
// Handle error
console.error(err);
} else {
console.log('File Id: ', file);
}
});
}
Somehow I either need to get the data for the google doc inside this web hook, or to listen for this web hook from the client.
OR I need an entirely different set up. I realize I also should be probably storing this token somewhere (local storage on client side?) and only making this call if they do not have a token.
Can anyone help me modify my set up? Thanks!

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

Post Request Firebase Cloud Functions Timing Out

I know that each HTTP function must end with end() or send(), so I'm thinking that might be related to my issue. I'm building a Shopify app that I want to host on Firebase. I've gotten it to authenticate and install, but when I try to capture the permanent access token via POST, Firebase times out. This same code works fine with ngrok. Entire route function below.
const dotenv = require('dotenv').config();
const functions = require('firebase-functions');
const express = require('express');
const app = express();
const crypto = require('crypto');
const cookie = require('cookie');
const nonce = require('nonce')();
const querystring = require('querystring');
const request = require('request-promise');
const apiKey = process.env.SHOPIFY_API_KEY;
const apiSecret = process.env.SHOPIFY_API_SECRET;
const scopes = 'read_products,read_customers';
const forwardingAddress = 'https://my-custom-app.firebaseapp.com/app';
app.get('/app/shopify/callback', (req, res) => {
const { shop, hmac, code, state } = req.query;
const stateCookie = cookie.parse(req.headers.cookie).__session;
if (state !== stateCookie) {
return res.status(403).send('Request origin cannot be verified');
}
if (shop && hmac && code) {
// DONE: Validate request is from Shopify
const map = Object.assign({}, req.query);
delete map['signature'];
delete map['hmac'];
const message = querystring.stringify(map);
const generatedHash = crypto
.createHmac('sha256', apiSecret)
.update(message)
.digest('hex');
if (generatedHash !== hmac) {
return res.status(400).send('HMAC validation failed');
}
// Collect permanent access token
const accessTokenRequestUrl = 'https://' + shop + '/admin/oauth/access_token';
const accessTokenPayload = {
client_id: apiKey,
client_secret: apiSecret,
code,
};
// Everything works up until here
request.post(accessTokenRequestUrl, { json: accessTokenPayload })
.then((accessTokenResponse) => {
const accessToken = accessTokenResponse.access_token;
// If below is uncommented, it will not show on browser, Firebase seems to timeout on the above request.post.
//res.status(200).send("Got an access token, let's do something with it");
// Use access token to make API call to 'shop' endpoint
const shopRequestUrl = 'https://' + shop + '/admin/shop.json';
const shopRequestHeaders = {
'X-Shopify-Access-Token': accessToken,
};
request.get(shopRequestUrl, { headers: shopRequestHeaders })
.then((shopResponse) => {
res.end(shopResponse);
})
.catch((error) => {
res.status(error.statusCode).send(error.error.error_description);
});
})
.catch((error) => {
res.status(error.statusCode).send(error.error.error_description);
});
} else {
res.status(400).send('Required parameters missing');
}
});
exports.shopifyValidate = functions.https.onRequest(app);
You're calling response.end() incorrectly:
request.get(shopRequestUrl, { headers: shopRequestHeaders })
.then((shopResponse) => {
res.end(shopResponse);
})
As you can see from the linked documentation, end() doesn't take a parameter. It just ends the response. You probably want to be calling send() instead if you have data to send.
If you're unsure how your function is executing, also use console.log() to log messages to figure out exactly what it's doing. It's rarely a good idea to just hope that a bunch of code is just working - you should verify that it's working the way you expect.
Solved. Turns out you need a paid plan (Blaze, pay as you go) to access external APIs. I upgraded and that solved the issue.
What is the request module that you are using for the request.post()
Please see : https://www.npmjs.com/package/request#promises--asyncawait
I hope you are using the https://github.com/request/request-promise module instead of request.

Resources