Issue with Google oAuth2 callback using Firebase functions - node.js

I would like use Firebase Functions to use the Google Developer API. Authentification is required to use this API.
I follow the doc: https://github.com/googleapis/google-api-nodejs-client
I have some troubles to get the authorization code in the callback url.
var {google} = require('googleapis');
google.options({ auth: oauth2Client });
var oauth2Client = new google.auth.OAuth2(
'XXXX.apps.googleusercontent.com',
'XXXX',
'https://us-central1-XXXX.cloudfunctions.net/oauth2callback'
);
function generateAuthenticationUrl() {
return oauth2Client.generateAuthUrl({
access_type: 'offline',
prompt: 'consent',
scope: 'https://www.googleapis.com/auth/androidpublisher'
});
}
exports.oauth2Callback = functions.https.onRequest((req, res) => {
console.log(req.query.code);
const code = req.query.code;
//do something
return null;
});
exports.hello = functions.https.onRequest((req, res) => {
var url = generateAuthenticationUrl();
console.log(url);
//-> url print in the console is : https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fandroidpublisher&response_type=code&client_id=XXXXX-XXX.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Fus-central1-XXX.cloudfunctions.net%2Foauth2callback
res.redirect(url);
});
Redirect url is set in the Google Console Developer:
When I call the url https://us-central1-XXX.cloudfunctions.net/hello, I got "Error: could not handle the request" and "finished with status: 'timeout'" in the Firebase logs.
What's wrong?

I found a solution.
Full code using JWT to authenticate, then get the list of app's reviews:
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase);
var {google} = require('googleapis');
const serviceAccount = require('./client_secret.json');
const { JWT } = require('google-auth-library');
const getAuthorizedClient = () => new JWT({
email: serviceAccount.client_email,
key: serviceAccount.private_key,
scopes: ['https://www.googleapis.com/auth/androidpublisher']
});
const getAndroidpublisher = () => google.androidpublisher({
version: 'v3',
auth: getAuthorizedClient()
});
const requestProductValidation = () => new Promise((resolve, reject) => {
getAndroidpublisher().reviews.list({
packageName: "com.my.packagename"
}, (err, response) => {
if (err) {
console.log(`The API returned an error: ${err}`);
resolve({status: "Error"});
} else {
return resolve(response);
}
});
});
exports.hello = functions.https.onRequest((req, res) => {
return requestProductValidation();
});

Related

passport-apple Node.js login error - Failed to obtain access token

I am a junior engineer working in a start-up in Seoul, Korea.
In the current project, I am trying to use the passport module to develop apple login.
I have already finished developing google social login, but I faced some problems while trying to do the same with apple.
The problem is that I get an error that states : "Failed to obtain access token".
I got really confused that you have to use the POST method in order to get the profile info from apple.
Can someone please help me?? Thanks in advance!
IT would be wonderful if I could success. Thanks again
The main problem I am expecting is that
passport.authenticate('apple') calls the function which handles passport module for google that I have already developed.
I send the redirect url to Frontend, in order to open the browser in my application.
ROUTER.get('/apple/login', async function (req, res) {
const result = { status: 'N' };
const config = {
client_id: APPLE_AUTH.CLIENT_ID, // This is the service ID we created.
redirect_uri: APPLE_AUTH.REDIRECT_URI, // As registered along with our service ID
response_type: 'code id_token',
// state: 'origin:web', // Any string of your choice that you may use for some logic. It's optional and you may omit it.
scope: 'name email', // To tell apple we want the user name and emails fields in the response it sends us.
response_mode: 'form_post',
m: 11,
v: '1.5.4',
};
const queryString = Object.entries(config)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&');
const redirectUrl = `https://appleid.apple.com/auth/authorize?${queryString}`;
result['redirectUrl'] = redirectUrl;
result['status'] = 'Y';
res.json(result);
});
I get the url, and open the browser
async openAppleSignIn() {
const result = await this.authService.postAppleLogin();
// await this.authService.postAppleLogin();
if (result.data.status === 'Y') {
const { redirectUrl } = result.data;
console.log(redirectUrl);
if (this.deviceInfo.platform !== 'web') {
// this.browserService.openBrowser(redirectUrl);
window.open(redirectUrl, '_self');
console.log('enter');
} else {
const timer = setInterval(async () => {
if (this.newWindowForLogin.closed) {
clearInterval(timer);
}
}, 500);
const isChrome = this.commonService.checkBrowserIsChrome();
if (!this.newWindowForLogin || isChrome) {
// this.newWindowForLogin = window.open();
window.open(redirectUrl);
}
// this.newWindowForLogin.location.href = redirectUrl;
}
} else {
}
Apple strategy
const passport = require('passport');
// var AppleStrategy = require('passport-google-oauth20').Strategy;
var AppleStrategy = require('passport-apple').Strategy;
const jwt = require('jsonwebtoken');
// var fs = require('fs');
const APPLE_AUTH = require('../../../config/secret_key').get('APPLE_AUTH');
const UserModelService = require('../../api/user/model/user_model_service');
// const applePrivateKey = fs.readFileSync('config/AuthKey_Y8BG5JY7P3.p8');
// console.log(applePrivateKey);
module.exports = () => {
passport.use(
'apple',
new AppleStrategy(
{
clientID: APPLE_AUTH.CLIENT_ID,
teamID: APPLE_AUTH.TEAM_ID,
callbackURL: APPLE_AUTH.LOGIN_CALLBACK_URL,
keyID: APPLE_AUTH.KEY_ID,
privateKeyLocation: 'config/AuthKey_Y8BG5JY7P3.p8',
privateKeyString: APPLE_AUTH.privateKey,
passReqToCallback: true,
},
async (accessToken, refreshToken, idToken, profile, done) => {
try {
// console.log(profile._json.email);
// const socialLoginEmail = req.user.emails[0].value;
// console.log(email);
//화면에서 백으로
// console.log(profile._json.email);
// const user = await UserModelService.getUser(profile._json.email);
// console.log(user);
console.log('jwt', jwt.decode(idToken));
console.log('strategy', req);
done(null, idToken);
} catch (error) {
console.error(error);
// done(error);
}
},
),
);
};
index.js
const passport = require('passport');
const apple = require('./apple_auth');
const google = require('./google_auth');
module.exports = () => {
apple();
google();
};
5.Then, I intended to get the results in callback
ROUTER.post(
'/apple/callback',
passport.authenticate('apple', { failureRedirect: '/', session: false }),
async (req, res) => {
try {
console.log(res);
console.log(req);
} catch (err) {}
},
);
I customized the passport-apple usage into this way, not following the instructions in the passport docs, because the way they listed in the official document did not work for my code.
Thanks again, and I hope to find an answer in stack overflow!!

How can I not confirm the action from Google api every time?

I have a Desktop App for Google Drive that create and set perms that i need. This app should work only for one account with google drive.
My problem is that when I launch an action, I always have to confirm this action in explorer. Can I somehow automatically send my data and confirmation to the server? I read about the access token, but it seems to be suitable only for web applications. I am based on the documentation from the Google API site.
And in future this should work from console.
My code right now:
const fs = require('fs').promises;
const path = require('path');
const process = require('process');
const {authenticate} = require('#google-cloud/local-auth');
const {google} = require('googleapis');
const { AuthClient } = require('google-auth-library');
const SCOPES = ['https://www.googleapis.com/auth/drive'];
const TOKEN_PATH = path.join(process.cwd(), 'token.json');
const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json');
async function loadSavedCredentialsIfExist() {
try {
const content = await fs.readFile(TOKEN_PATH);
const credentials = JSON.parse(content);
return google.auth.fromJSON(credentials)
} catch (err) {
return null;
}
}
async function saveCredentails(client) {
const content = await fs.readFile(CREDENTIALS_PATH);
const keys = JSON.parse(content);
const key = keys.installed || keys.web;
const payload = JSON.stringify({
type: 'authorized_user',
access_type: 'offline',
client_id: key.client_id,
client_secred: key.client_secret,
refresh_token: client.credentials.refresh_token,
});
await fs.writeFile(TOKEN_PATH, payload);
}
async function authorize() {
let client = await loadSavedCredentialsIfExist();
if (client) {
return client;
}
client = await authenticate({
scopes: SCOPES,
keyfilePath: CREDENTIALS_PATH,
});
if (client.credentials) {
await saveCredentails(client);
}
return client;
}
async function createFolder(authClient) {
const service = google.drive({version: 'v3', auth: authClient});
const fileMetadata = {
name: 'testmeows',
mimeType: 'application/vnd.google-apps.folder',
};
try {
const file = await service.files.create({
resource: fileMetadata,
fields: 'id',
});
console.log('Folder Id:', file.data.id);
const body = {"role": "writer", "type": "anyone"}
const result = await service.permissions.create({
resource: body,
fileId: file.data.id,
//fields: 'id',
});
const align = `https://drive.google.com/drive/folders/${file.data.id}?usp=sharing`;
console.log(align);
} catch (err) {
throw err;
}
}
//module.exports = test;ф
authorize().then(createFolder).catch(console.error);
Well, how to better get refresh token without user invention and opening explorer on Desktop App Google Api?

Node set variable from text file

I am working on oauth2 for an API client, and I am having trouble getting the promise back in time to continue with the other calls to the API. My tokens expire every 30 minutes and my node runs every 10 minutes. I thought I could set a text file to the newest token each time the script runs, and grab it at the beginning and always have a good token to use for auth. The variable doesn't get set in time to make the calls, so the header has undefined next the Bearer. I can't figure out how to get the variable set before the call goes off.
Here is the script I am using to make the calls
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Axios = require("axios");
const moment = require("moment");
const config = require("../config/default");
const oauth = require("axios-oauth-client");
const fs = require('fs');
let accessToken;
var token = fs.readFile('token.txt', 'utf8', function(err, data) {
if (err) throw err;
return data;
});
const getOwnerCredentials = oauth.client(Axios.create(), {
url: 'url',
grant_type: 'password',
client_id: 'username',
client_secret: 'secret',
username: 'username',
password: 'password',
scope: 'scope'
});
const tokenCall = async () => {
const result = await getOwnerCredentials();
return result
}
const getToken = async () => {
const accessToken = await tokenCall();
const fs = require('fs')
fs.writeFile('/root/qt-cwsedona/token.txt', '', function(){console.log('done')})
fs.writeFile('token.txt', accessToken.access_token, function (err) {
if (err) return console.log(err);
});
};
getToken();
class Sedona {
constructor() {
this.baseUrl = config.sedonaUrl;
this.client = Axios.default.create({
baseURL: this.baseUrl,
auth: {
Authorization: 'Bearer ' + token
},
});
console.log(this.client);
}
getCustomerBillId(customer_id) {
return this.client.get('/CustomerBill/' + customer_id).then(response => {
let result = parseInt(response['data'][0]['CustomerBillId']);
if (isNaN(result)) {
return "";
}
else {
return result.toString();
}
}).catch(error => {
throw error;
});
}
Then I am this code to actually kick off these functions
const Sedona = require("./services/SedonaService");
Promise.all([
sedona.getCustomerBillId(customerId)
]).then(sedona_results => {
This is probably not the right way to do this, but I was able to set the default header for axios in the async function and it worked.
const getToken = async () => {
const accessToken = await tokenCall();
const token = accessToken.access_token;
Axios.defaults.headers.common['Authorization'] = 'Bearer '+accessToken.access_token;
};

Verify email content using mocha

I am using Node JS, mocha and googleapi writing tests to verify email content
When i run the googleapi as a standalone node js file i am able to get mails but when i integrate it with mocha tests i am not seeing any result, please help
test spec file (verify.js)
var checkEmail = require('../shared/checkEmail');
const { google } = require('googleapis');
var expect = require('chai').expect;
var chai = require('chai');
var chaiAsPromised = require('chai-as-promised');
const { isNullOrUndefined } = require('util');
chai.use(chaiAsPromised);
chai.should();
var emailRecords = new Array();
var content;
var auth;
describe('GMAIL Testing', function () {
before('Connect to Gmail server', function () {
content = checkEmail.getAuthentication();
auth = checkEmail.authorize(content);
// Random test data
var a = [{ "Id": "123", "MsgId": "34677", "Type": "aaa", "Subject": "subxxxx", "ToAddress": "abc#gmail.com", "ToName": "ABC", "DateCreated": "2020-07-09T18:25:38.047Z" }];
emailRecords.push(a);
var b = [{ "Id": "456", "MsgId": "34655", "Type": "bbb", "Subject": "subject", "ToAddress": "abc#gmail.com", "ToName": "ABC", "DateCreated": "2020-06-09T18:25:38.047Z" }];
emailRecords.push(b);
});
it('Gmail Verification', function () {
emailRecords.forEach(element => {
const gmail = google.gmail({ version: 'v1', auth });
var query = "from:noreply#somedomain.com " + element[0].MsgId;
console.log('getting mail '+ element[0].MsgId);
gmail.users.messages.list({
userId: 'me',
q: query
}, (err, res) => {
if (err) return console.log('The API returned an error: ' + err);
var mails = res.data.messages;
console.log('mail(s) found');
expect(mails.length).to.be.at.least(1);
});
console.log('completed search');
});
});
});
Utility File checkEmail.js Ref -> Gmail API
const fs = require('fs');
const readline = require('readline');
const { google } = require('googleapis');
var base64 = require('js-base64').Base64;
const cheerio = require('cheerio');
var open = require('open');
const { isNullOrUndefined } = require('util');
var Mailparser = require('mailparser').MailParser;
const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = 'token.json';
module.exports = new class checkEmail {
getAuthentication() {
// Load client secrets from a local file.
console.log('getting auth');
this.content = JSON.parse(fs.readFileSync('shared/config/credentials.json', 'utf8'));
return this.content;
}
authorize(credentials) {
const { client_secret, client_id, redirect_uris } = credentials.installed;
const oAuth2Client = new google.auth.OAuth2(
client_id, client_secret, redirect_uris[0]);
var token = fs.readFileSync(TOKEN_PATH, 'utf-8');
if (token == isNullOrUndefined) {
token = getNewToken(oAuth2Client);
}
oAuth2Client.setCredentials(JSON.parse(token));
return oAuth2Client;
}
getNewToken(oAuth2Client) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
var newToken;
console.log('Authorize this app by visiting this url:', authUrl);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question('Enter the code from that page here: ', (code) => {
rl.close();
oAuth2Client.getToken(code, (err, token) => {
if (err) return console.error('Error retrieving access token', err);
oAuth2Client.setCredentials(token);
// Store the token to disk for later program executions
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) return console.error(err);
console.log('Token stored to', TOKEN_PATH);
});
newToken = token;
});
});
return newToken;
}
}
I have tried adding debug messages but nothing is printed nor any error is thrown, Please let me know if i am missing something
output after running tests
> .\node_modules.bin\mocha .\test\verify.js
Tests are appearing to be passed but console.log('mail(s) found');
didnt show up in the output
Your test is finishing before your network request finishes.
See this section in the mocha docs.
https://mochajs.org/#asynchronous-code
You need to either use the done callback or return a promise.
If you can use async/await I find this to be the easiest because an async function always returns a promise: https://mochajs.org/#using-async-await

how to refresh token on google oauth2 using firebase functions?

I developed an integration using Google Oauth2 inside firebase functions to access Google Sheets API. The integration works correctly but I'm having problems to make sure the refresh token is running correctly. The function stops working after the first token expires.
when this happens the following error occur:
Function execution started
Error: No refresh token is set.
at OAuth2Client.refreshTokenNoCache (/workspace/node_modules/googleapis-common/node_modules/google-auth-library/build/src/auth/oauth2client.js:161:19)
at OAuth2Client.refreshToken (/workspace/node_modules/googleapis-common/node_modules/google-auth-library/build/src/auth/oauth2client.js:142:25)
at OAuth2Client.getRequestMetadataAsync (/workspace/node_modules/googleapis-common/node_modules/google-auth-library/build/src/auth/oauth2client.js:256:28)
at OAuth2Client.requestAsync (/workspace/node_modules/googleapis-common/node_modules/google-auth-library/build/src/auth/oauth2client.js:329:34)
at OAuth2Client.request (/workspace/node_modules/googleapis-common/node_modules/google-auth-library/build/src/auth/oauth2client.js:323:25)
at createAPIRequestAsync (/workspace/node_modules/googleapis-common/build/src/apirequest.js:292:27)
at Object.createAPIRequest (/workspace/node_modules/googleapis-common/build/src/apirequest.js:43:9)
at Resource$Spreadsheets$Values.update (/workspace/node_modules/googleapis/build/src/apis/sheets/v4.js:601:37)
at exports.loadStripeData.functions.runWith.https.onRequest (/workspace/index.js:176:32)
at process._tickCallback (internal/process/next_tick.js:68:7)
I want to make sure the token refresh correctly and get stored on Firestore.
What am I doing wrong?
index.js:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {google} = require('googleapis');
const sheets = google.sheets('v4');
admin.initializeApp();
const CLIENT_ID = 'CLIENT_ID';
const CLIENT_SECRET = 'CLIENT_SECRETT';
const REDIRECT_URL = 'https://us-central1-MY-PROJECT.cloudfunctions.net/oauth2callback';
const SCOPES = ['https://www.googleapis.com/auth/spreadsheets'];
oauth2Client.on('tokens', (tokens) => {
if (tokens.refresh_token) {
try {
admin.firestore()
.collection('oauth2')
.doc('google')
.set({
tokens: tokens.refresh_token,
});
} catch (error) {
console.error(JSON.stringify(error));
}
}
});
/*asks user permission to access his spreadsheets*/
exports.authenticate = functions.https.onRequest((req, res) => {
const authorizeUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES.join(','),
});
res.send(`<html>click here: ${authorizeUrl}</html>`)
});
/*callback function for when the user finishes authenticating*/
exports.oauth2callback = functions.https.onRequest(async(req, res) => {
const code = req.query.code.toString() || '';
try {
await admin.firestore()
.collection('oauth2')
.doc('google')
.set({
code: decodeURIComponent(code)
});
} catch(error) {
res.send(JSON.stringify(error))
}
res.send('auth successfully. You can close this tab');
});
/* get token from Firestone to execute function*/
async function oauth2Auth() {
const doc = await admin.firestore()
.collection('oauth2')
.doc('google')
.get();
const credentials = doc.data();
if (credentials.code !== undefined) {
const response = await oauth2Client.getToken(credentials.code);
credentials.tokens = response.tokens;
delete credentials.code;
try {
await admin.firestore()
.collection('oauth2')
.doc('google')
.set({
tokens: credentials.tokens,
})
} catch (error) {
console.error(error);
}
}
oauth2Client.setCredentials(credentials.tokens);
}
/*function that requires google sheets api*/
exports.mainFunction = functions.https.onRequest(async(req, res) => {
oauth2Auth();
//do main function
});
Finally discovered the problem!
You only get the refreshing token in the first time you ask for authorization. So if you're don't save it correctly you have to ask permission again.
To solve it:
when redirecting the user to the authorization URL add the following parameters to have sure you get the refreshing token:
access_type=offline&prompt=consent
to save the refreshing token:
oauth2Client.on('tokens', async(tokens:any) => {
if (tokens.refresh_token) {
try {
const authorization = await oauth2Client.getToken(tokens.refresh_token);
await admin.firestore()
.collection('collectionName')
.doc(docId)
.update({
token: authorization.tokens
})
} catch (error) {
console.error(JSON.stringify(error));
}
}
});

Resources