OAuth 2.0 Authorization Script for Google Drive API

Prerequisites

  • Completed Lesson 1 — you have a Google Cloud project with Drive API enabled and an OAuth credentials/gdrive-oauth.json file
  • Node.js 18+ installed
  • googleapis npm package installed
npm install googleapis

Project Structure

We will create two files:

your-project/
├── credentials/
│   ├── gdrive-oauth.json       ← Downloaded from Google Cloud Console
│   └── gdrive-token.json       ← Created by the auth script (auto-generated)
├── src/
│   ├── gdrive-auth.js          ← Auth helper (shared by all scripts)
│   └── gdrive-authorize.js     ← One-time authorization (run once)
├── .gitignore
└── package.json

Add to .gitignore:

credentials/

Credential Files Reference

FileContainsCreated howAutomatic?Security
credentials/gdrive-oauth.jsonOAuth client ID + client secretDownload manually from Google Cloud Console → Credentials → OAuth clientNo — one-time manual downloadSecret — anyone with this can impersonate your app
credentials/gdrive-token.jsonAccess token + refresh tokenGenerated automatically when you run npm run gdrive-authSemi — you run the command once, it saves the fileSecret — anyone with this can read/write your Drive files without logging in
credentials/gdrive-service-account.jsonPrivate key for a service accountDownload manually from Google Cloud Console → Service Accounts → KeysNo — one-time manual downloadSecret — full access to whatever the service account can reach

Never commit these files to git. The credentials/ directory should always be in .gitignore. If someone clones your repo, they run npm run gdrive-auth to generate their own tokens with their own Google account.

Service accounts vs OAuth: Most projects use OAuth (the first two files). Service accounts are a separate auth method — a robot account with its own email, useful for server-to-server automation without user interaction. If your project only uses OAuth, you don't need a service account file.


File 1: src/gdrive-auth.js — Authentication Helper

This module is imported by both the authorization script and the upload script.

const { google } = require('googleapis')
const path = require('path')
const fs = require('fs')

const CREDS_PATH = path.join(
  __dirname,
  '..',
  'credentials',
  'gdrive-oauth.json',
)
const TOKEN_PATH = path.join(
  __dirname,
  '..',
  'credentials',
  'gdrive-token.json',
)

const SCOPES = ['https://www.googleapis.com/auth/drive.file']

function loadOAuthClient() {
  if (!fs.existsSync(CREDS_PATH)) {
    console.error(
      '\n❌ OAuth credentials not found at: credentials/gdrive-oauth.json',
    )
    console.error(
      '   Download from Google Cloud Console → Credentials → OAuth client\n',
    )
    process.exit(1)
  }

  const content = JSON.parse(fs.readFileSync(CREDS_PATH, 'utf-8'))
  const creds = content.installed || content.web
  if (!creds) {
    console.error('\n❌ Invalid OAuth credentials. Use "Desktop app" type.\n')
    process.exit(1)
  }

  return new google.auth.OAuth2(
    creds.client_id,
    creds.client_secret,
    `http://localhost:${process.env.GDRIVE_AUTH_PORT || 3456}`,
  )
}

function getDriveClient() {
  const oAuth2Client = loadOAuthClient()

  if (!fs.existsSync(TOKEN_PATH)) {
    console.error('\n❌ No saved token. Run: npm run gdrive-auth\n')
    process.exit(1)
  }

  const token = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf-8'))
  oAuth2Client.setCredentials(token)

  // Auto-save refreshed tokens
  oAuth2Client.on('tokens', (newTokens) => {
    const merged = { ...token, ...newTokens }
    fs.writeFileSync(
      TOKEN_PATH,
      JSON.stringify(merged, null, 2) + '\n',
      'utf-8',
    )
  })

  return google.drive({ version: 'v3', auth: oAuth2Client })
}

module.exports = { loadOAuthClient, getDriveClient, SCOPES, TOKEN_PATH }

Key details

  • SCOPES: drive.file only allows access to files created by this app — it cannot see or modify your other Drive files. This is the most restrictive scope that still allows uploads.
  • Redirect URI: Uses http://localhost:3456 — the port can be changed via the GDRIVE_AUTH_PORT environment variable if 3456 is in use.
  • Auto token refresh: When the access token expires, the googleapis library automatically uses the refresh token. The on("tokens") listener saves the new token to disk.

File 2: src/gdrive-authorize.js — One-Time Authorization

This script runs once to get your consent and save a refresh token.

const http = require('http')
const { URL } = require('url')
const fs = require('fs')
const { loadOAuthClient, SCOPES, TOKEN_PATH } = require('./gdrive-auth')

const PORT = 3456

async function authorize() {
  const oAuth2Client = loadOAuthClient()

  const authUrl = oAuth2Client.generateAuthUrl({
    access_type: 'offline',
    prompt: 'consent',
    scope: SCOPES,
  })

  console.log('\n🔐 Google Drive Authorization\n')
  console.log('   Opening your browser...\n')
  console.log("   If it doesn't open, visit this URL manually:\n")
  console.log(`   ${authUrl}\n`)

  // Open browser (macOS)
  const { exec } = require('child_process')
  exec(`open "${authUrl}"`)

  return new Promise((resolve, reject) => {
    const server = http.createServer(async (req, res) => {
      try {
        const url = new URL(req.url, `http://localhost:${PORT}`)

        const code = url.searchParams.get('code')
        if (!code) {
          res.writeHead(400)
          res.end('No authorization code received')
          return
        }

        const { tokens } = await oAuth2Client.getToken(code)

        fs.writeFileSync(
          TOKEN_PATH,
          JSON.stringify(tokens, null, 2) + '\n',
          'utf-8',
        )

        res.writeHead(200, { 'Content-Type': 'text/html' })
        res.end(`
          <html><body style="font-family: sans-serif; text-align: center; padding: 50px;">
            <h1>✅ Authorization successful!</h1>
            <p>You can close this tab and go back to the terminal.</p>
          </body></html>
        `)

        console.log('   ✅ Token saved to credentials/gdrive-token.json\n')
        server.close()
        resolve()
      } catch (err) {
        res.writeHead(500)
        res.end('Authorization failed: ' + err.message)
        server.close()
        reject(err)
      }
    })

    server.listen(PORT, () => {
      console.log(
        `   Waiting for authorization on http://localhost:${PORT}...\n`,
      )
    })

    server.on('error', (err) => {
      if (err.code === 'EADDRINUSE') {
        console.error(
          `\n❌ Port ${PORT} is in use. Set GDRIVE_AUTH_PORT or close the process.\n`,
        )
      }
      reject(err)
    })
  })
}

authorize().catch((err) => {
  console.error('\n❌ Authorization failed:', err.message)
  process.exit(1)
})

Add npm Scripts

In package.json:

{
  "scripts": {
    "gdrive-auth": "node src/gdrive-authorize.js"
  }
}

Running the Authorization

npm run gdrive-auth

What happens

  1. The script starts a local HTTP server on port 3456
  2. Your browser opens to Google's consent page
  3. You log in with the Google account that owns the Drive folders
  4. Google shows a consent screen — click Allow
  5. Google redirects to http://localhost:3456 with an authorization code
  6. The script exchanges the code for access + refresh tokens
  7. Tokens are saved to credentials/gdrive-token.json
  8. The browser shows "Authorization successful!" — close the tab

"This app isn't verified" warning

If your OAuth consent screen is set to External and not verified by Google, you'll see a warning. This is normal for personal projects:

  1. Click Advanced
  2. Click Go to your-app-name (unsafe)
  3. Click Continue

This warning does not appear if your consent screen is set to Internal (Google Workspace only).

Port conflict

If port 3456 is already in use (e.g., by nginx or another service), change it:

GDRIVE_AUTH_PORT=4567 npm run gdrive-auth

Token Lifecycle

The saved gdrive-token.json contains:

FieldPurposeLifetime
access_tokenUsed for API calls~1 hour
refresh_tokenUsed to get new access tokensIndefinite (until revoked)
expiry_dateWhen the access token expiresAuto-managed

The getDriveClient() function in gdrive-auth.js automatically refreshes expired tokens and saves the new ones to disk. You only need to run npm run gdrive-auth once — unless you revoke access or the refresh token expires.


What's Next

In the next lesson, we'll build the upload script that watches local directories and automatically uploads new files to Google Drive folders.