Skip to content

Instantly share code, notes, and snippets.

@stevenvachon
Created May 9, 2025 18:37
Show Gist options
  • Save stevenvachon/a78e7dac3dd3489eb83231f58d5b3d75 to your computer and use it in GitHub Desktop.
Save stevenvachon/a78e7dac3dd3489eb83231f58d5b3d75 to your computer and use it in GitHub Desktop.
Google API OAuth in a CLI tool
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();
{
"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