@google/clasp
Version:
Develop Apps Script Projects locally
246 lines (245 loc) • 10.8 kB
JavaScript
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file contains functions for initializing and managing authentication,
// including OAuth2 client creation, authorization flows, and credential storage.
import { readFileSync } from 'fs';
import os from 'os';
import path from 'path';
import Debug from 'debug';
import { GoogleAuth, OAuth2Client } from 'google-auth-library';
import { google } from 'googleapis';
import { FileCredentialStore } from './file_credential_store.js';
import { LocalServerAuthorizationCodeFlow } from './localhost_auth_code_flow.js';
import { ServerlessAuthorizationCodeFlow } from './serverless_auth_code_flow.js';
const debug = Debug('clasp:auth');
/**
* Initializes authentication, loading credentials if available or preparing for a new auth flow.
* @param {InitOptions} options - Options for initializing authentication.
* @param {string} [options.authFilePath] - Path to the credentials file. Defaults to ~/.clasprc.json.
* @param {string} [options.userKey] - Identifier for the user credentials to load. Defaults to 'default'.
* @param {boolean} [options.useApplicationDefaultCredentials] - Whether to use Application Default Credentials.
* @returns {Promise<AuthInfo>} An AuthInfo object with the credential store and potentially loaded credentials.
*/
export async function initAuth(options) {
var _a, _b, _c;
const authFilePath = (_a = options.authFilePath) !== null && _a !== void 0 ? _a : path.join(os.homedir(), '.clasprc.json');
const credentialStore = new FileCredentialStore(authFilePath);
debug('Initializng auth from %s', options.authFilePath);
if (options.useApplicationDefaultCredentials) {
const credentials = await createApplicationDefaultCredentials();
return {
credentials,
credentialStore,
user: (_b = options.userKey) !== null && _b !== void 0 ? _b : 'default',
};
}
const credentials = await getAuthorizedOAuth2Client(credentialStore, options.userKey);
return {
credentials,
credentialStore,
user: (_c = options.userKey) !== null && _c !== void 0 ? _c : 'default',
};
}
/**
* Fetches user information (email, ID) using the provided OAuth2 client.
* @param {OAuth2Client} credentials - An authorized OAuth2 client.
* @returns {Promise<{email?: string | null; id?: string | null} | undefined>}
* User's email and ID, or undefined if an error occurs or no data is returned.
*/
export async function getUserInfo(credentials) {
debug('Fetching user info');
const api = google.oauth2('v2');
try {
const res = await api.userinfo.get({ auth: credentials });
if (!res.data) {
debug('No user info returned');
return undefined;
}
return {
email: res.data.email,
id: res.data.id,
};
}
catch (err) {
debug('Error while fetching userinfo: %O', err);
return undefined;
}
}
/**
* Creates an an unauthorized oauth2 client given the client secret file. If no path is provided,
* teh default client is returned.
* @param {string} [clientSecretPath] - Optional path to a client secrets JSON file.
* If not provided, the default clasp OAuth client is used.
* @returns {OAuth2Client} An unauthorized OAuth2 client instance.
*/
export function getUnauthorizedOuth2Client(clientSecretPath) {
if (clientSecretPath) {
return createOauthClient(clientSecretPath);
}
return createDefaultOAuthClient();
}
/**
* Create an authorized oauth2 client from saved credentials.
* @param {CredentialStore} store - The credential store to load from.
* @param {string} [userKey='default'] - The user key for the credentials.
* @returns {Promise<OAuth2Client | undefined>} An authorized OAuth2 client if credentials
* are found and valid, otherwise undefined. The client is configured to auto-refresh
* tokens and save them back to the store.
*/
export async function getAuthorizedOAuth2Client(store, userKey) {
if (!userKey) {
userKey = 'default';
}
debug('Loading credentials for user %s', userKey);
const savedCredentials = await store.load(userKey);
if (!savedCredentials) {
debug('No saved credentials found.');
return undefined;
}
const client = new GoogleAuth().fromJSON(savedCredentials);
client.setCredentials(savedCredentials);
client.on('tokens', async (tokens) => {
debug('Saving refreshed token for user %s', userKey);
const refreshedCredentials = {
...savedCredentials,
expiry_date: tokens.expiry_date,
access_token: tokens.access_token,
id_token: tokens.access_token,
};
await store.save(userKey, refreshedCredentials);
});
return client;
}
/**
* Initiates an OAuth 2.0 authorization flow to obtain user consent and credentials.
* It selects between a local server flow or a serverless (manual) flow based on options.
* @param {AuthorizationOptions} options - Configuration for the authorization flow.
* @returns {Promise<OAuth2Client>} The authorized OAuth2 client.
*/
export async function authorize(options) {
let flow;
if (options.noLocalServer) {
debug('Starting auth with serverless flow');
flow = new ServerlessAuthorizationCodeFlow(options.oauth2Client);
}
else {
debug('Starting auth with local server flow');
flow = new LocalServerAuthorizationCodeFlow(options.oauth2Client, options.redirectPort);
}
const client = await flow.authorize(options.scopes);
await saveOauthClientCredentials(options.store, options.userKey, client);
debug('Auth complete');
return client;
}
/**
* Saves the obtained OAuth2 client credentials to the provided credential store.
* It also sets up an event listener on the client to save refreshed tokens.
* @param {CredentialStore} store - The credential store.
* @param {string} userKey - The user key for saving credentials.
* @param {OAuth2Client} oauth2Client - The OAuth2 client whose credentials are to be saved.
*/
async function saveOauthClientCredentials(store, userKey, oauth2Client) {
var _a, _b;
const savedCredentials = {
client_id: oauth2Client._clientId,
client_secret: oauth2Client._clientSecret,
type: 'authorized_user',
refresh_token: (_a = oauth2Client.credentials.refresh_token) !== null && _a !== void 0 ? _a : undefined,
access_token: (_b = oauth2Client.credentials.access_token) !== null && _b !== void 0 ? _b : undefined,
};
oauth2Client.on('tokens', async (tokens) => {
const refreshedCredentials = {
...savedCredentials,
expiry_date: tokens.expiry_date,
access_token: tokens.access_token,
id_token: tokens.access_token,
};
debug('Saving refreshed credentials for user %s', userKey);
await store.save(userKey, refreshedCredentials);
});
debug('Saving credentials for user %s', userKey);
await store.save(userKey, savedCredentials);
}
/**
* Creates an aunthorized oauth2 client with the given credentials
* @param clientSecretPath
* @returns
*/
function createOauthClient(clientSecretPath) {
debug('Creating new oauth client from %s', clientSecretPath);
if (!clientSecretPath) {
throw new Error('Invalid credentials');
}
const contents = readFileSync(clientSecretPath);
const keyFile = JSON.parse(contents.toString());
const keys = keyFile.installed || keyFile.web;
if (!keys.redirect_uris || keys.redirect_uris.length === 0) {
throw new Error('Invalid redirect URL');
}
const redirectUrl = keys.redirect_uris.find((uri) => new URL(uri).hostname === 'localhost');
if (!redirectUrl) {
throw new Error('No localhost redirect URL found');
}
// create an oAuth client to authorize the API call
const client = new OAuth2Client({
clientId: keys.client_id,
clientSecret: keys.client_secret,
redirectUri: redirectUrl,
});
debug('Created built-in oauth client, id: %s', client._clientId);
return client;
}
/**
* Creates an aunthorized oauth2 client using the default id & secret.
* @param clientSecretPath
* @returns
*/
function createDefaultOAuthClient() {
// Default client
const client = new OAuth2Client({
clientId: '1072944905499-vm2v2i5dvn0a0d2o4ca36i1vge8cvbn0.apps.googleusercontent.com',
clientSecret: 'v6V3fKV_zWU7iw1DrpO1rknX',
redirectUri: 'http://localhost',
});
debug('Created built-in oauth client, id: %s', client._clientId);
return client;
}
/**
* Attempts to create an OAuth2Client using Google Application Default Credentials (ADC).
* This is typically used in server environments where credentials can be automatically discovered.
* @returns {Promise<OAuth2Client | undefined>} An OAuth2Client if ADC are available and valid,
* otherwise undefined.
*/
export async function createApplicationDefaultCredentials() {
const defaultCreds = await new GoogleAuth({
scopes: [
'https://www.googleapis.com/auth/script.deployments', // Apps Script deployments
'https://www.googleapis.com/auth/script.projects', // Apps Script management
'https://www.googleapis.com/auth/script.webapp.deploy', // Apps Script Web Apps
'https://www.googleapis.com/auth/drive.metadata.readonly', // Drive metadata
'https://www.googleapis.com/auth/drive.file', // Create Drive files
'https://www.googleapis.com/auth/service.management', // Cloud Project Service Management API
'https://www.googleapis.com/auth/logging.read', // StackDriver logs
'https://www.googleapis.com/auth/userinfo.email', // User email address
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/cloud-platform',
],
}).getClient();
// Remove this check after https://github.com/googleapis/google-auth-library-nodejs/issues/1677 fixed
if (defaultCreds instanceof OAuth2Client) {
debug('Created service account credentials, id: %s', defaultCreds._clientId);
return defaultCreds;
}
return undefined;
}