How to authenticate Google Calendar API from Dialogflow Fulfillment using OAuth2 Client? - node.js

I am trying to build Google Calendar assistant chatbot with Dialogflow fulfillment hosted in GCP using node.js, dialogflow-fulfillment, and googleapis client libraries. I have a problem to create an authentication method using OAuth Client ID. The idea is when the user adds the bot in Google Chat the bot should greet him/her and ask the user for permission for the defined scopes (to create events in one's Google Calendar in this case). What I currently managed to do is to send the user a link where the one will see the scopes, approve those and a code will be generated, but then this code should be passed back to the function to get the token and set the credentials.
link sent to the user
generated code
code passed to the user
Is there a way to get this code automatically and authenticate the user?
My code looks like this (it's a bit messy because of all the tests that I made):
const {google} = require('googleapis');
const {WebhookClient} = require('dialogflow-fulfillment');
const credentials = {"installed":{"client_id":"618408396856-vrd3it4s4nk19tlo7qrnbb51a9f8bq6t.apps.googleusercontent.com","project_id":"pg-xc-n-app-577847","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_secret":"d_qDDlFVBtllcotgn2xvc00N","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}};
//setting authentication details
const SCOPES = [
'https://www.googleapis.com/auth/calendar.events',
'https://www.googleapis.com/auth/spreadsheets'
];
const {client_secret, client_id, redirect_uris} = credentials.installed;
const authentication = new google.auth.OAuth2(
client_id,
client_secret,
redirect_uris[0]
);
const url = authentication.generateAuthUrl({
access_type: 'offline',
scope: SCOPES
});
const calendarId = 'primary';
const calendar = google.calendar('v3');
process.env.DEBUG = 'dialogflow:*'; // enables lib debugging statements
exports.meetingRoomFulfillment = function meetingRoomFulfillment(req, res) {
const agent = new WebhookClient({ request: req, response: res });
console.log(`Intent ${((req.body.queryResult || {}).intent || {}).displayName}`);
console.log(`Dialogflow Request body`, JSON.stringify(req.body));
if (req.body.queryResult === undefined || req.body.queryResult.intent === undefined || req.body.queryResult.intent.displayName === undefined) {
console.log(`Missing intent so cancelling fulfillment`);
res.send({});
return;
}
function authenticate(agent){
agent.add(`To authenticate this app please visit the following url: ${url}`);
}
function authenticationCode(agent){
const code = agent.parameters.authenticationCode;
console.log('The code: ' + code);
authentication.getToken(code, (err, token) => {
if (err) return console.error('Error retrieving access token', err);
authentication.setCredentials(token);
retrievedToken = token;
console.log(retrievedToken);
});
agent.add('Successfully authenticated!');
}
function makeAppointment (agent) {
const dateTimeStart = new Date(agent.parameters.date.split('T')[0] + 'T' + agent.parameters.time.split('T')[1]);
const dateTimeEnd = new Date(new Date(dateTimeStart).setHours(dateTimeStart.getHours() + 1));
const appointmentTimeString = dateTimeStart.toLocaleString();
const eventDescription = agent.parameters.text;
// Check the availibility of the time, and make an appointment if there is time on the calendar
return createCalendarEvent(dateTimeStart, dateTimeEnd, eventDescription).then(() => {
agent.add(`Ok, let me see if we can fit you in. ${appointmentTimeString} is fine!. I am creating an event called: ${eventDescription}`);
}).catch(() => {
agent.add(`I'm sorry, there are no slots available for this period.`);
});
}
let intentMap = new Map();
intentMap.set('authenticate', authenticate);
intentMap.set('authentication code', authenticationCode);
intentMap.set('Make Appointment', makeAppointment);
agent.handleRequest(intentMap);
}
function createCalendarEvent (dateTimeStart, dateTimeEnd, eventDescription) {
return new Promise((resolve, reject) => {
calendar.events.list({
auth: authentication,
calendarId: calendarId,
timeMin: dateTimeStart.toISOString(),
timeMax: dateTimeEnd.toISOString()
}, (err, calendarResponse) => {
// Check if there is a event already in the calendar
if (err || calendarResponse.data.items.length > 0) {
reject(err || new Error('Requested time conflicts with another appointment'));
console.log(err);
} else {
// Create event for the requested time period
calendar.events.insert({
auth: authentication,
calendarId: calendarId,
resource: {
summary: eventDescription,
start: {dateTime: dateTimeStart},
end: {dateTime: dateTimeEnd}
}
}, (err, event) => {
err ? reject(err) : resolve(event);
console.log(err);
}
);
}
});
});
}

You're on the right track, but having the user go directly to the OAuth link means that they'll get the code, and thus must send it to you.
Instead, you can send them to a page on your site, which redirects them to the OAuth link. The redirection URI you include should redirect back to your site again. This way you get the code and can process it on your server. Once you complete the OAuth dance, your website would tell them they have authorized you and can continue with the chat.

Related

How to get accestoken for Subscribe With Google when using Sign In With Google?

I have to switch from 'Google Sign In' to 'Sign In With Google' (SIWG).
I use passport.js with passport-google-oidc. SIWG works, I ask for the scopes openid, profile and email, and I get it.
But SIWG does not give me any access token as it did before Google Sign In. I need this access token to continue with 'Subscribe With Google' (SWG).
Is there any scope that gives it to me?
As I didn't get from SIWG any access token but at least a 'code' I tried to work with this code.
With google-auth-library and the code, I tried to get an access token. With this library, I use the same id, secret and callback url as with SIWG.
But oAuth2Client.getToken(code) only gives me {"error": "invalid_grant", "error_description": "Bad Request"}
Any idea how to make the request go through ok?
` // 1. Defining strategy
const GoogleStrategy = require('passport-google-oidc');
const Config_Account_Passport = {
// "google" will refer to strategy passport-google-oidc SIWG
Google : new GoogleStrategy({
clientID : '6...MyClientID0c...apps.googleusercontent.com',
clientSecret : 'a...MyClientSecret...',
callbackURL: "https://...UrlOfMyServer.../google/auth",
passReqToCallback : true
}, async function(req, issuer, profile, done) {
// verified function
try {
// get code
const code = req.query.code;
// ...
// get data for login of user. Store data in database if new user.
const email = profile.emails[0].value;
const oauth_id = profile.id;
// Look for user in database. If new store. In Both cases: Having a user object.
//...
// AND now to entitlements (part 4 below)
await checkEntitlements(user, code);
// following never executed because checkEntitlements fails -> goes to catch
// Everything is fine
return done(null, user)
} catch(e) {
return done(true, false); // error and no user
}
})
}
module.exports = Config_Account_Passport;
// 2. Applying strategy
//...
passport.authenticate('google', { scope: ['openid','profile','email','https://www.googleapis.com/auth/subscribewithgoogle.publications.readonly'], accessType: 'offline'});
// 3. Google calls callbackURL -> Successful verified
//...
function(req, res, next) {
// in callback from google
passport.authenticate('google', {}, function (err, user) {
// verified done
if (err) {
// done(true, ...) was called
// user is stays NOT logged id
return res.redirect('/user/login');
}
return res.redirect('/user/loggedin');
})
}
// 4. Check entitlements
// First get access token from code
const {OAuth2Client} = require('google-auth-library');
function getAccessToken(code) {
return new Promise(async (resolve, reject) => {
try {
const oAuth2Client = new OAuth2Client(
clientID : '6...MyClientID0c...apps.googleusercontent.com',
clientSecret : 'a...MyClientSecret...',
callbackURL: "https://...UrlOfMyServer.../google/auth",
);
// Generate the url that will be used for the consent dialog.
await oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: ['https://www.googleapis.com/auth/subscribewithgoogle.publications.readonly'],
});
const {tokens} = await oAuth2Client.getToken(code);
// following never executed because getToken fails -> goes to catch
// "error": "invalid_grant"
//...get access token from tokens
resolve (token);
} catch (e){
return reject(e);
}
})
}
const Payment_SwG = require('./../modules/Payment/SwG');
// Then Get Entitlement with access token
async function checkEntitlements(user, code) {
return new Promise(async (resolve, reject) => {
try {
// first get access token by using code
const accesstoken = await getAccessToken(code);
// following never executed because getAccessToken fails -> goes to catch
// get entitlements by access token
const entitlements = await Payment_SwG.checkEntitlements(accesstoken);
//...
} catch(e) {
return reject(e);
}
})
}`
I tried different scopes. I examined all parameters I got back from SIWG, looking out for an access token.
The Scopes you see in the program code are accepted by SIWG. Execution stops after successful SIWG when I try to get an access token.

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!

Salesforce Login Page not coming for OAuth using NodeJS

I'm trying to login to Salesforce from Google Assistant (using dialogflow) using OAuth. Whatever I say to Google Assistant is supposed to be fulfilled (matched to intent which is then gets matched to the code that fulfills the intent of the user. So, basically the fulfillment code resides on a server (node js express) hosted on Heroku.
The problem is whenever I start by saying 'Talk to test app' I expect to see the Salesforce login page coming up (where I would enter the creds and then the consent part comes) but this page never comes. I'm sure that there might be something missing on my configuration of account linking / code but i'm not able to understand it.
const express = require('express');
const bodyParser = require('body-parser');
const jsforce = require('jsforce');
const { dialogflow } = require('actions-on-google');
const {
SimpleResponse,
BasicCard,
Image,
Suggestions,
Button
} = require('actions-on-google');
var options;
var port = process.env.PORT || 3000;
const expApp = express().use(bodyParser.json());
//app instance
const app = dialogflow({
debug: true
});
app.intent('Default Welcome Intent', (conv) => {
expApp.get('/oauth2/auth', function(req, res) {
const oauth2 = new jsforce.OAuth2({
clientId: process.env.SALESFORCE_CONSUMER_KEY,
clientSecret: process.env.SALESFORCE_CONSUMER_SECRET,
redirectUri: process.env.REDIRECT_URI
});
res.redirect(oauth2.getAuthorizationUrl({}));
});
//
// Pass received authorization code and get access token
//
expApp.get('/getAccessToken', function(req,res) {
const oauth2 = new jsforce.OAuth2({
clientId: process.env.SALESFORCE_CONSUMER_KEY,
clientSecret: process.env.SALESFORCE_CONSUMER_SECRET,
redirectUri: process.env.REDIRECT_URI
});
const conn = new jsforce.Connection({ oauth2 : oauth2 });
conn.authorize(req.query.code, function(err, userInfo) {
if (err) {
return console.error(err);
}
const conn2 = new jsforce.Connection({
instanceUrl : conn.instanceUrl,
accessToken : conn.accessToken
});
conn2.identity(function(err, res) {
if (err) { return console.error(err); }
console.log("user ID: " + res.user_id);
console.log("organization ID: " + res.organization_id);
console.log("username: " + res.username);
console.log("display name: " + res.display_name);
options = { Authorization: 'Bearer '+conn.accessToken};
});
});
});
conv.ask(new SimpleResponse({
speech:'Hi, how is it going? You are being guided to the login page',
text:'Hi, how is it going? You are being guided to the login page',
}));
});
expApp.get('/', function (req, res) {
res.send('Hello World!');
});
expApp.listen(port, function () {
expApp.post('/fulfillment', app);
console.log('Example app listening on port !');
});
OAuth with Google Assistant is managed from the Actions on Google project that you create for your assistant. In these settings you manage which Token and OAuth URL have to be used for the sign-in in your app. If you want the users to sign-in through the assistant app, you will have to choose the OAuth sign-in option.
So you don't have to use your own code to get the OAuth page, you can just use the SignIn() response given to you in the Actions on Google SDK. This will trigger the account linking flow for Google Assistant.
app.intent('Start Signin', (conv) => {
conv.ask(new SignIn('To get your account details'));
});
app.intent('ask_for_sign_in_confirmation', (conv, params, signin) => {
if (signin.status !== 'OK') {
return conv.ask('You need to sign in before using the app.');
}
// const access = conv.user.access.token;
// possibly do something with access token
return conv.ask('Great! Thanks for signing in.');
});

If MFA enabled in AWS cognito, do I need to create js on client side to call cognitoUser.authenticateUser() because of the promt for code?

I am using reactjs and node for server side.
As you can see in the "mfa required" part of the code below, if this is all on node, then I can't really do "prompt" the user for the code, I have to pass this back to the front end.
Tried solution: If I do pass the MFA required to front end and get the users input then send it back to node to call "respondToAuth" I am getting two MFA codes in my SMS message.
Have I tried other solutions?
I am hesitant to use amplify because everything is on the front end, I would ideally like to do my authentication on the back end (thus node).
Another option I am leaning towards is just using initiateAuth api instead of "cognitoUser.AuthenticateUser". This way I can get the challenge response and pass it on in sequence. But as per my initial question, I am wondering if I can implement the below code and be able to route users to input MFA code (without duplicating MFA sms message)
AWS.config.update({
region: process.env.Region
});
var AmazonCognitoIdentity = require('amazon-cognito-identity-js');
const poolData = { //--Moved to env variables
UserPoolId: process.env.UserPoolId, // your user pool id here
ClientId: process.env.ClientId // your app client id here
};
const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
router.post('/api/authenticateuser', (req, res) => {
const val = req.body;
var userData = {
Username: val.value.user, // your username here
Pool: userPool
};
var authenticationData = {
Username: val.value.user, // your username here
Password: val.value.pass, // your password here
};
const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function(result) {
console.log('You are now logged in.');
console.log(result);
const accessToken = result.getAccessToken().getJwtToken();
const idToken = result.getIdToken().getJwtToken();
res.json({
accessToken,
idToken
});
},
onFailure: function(err) {
res.json(err);
},
mfaRequired: function(codeDeliveryDetails) {
// console.log("mfa enabled");
// var verificationCode = prompt('Please input verification code' ,'');
// cognitoUser.sendMFACode(verificationCode, this);
// res.json({ MFA:codeDeliveryDetails})
}
});
})

Google Suite - Google API access - Client is unauthorized to retrieve access tokens using this method

I am struggling for days with the set up in trying to access GMail Google API from a node.js script using googleapis lib. I succeeded once but I cannot remember how I did it , I tried to reset a project, service-account and G-Suite Domain wide delegation following the Google doc ..
Here is what I did :
In my GCP Console console,
1. Existing organisation : lechorodescharentes.org
2. In this organisation , I created a project : choro-dev
3. In this project I created a service account : choro-dev-postoffice
with choro-dev-postoffice with role TokenGenerator
and enabled the Google Apps Domain-wid Delegation
downloaded the new private key ( JSON file )
4. I enabled the GMail API ( from Libray menu)
In my G-Suite domain's admin console,
5. I added the following copes for this service account's ClientID
"https://www.googleapis.com/auth/admin.directory.user",
"https://www.googleapis.com/auth/admin.directory.group"
Node.js client
I am trying to access the GMail API with the following Firebase function code using the node.js googleapis library
with server -server authentication using service account
see node.js client code
In this code, I have 2 authentication functions
connect() : to a JSON Web Token
authorize() : to request an access token from the Google OAuth 2.0 Authorization Server
Deployed the Firebase function
Run the function
Got the JWT client displayed
Function ended with error :
{"infos":"unauthorized_client: Client is unauthorized to retrieve access tokens using this method."}
node.js client code
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const {google} = require('googleapis');
const nodemailer = require('nodemailer')
const _ = require('lodash');
const KEY = require('./service-key.json');
function connect () {
return new Promise((resolve, reject) => {
const jwtClient = new google.auth.JWT(
KEY.client_email,
null,
KEY.private_key,
_.values(KEY.scopes), // scopes as authorized in G-Suite admin
KEY.admin_email . // impersonated user
);
jwtClient.authorize((err) => {
if(err) {
reject(err);
} else {
resolve(jwtClient); // returns client
}
});
});
}
// Send a message to the contact user
function sendMessage (client, sender, msg) {
return new Promise((resolve, reject) => {
var transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: {
type: 'OAuth2',
user: KEY.admin_email,
serviceClient: KEY.client_id,
privateKey: KEY.private_key,
accessToken: client.access_token,
refreshToken: client.refresh_token,
expires: client.expiry_date
}
});
const mailOptions = {
from: 'SITE CONTACT<' + sender + '>',
to: KEY.contact_email,
subject: 'Message',
text: 'From: ' + sender + '\n\n' + msg,
html: '<h1>Message</h1><p>From: ' + sender + '</p><p>' + msg + '</p>'
};
transporter.sendMail(mailOptions, (err, response) => {
if (err) {
reject(err);
return;
}
resolve(response);
});
});
}
function newContactMessage (from, msg) {
return connect()
.then(client => {
return sendMessage(client, from, msg);
});
}
exports.sendContactMessage = functions.https.onRequest((req, res) => {
const sender_email = 'dufourisabelle#orange.fr';
const sender_msg = 'just a test message to contact the site owner.'
newContactMessage(sender_email, sender_msg).then(() => {
return {status: 200};
}, error => {
return {status: error.status, infos: error.message};
}).then(response => {
return res.send(response);
}).catch(console.error);
});
What could I add to it ? I'll try to re-initiate the all process and pray ... ??

Resources