Search code examples
javascriptnode.jsoauth-2.0google-apigoogle-oauth

How do I change a discord bot that accesses only my Google account in prep for OAuth2 OOB deprecation?


I have a discord bot, written in node.js using discord.js, that accesses a slice of my Google Drive for storing user preferences and data. Everything is currently working. It only accesses my Google account.

So far, I don't understand what to do in response to the email I received from Google last night, subject "[Action Required] Migrate your OAuth out-of-band flow to an alternative method before Oct. 3, 2022".

My bot currently uses a slightly modified version of the node.js quickstart code, found here: https://developers.google.com/drive/api/quickstart/nodejs (the modifications check for environment variables for authorization data, before checking for that data in files).

The bot is hosted at Heroku as a worker dyno (not a web dyno) and frankly it doesn't seem sensible that I should need to add web server functionality to the bot just to log in to Google... I'm probably missing something reasonably easy in their gobbledygook blog post and help files.

Relevant blog post: https://developers.googleblog.com/2022/02/making-oauth-flows-safer.html

Relevant help file: https://developers.google.com/identity/protocols/oauth2/native-app#redirect-uri_loopback

The relevant part of the bot's code:

// @ ============ GOOGLE * google * Google ===========
const fs = require('fs');
const readline = require('readline');
const {google} = require('googleapis');

// If modifying these scopes, delete googletoken.json.
const G_SCOPES = ['https://www.googleapis.com/auth/drive.appdata',
                'https://www.googleapis.com/auth/drive.file'];
// The file googletoken.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const G_TOKEN_PATH = 'googletoken.json';

// Load client secrets from env or local file.
if (process.env.hasOwnProperty('GOOGLE_CREDENTIALS')) {
  authorize(JSON.parse(process.env.GOOGLE_CREDENTIALS), initAll);
} else {
  fs.readFile('googlecredentials.json', (err, content) => {
    if (err) return console.log('Error loading client secret file:', err);
    // Authorize a client with credentials, then call the Google Drive API.
    authorize(JSON.parse(content), initAll);
  });
}

// @ =========== google's library functions =============
/**
 * Create an OAuth2 client with the given credentials, and then execute the
 * given callback function.
 * @param {Object} credentials The authorization client credentials.
 * @param {function} callback The callback to call with the authorized client.
 */
function authorize(credentials, callback) {
  const {client_secret, client_id, redirect_uris} = credentials.installed;
  const oAuth2Client = new google.auth.OAuth2(
      client_id, client_secret, redirect_uris[0]);

  // Check if we have previously stored a token.
  if (process.env.hasOwnProperty('GOOGLE_TOKEN')) {
    oAuth2Client.setCredentials(JSON.parse(process.env.GOOGLE_TOKEN));
    callback(oAuth2Client);
  } else {
    fs.readFile(G_TOKEN_PATH, (err, token) => {
      if (err) return getAccessToken(oAuth2Client, callback);
      oAuth2Client.setCredentials(JSON.parse(token));
      callback(oAuth2Client);
    });
  }
}

/**
 * Get and store new token after prompting for user authorization, and then
 * execute the given callback with the authorized OAuth2 client.
 * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
 * @param {getEventsCallback} callback The callback for the authorized client.
 */
function getAccessToken(oAuth2Client, callback) {
  const authUrl = oAuth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: G_SCOPES,
  });
  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(G_TOKEN_PATH, JSON.stringify(token), (err) => {
        if (err) return console.error(err);
        console.log('Token stored to', G_TOKEN_PATH);
      });
      callback(oAuth2Client);
    });
  });
}

Solution

  • Your credentials file should look something like this.

    {
      "installed": {
        "client_id": "[REDACTED]",
        "project_id": "daimto-tutorials-101",
        "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": "[REDACTED]",
        "redirect_uris": [
          "urn:ietf:wg:oauth:2.0:oob",
          "http://localhost"
        ]
      }
    }
    

    Remove the line that says "urn:ietf:wg:oauth:2.0:oob",

    Note when you run your code now your going to get a web page popping up with a 400 error in it. Ignore the error. The authorization code you need is in the URL bar. This is the code that you add when your application asks for it

    'Enter the code from that page here: '

    This is the best you can do currently I am in talks with the Oauth2 team to trying to get some feed back on how we can get a better result then a 400 error window when running installed apps like this. Currently this is what we have.

    Note google is in the process of updating all their samples but its a slow process.

    note

    Your code is currently authorized and authorization is stored in G_TOKEN_PATH. You need to force it to authorize the user again. To do this move that file out. I wouldn't delete it because if this doesn't work then your app will be broken just copy it someplace. And run your app again