Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fixes refresh_token not being obtained all the time. #10

Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 39 additions & 14 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@ module.exports.apifyGoogleAuth = async ({ scope, tokensStore, credentials, googl
const authorizeUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: `https://www.googleapis.com/auth/${scope}`,
// Always prompt for user consent otherwise, there is a chance to not obtain refresh_token
// https://stackoverflow.com/a/10857806
prompt: 'consent'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this help us in current situation? Because by default we will use the code stored in KV store even if it is not viable and it will fail. But I didn't really have problems with needing a refresh. What is the actual problem you are solving?

Copy link
Author

@JJetmar JJetmar Jan 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the stackoverflow - It helps in development when you are testing/using the same account for the same app and redirectUrl. The refresh_token seems to be returned only for the first time in this case. Then you have to manually go to your google account and remove the consent and then give it again to the application I noticed this because otherwise I had to give new consent every 30minutes (which is pretty annoying) since the credentials were saved without refresh_token which is required for obtaining the new tokens.

See googleapis/google-api-nodejs-client#750 (comment)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are not storing the code, you are storing the received tokens.

});
let code;

const codeHolder = { code: null }; // To be able to access code as reference from any scope
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the scope we want to access this? We are sending it somewhere?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change in code was not detected in the setInterval body (I think due to different closure).


if (googleCredentials.email) {
console.log('You provided an email so we will try to log in with puppeteer. You may have to allow for access after authorization in your email depending on the security level you have.');
let puppeteerOptions;
Expand Down Expand Up @@ -93,7 +98,7 @@ module.exports.apifyGoogleAuth = async ({ scope, tokensStore, credentials, googl
await page.waitForSelector('#submit_approve_access');
await page.click('#submit_approve_access');
await page.waitForSelector('#code', { timeout: 120000 });
code = await page.$eval('#code', (el) => el.value);
codeHolder.code = await page.$eval('#code', (el) => el.value);
await page.close();
await browser.close();
} catch (e) {
Expand All @@ -109,15 +114,22 @@ module.exports.apifyGoogleAuth = async ({ scope, tokensStore, credentials, googl
console.log(pleaseOpen);
console.log(information);

let codeBeenSet;
const waitForCodeBeenSet = new Promise((resolve) => {
codeBeenSet = resolve;
});

const server = http.createServer((req, res) => {
code = new URL(req.url, pickedCredentials.redirect_uri).searchParams.get('code');
if (code) {
codeHolder.code = new URL(req.url, pickedCredentials.redirect_uri).searchParams.get('code');
if (codeHolder.code) {
let data = '';
req.on('data', (body) => {
if (body) data += body;
});
req.on('end', () => {
res.end(close());
codeBeenSet();
console.log("Code was successfully provided!")
});
} else {
res.end(authorize(authorizeUrl));
Expand All @@ -126,21 +138,34 @@ module.exports.apifyGoogleAuth = async ({ scope, tokensStore, credentials, googl

server.listen(port, () => console.log('server is listening on port', port));

const start = Date.now();
while (!code) {
const now = Date.now();
if (now - start > 5 * 60 * 1000) {
throw new Error('You did not provide the code in time!');
}
console.log(`waiting for code...You have ${300 - Math.floor((now - start) / 1000)} seconds left`);
await new Promise((resolve) => setTimeout(resolve, 10000));
}
let timeoutInterval;
const waitForTimeout = new Promise((resolve, reject) => {
const start = Date.now();

timeoutInterval = setInterval(() => {
const now = Date.now();
if (now - start > 5 * 60 * 1_000) {
reject('You did not provide the code in time!');
} else {
console.log(`waiting for code...You have ${300 - Math.floor((now - start) / 1000)} seconds left`);
}
}, 10000)
});

// Wait for code being set by user or time runs out
await Promise.race([
waitForTimeout.catch((errorMessage) => { throw new Error(errorMessage) }),
waitForCodeBeenSet
]);

clearInterval(timeoutInterval); // clear Interval no matter what happened
codeBeenSet(); // Resolve in case of timeout
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for this rewrite, it seems like the same thing with more complicated code, am I missing something?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have to wait another 10sec after the code was already set.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may use synchronize it with EventEmitter without creating extra promises.


server.close(() => console.log('closing server'));
}

// Now that we have the code, use that to acquire tokens.
const tokensResponse = await oAuth2Client.getToken(code);
const tokensResponse = await oAuth2Client.getToken(codeHolder.code);
console.log(`Storing the tokens to your store under key ${tokensRecordKey}`);
await store.setValue(tokensRecordKey, tokensResponse.tokens);
oAuth2Client.setCredentials(tokensResponse.tokens);
Expand Down