byeokim / gmailpush

Gmail API push notification handler for Node.js
MIT License
53 stars 7 forks source link

How to automatically refresh the access token? #21

Open shelomito12 opened 1 year ago

shelomito12 commented 1 year ago

My application has a status of 'Testing' and the consent screen is configured for an external user type, causing the token to expire in 7 days.

How to automatically refresh the access token on the next API call after it expires?

Currently, I'm manually generating the access token with the following code:

require('dotenv').config();
const readline = require('readline');
const {google} = require('googleapis');

function getToken() {
  const auth = new google.auth.OAuth2(
    process.env.CLIENT_ID,
    process.env.CLIENT_SECRET,
    'urn:ietf:wg:oauth:2.0:oob'
  );

  const authUrl = auth.generateAuthUrl({
    access_type: 'offline',
    scope: ['https://www.googleapis.com/auth/gmail.readonly'],
  });

  console.log('Authorize this app by visiting this url:');
  console.log(authUrl);

  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  rl.question('Enter the code from that page here: ', (authCode) => {
    rl.close();
    auth.getToken(authCode, (err, token) => {
      if (err) {
        return console.log('Error retrieving access token', err);
      }

      console.log('Token:');
      console.log(token);
    });
  });
}

getToken();

Can you please advice? thanks

shelomito12 commented 1 year ago

I see your API requires the token object.

const messages = await gmailpush.getMessages({
  notification: req.body,
  token,
...

Is this the only auth method supported in your code? Can service account be also supported?

shelomito12 commented 1 year ago

@byeokim If this is not possible at the moment due to your limited time for upgrading the API, then could you perhaps show some code example on how to call, for example, the refreshAccessToken() function to save all returned credentials such as refresh_token, access_token to a JSON file, so when the API calls gmailpush.getNewMessage I can get those credentials updated in the JSON file?

byeokim commented 1 year ago

Did you mean renewing mailbox watch because though access_token expires soon refresh_token can be used instead as long as you want unless some expiration conditions are met? If that's the case you might want to use some 3rd party scheduler module such as node-schedule to extend seven-days watch expiration as presented here.

shelomito12 commented 1 year ago

I've added the following

        fs.readFile('./gmailpush_history.json')
          .then((result) => {
            const prevHistories = JSON.parse(result);
            const prevHistory = prevHistories.find((prevHistory) => prevHistory.emailAddress === email);
            schedule.scheduleJob(new Date(prevHistory.watchExpiration - 1000 * 60 * 30), async () => {
              prevHistory.watchExpiration = await gmailpush._refreshWatch();
              fs.writeFile('./gmailpush_history.json', JSON.stringify(prevHistories));
            });
          });

https://github.com/jzvi12/gmail-push-to-discord/blob/master/index.js#L53-L61

I've checked that it updated the watchExpiration value in the gmailpush_history.json generated file when I got a new email to "extend" it by 1 day... but after testing... on the 8th+ day, I was not getting notifications even though I was getting emails each day.

Thoughts?

byeokim commented 1 year ago

I don't have ideas why you are not getting notifications. But looking into your code I found that my suggestion was flawed. Instead of setting up node-schedule only when email is received, here is a code to extend watch expirations of all users in gmailpush_history.json every day on 00:00 GMT. Note that googleapis is added.

// From https://github.com/jzvi12/gmail-push-to-discord/blob/master/index.js

require("dotenv").config();
const jsonToken = require("./token.json");
const Gmailpush = require("gmailpush");
const express = require("express");
const app = express();
const schedule = require('node-schedule');
const fs = require('fs').promises;
const { WebhookClient } = require("discord.js");
const webhook = new WebhookClient({ url: process.env.WEBHOOK_URL });

// Added dependency
const { google } = require("googleapis");

// Initialize with OAuth2 config and Pub/Sub topic
const gmailpush = new Gmailpush({
  clientId: process.env.CLIENT_ID,
  clientSecret: process.env.CLIENT_SECRET,
  pubsubTopic: process.env.TOPIC_URL,
});

const users = [
  {
    email: process.env.EMAIL,
    token: {
      access_token: jsonToken.access_token,
      refresh_token: jsonToken.refresh_token,
      scope: jsonToken.scope,
      token_type: jsonToken.token_type,
      expiry_date: jsonToken.expiry_date,
    },
  },
];

const job = schedule.scheduleJob("0 0 * * *", async () => {
  try {
    const gmailpushHistoryJson = await fs.readFile("gmailpush_history.json");
    const prevHistories = JSON.parse(gmailpushHistoryJson);

    await Promise.all(
      prevHistories.map(async (prevHistory) => {
        try {
          const { token } = users.find(
            (user) => user.email === prevHistory.emailAddress
          );

          gmailpush._api.auth.setCredentials(token);
          gmailpush._api.gmail = google.gmail({
            version: 'v1',
            auth: gmailpush._api.auth,
          });

          await gmailpush._api.gmail.users.watch({
            userId: prevHistory.emailAddress,
            requestBody: {
              topicName: gmailpush._api.pubsubTopic,
            },
          });
        } catch (err) {
          console.error(err);
        }
      })
    );
  } catch (err) {
    console.error(err);
  }
});

app.post(
  // Use URL set as Pub/Sub Subscription endpoint
  "/pubsub",
  express.json(),
  async (req, res) => {
    res.sendStatus(200);
    const email = gmailpush.getEmailAddress(req.body);
    const token = users.find((user) => user.email === email).token;

    const { subject } = await gmailpush
      .getNewMessage({
        notification: req.body,
        token,
      })
      .then((message) => {
        if (message === null) {
          return {};
        }
        if (!message.labelIds.includes(process.env.EMAIL_LABEL)) {
          return {};
        }
        /*
        fs.readFile('./gmailpush_history.json')
          .then((result) => {
            const prevHistories = JSON.parse(result);
            const prevHistory = prevHistories.find((prevHistory) => prevHistory.emailAddress === email);
            schedule.scheduleJob(new Date(prevHistory.watchExpiration - 1000 * 60 * 30), async () => {
              prevHistory.watchExpiration = await gmailpush._refreshWatch();
              fs.writeFile('./gmailpush_history.json', JSON.stringify(prevHistories));
            });
          });
        */

        return message;
      });
    if (subject) {
      webhook
        .send({ content: subject })
        .then((message) => console.log(`Sent message: ${message.content}`))
        .catch(console.error);
    } else {
      console.log(
        "Not sending message: Email Subject does not match label ID."
      );
    }
  }
);

app.listen(3002, () => {
  console.log("Server listening on port 3002...");
});
shelomito12 commented 1 year ago

Unfortunately, it works for a while then I have to manually renew token. Any plans for your API to accept using service account credential that is easier to implement API to work server to server?