how to refresh token on google oauth2 using firebase functions? - node.js

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));
}
}
});

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?

JSONP callback doesn't seem to be working

I've been working with the Firebase Admin SDK for Nodejs in the cloud functions to create custom auth tokens using Spotify and Firebase auth.
I've been trying to use the example given by Google that goes as follows:
exports.token = functions.https.onRequest((req, res) => {
try {
cookieParser()(req, res, () => {
functions.logger.log('Received verification state:', req.cookies.state);
functions.logger.log('Received state:', req.query.state);
if (!req.cookies.state) {
throw new Error('State cookie not set or expired. Maybe you took too long to authorize. Please try again.');
} else if (req.cookies.state !== req.query.state) {
throw new Error('State validation failed');
}
functions.logger.log('Received auth code:', req.query.code);
Spotify.authorizationCodeGrant(req.query.code, (error, data) => {
if (error) {
throw error;
}
functions.logger.log(
'Received Access Token:',
data.body['access_token']
);
Spotify.setAccessToken(data.body['access_token']);
Spotify.getMe(async (error, userResults) => {
if (error) {
throw error;
}
functions.logger.log(
'Auth code exchange result received:',
userResults
);
// We have a Spotify access token and the user identity now.
const accessToken = data.body['access_token'];
const spotifyUserID = userResults.body['id'];
const profilePic = userResults.body['images'][0]['url'];
const userName = userResults.body['display_name'];
const email = userResults.body['email'];
// Create a Firebase account and get the Custom Auth Token.
const firebaseToken = await createFirebaseAccount(spotifyUserID, userName, profilePic, email, accessToken);
// Serve an HTML page that signs the user in and updates the user profile.
res.jsonp({token: firebaseToken});
});
});
});
} catch (error) {
res.jsonp({error: error.toString()});
}
return null;
});
Here's the code from the client for making the request
const loginError = ref(null)
const route = useRoute()
console.log(route.query)
const { code, state, error } = route.query
function tokenReceived(data) {
if (data.token) {
projectAuth.signInWithCustomToken(data.token).then((userCredential) => {
console.log(userCredential)
})
} else {
console.error(data)
document.body.innerText = 'Error in the token Function: ' + data.error
}
}
if (error) {
loginError.value = 'Error back from the Spotify auth page: ' + error
} else if (code) {
// Use JSONP to load the 'token' Firebase Function to exchange the auth code against a Firebase custom token.
const script = document.createElement('script')
script.type = 'text/javascript'
// This is the URL to the HTTP triggered 'token' Firebase Function.
// See https://firebase.google.com/docs/functions.
const tokenFunctionURL =
'http://localhost:5001/pacelist-main/us-central1/token'
script.src =
tokenFunctionURL +
'?code=' +
encodeURIComponent(code) +
'&state=' +
encodeURIComponent(state) +
'&callback=' +
tokenReceived.name
document.head.appendChild(script)
}
const signIn = () => {
// Start the auth flow.
window.location.href =
'http://localhost:5001/pacelist-main/us-central1/redirect'
}
return { loginError, signIn }
Full repository here: https://github.com/firebase/functions-samples/tree/main/spotify-auth
But the jsonp callback doesn't seem to run when it goes back to my site. It should "Serve an HTML page that signs the user in and updates the user profile." and log the user in, but it does nothing. Been stuck on this one for days...

Unhandled error { Error: You did not provide an API key

I am integrating Stripe into Firebase with my iOS project and this is the error I receive when I try to open PaymentOptionsViewController (in my iOS app simulator):
Unhandled error { Error: You did not provide an API key. You need to provide your API key in the Authorization header, using Bearer auth (e.g. 'Authorization: Bearer YOUR_SECRET_KEY'). See https://stripe.com/docs/api#authentication for details, or we can help at https://support.stripe.com/.
Here is my code in my nodejs index.js:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const stripe = require("stripe")(functions.config().stripe.secret_test_key);
exports.createStripeCustomer = functions.firestore.document('users/{userId}').onCreate(async (snap, context) => {
const data = snap.data();
const email = data.email;
const customer = await stripe.customers.create({ email: email })
return admin.firestore().collection('users').doc(data.id).update({ stripeId : customer.id})
});
exports.createCharge = functions.https.onCall(async (data, context) => {
const customerId = data.customerId;
const totalAmount = data.total;
const idempotency = data.idempotency;
const uid = context.auth.uid
if (uid === null) {
console.log('Illegal access attempt due to unauthenticated user');
throw new functions.https.HttpsError('permission-denied', 'Illegal access attempt.')
}
return stripe.charges.create({
amount: totalAmount,
currency: 'usd',
customer: customerId
}, {
idempotency_key: idempotency
}).then( _ => {
return
}).catch( err => {
console.log(err);
throw new functions.https.HttpsError('internal', 'Unable to create charge')
});
})
exports.createEphemeralKey = functions.https.onCall(async (data, context) => {
const customerId = data.customer_id;
const stripeVersion = data.stripe_version;
const uid = context.auth.uid;
if (uid === null) {
console.log('Illegal access attempt due to unauthenticated user');
throw new functions.https.HttpsError('permission-denied', 'Illegal access attempt.')
}
let key = await stripe.ephemeralKeys.create(
{customer: '{{CUSTOMER_ID}}'},
{stripe_version: '{{2019-05-16}}'}
);
return stripe.ephemeralKeys.create(
{customer: customerId},
{stripe_version: stripeVersion}
).then((key) => {
return key
}).catch((err) => {
console.log(err)
throw new functions.https.HttpsError('internal', 'Unable to create ephemeral key.')
})
})
// exports.helloWorld = functions.https.onRequest((request, response) => {
// console.log('This is the console message.')
// response.send("Hello from JonnyB Codes!");
// });s
How do I solve the error above? Let me know if more information is needed to get an answer.
Got this error message in iOS too.
In my case I was not setting correctly the publishable key when the app was launched, before doing the payment. In Swift this did the trick.
Stripe.setDefaultPublishableKey("pk_test_....")
Probably doesn't solve your issue, but maybe give you a clue.

Issue with Google oAuth2 callback using Firebase functions

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();
});

Resources