Created
May 9, 2025 18:37
-
-
Save stevenvachon/a78e7dac3dd3489eb83231f58d5b3d75 to your computer and use it in GitHub Desktop.
Google API OAuth in a CLI tool
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { confirm } from '@inquirer/prompts'; | |
import { createServer } from 'node:https'; | |
import { google } from 'googleapis'; | |
import open from 'open'; | |
import { readFile, unlink as removeFile } from 'node:fs/promises'; | |
import spawn from 'nano-spawn'; | |
// Google API OAuth | |
const CLIENT_ID = | |
'129949395122-ht30bnmmmor0qenascegaa4vmgu09hgr.apps.googleusercontent.com'; | |
const CLIENT_SECRET = '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'; | |
const CLIENT_REDIRECT_URL_DOMAIN = 'localhost'; | |
const CLIENT_REDIRECT_URL_PORT = 3000; | |
const CLIENT_REDIRECT_URL = `https://${CLIENT_REDIRECT_URL_DOMAIN}:${CLIENT_REDIRECT_URL_PORT}`; | |
/** | |
* Authenticate with Google Docs API using OAuth2. | |
* @returns {Promise<OAuth2Client>} | |
*/ | |
const authenticate = async () => { | |
if ( | |
await confirm({ | |
message: 'Open authorization in your default web browser?', | |
}) | |
) { | |
const auth = new google.auth.OAuth2( | |
CLIENT_ID, | |
CLIENT_SECRET, | |
CLIENT_REDIRECT_URL | |
); | |
open( | |
auth.generateAuthUrl({ | |
access_type: 'offline', | |
scope: ['https://www.googleapis.com/auth/documents.readonly'], | |
}) | |
); | |
console.info( | |
'(Be sure to click "Advanced" then "Proceed to…" after authorizing)' | |
); | |
auth.setCredentials( | |
(await auth.getToken(await getRedirectedAuthCode())).tokens | |
); | |
return auth; | |
} | |
}; | |
/** | |
* Get the authorization code from Google API's redirect. | |
* @returns {Promise<string>} | |
*/ | |
const getRedirectedAuthCode = async () => { | |
// Generate a self-signed certificate | |
const CERT_FILE = `${CLIENT_REDIRECT_URL_DOMAIN}.pem`; | |
const KEY_FILE = `${CLIENT_REDIRECT_URL_DOMAIN}-key.pem`; | |
// Could've used `mkcert` to avoid self-signing, but its `-install` command requires `sudo` | |
await spawn('openssl', [ | |
'req', | |
'-x509', | |
'-newkey', | |
'rsa:2048', | |
'-nodes', | |
'-keyout', | |
KEY_FILE, | |
'-out', | |
CERT_FILE, | |
'-days', | |
'1', | |
'-subj', | |
`/C=US/ST=California/L=San Francisco/O=MyOrg/OU=Dev/CN=${CLIENT_REDIRECT_URL_DOMAIN}`, | |
'-addext', | |
`subjectAltName=DNS:${CLIENT_REDIRECT_URL_DOMAIN}`, | |
]); | |
const [cert, key] = await Promise.all( | |
[CERT_FILE, KEY_FILE].map(async (filePath) => { | |
const buffer = readFile(filePath); | |
await removeFile(filePath); | |
return buffer; | |
}) | |
); | |
// Start a simple server to catch the redirect | |
const code = await new Promise((resolve, reject) => { | |
const server = createServer({ cert, key }, async ({ url }, res) => { | |
const HEADERS = { 'Content-Type': 'text/plain' }; | |
const code = new URL(url, CLIENT_REDIRECT_URL).searchParams.get('code'); | |
if (code) { | |
res.writeHead(200, HEADERS); | |
res.end('Authorization successful! You can close this window.'); | |
resolve(code); | |
} else { | |
res.writeHead(400, HEADERS); | |
const msg = 'No code found in query string'; | |
res.end(`${msg}. You can close this window.`); | |
reject(new Error(msg)); | |
} | |
server.close(); | |
}).listen(CLIENT_REDIRECT_URL_PORT); | |
}); | |
return code; | |
}; | |
const run = async () => { | |
// Handle Ctrl+C gracefully | |
try { | |
await authenticate(); | |
} catch (error) { | |
if (error instanceof Error && error.name === 'ExitPromptError') { | |
// Quietly exit | |
} else { | |
throw error; | |
} | |
} | |
}; | |
run(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"private": true, | |
"name": "google-oauth-cli", | |
"version": "1.0.0", | |
"main": "index.mjs", | |
"dependencies": { | |
"@inquirer/prompts": "^7.5.0", | |
"googleapis": "^148.0.0", | |
"nano-spawn": "^0.2.1", | |
"open": "^10.1.2" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment