google-auth-library
Version:
Google APIs Authentication Client Library for Node.js
157 lines (156 loc) • 6.77 kB
JavaScript
// Copyright 2022 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
//
// http://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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluggableAuthHandler = void 0;
const pluggable_auth_client_1 = require("./pluggable-auth-client");
const executable_response_1 = require("./executable-response");
const childProcess = require("child_process");
const fs = require("fs");
/**
* A handler used to retrieve 3rd party token responses from user defined
* executables and cached file output for the PluggableAuthClient class.
*/
class PluggableAuthHandler {
/**
* Instantiates a PluggableAuthHandler instance using the provided
* PluggableAuthHandlerOptions object.
*/
constructor(options) {
if (!options.command) {
throw new Error('No command provided.');
}
this.commandComponents = PluggableAuthHandler.parseCommand(options.command);
this.timeoutMillis = options.timeoutMillis;
if (!this.timeoutMillis) {
throw new Error('No timeoutMillis provided.');
}
this.outputFile = options.outputFile;
}
/**
* Calls user provided executable to get a 3rd party subject token and
* returns the response.
* @param envMap a Map of additional Environment Variables required for
* the executable.
* @return A promise that resolves with the executable response.
*/
retrieveResponseFromExecutable(envMap) {
return new Promise((resolve, reject) => {
// Spawn process to run executable using added environment variables.
const child = childProcess.spawn(this.commandComponents[0], this.commandComponents.slice(1), {
env: { ...process.env, ...Object.fromEntries(envMap) },
});
let output = '';
// Append stdout to output as executable runs.
child.stdout.on('data', (data) => {
output += data;
});
// Append stderr as executable runs.
child.stderr.on('data', (err) => {
output += err;
});
// Set up a timeout to end the child process and throw an error.
const timeout = setTimeout(() => {
// Kill child process and remove listeners so 'close' event doesn't get
// read after child process is killed.
child.removeAllListeners();
child.kill();
return reject(new Error('The executable failed to finish within the timeout specified.'));
}, this.timeoutMillis);
child.on('close', (code) => {
// Cancel timeout if executable closes before timeout is reached.
clearTimeout(timeout);
if (code === 0) {
// If the executable completed successfully, try to return the parsed response.
try {
const responseJson = JSON.parse(output);
const response = new executable_response_1.ExecutableResponse(responseJson);
return resolve(response);
}
catch (error) {
if (error instanceof executable_response_1.ExecutableResponseError) {
return reject(error);
}
return reject(new executable_response_1.ExecutableResponseError(`The executable returned an invalid response: ${output}`));
}
}
else {
return reject(new pluggable_auth_client_1.ExecutableError(output, code.toString()));
}
});
});
}
/**
* Checks user provided output file for response from previous run of
* executable and return the response if it exists, is formatted correctly, and is not expired.
*/
async retrieveCachedResponse() {
if (!this.outputFile || this.outputFile.length === 0) {
return undefined;
}
let filePath;
try {
filePath = await fs.promises.realpath(this.outputFile);
}
catch (_a) {
// If file path cannot be resolved, return undefined.
return undefined;
}
if (!(await fs.promises.lstat(filePath)).isFile()) {
// If path does not lead to file, return undefined.
return undefined;
}
const responseString = await fs.promises.readFile(filePath, {
encoding: 'utf8',
});
if (responseString === '') {
return undefined;
}
try {
const responseJson = JSON.parse(responseString);
const response = new executable_response_1.ExecutableResponse(responseJson);
// Check if response is successful and unexpired.
if (response.isValid()) {
return new executable_response_1.ExecutableResponse(responseJson);
}
return undefined;
}
catch (error) {
if (error instanceof executable_response_1.ExecutableResponseError) {
throw error;
}
throw new executable_response_1.ExecutableResponseError(`The output file contained an invalid response: ${responseString}`);
}
}
/**
* Parses given command string into component array, splitting on spaces unless
* spaces are between quotation marks.
*/
static parseCommand(command) {
// Split the command into components by splitting on spaces,
// unless spaces are contained in quotation marks.
const components = command.match(/(?:[^\s"]+|"[^"]*")+/g);
if (!components) {
throw new Error(`Provided command: "${command}" could not be parsed.`);
}
// Remove quotation marks from the beginning and end of each component if they are present.
for (let i = 0; i < components.length; i++) {
if (components[i][0] === '"' && components[i].slice(-1) === '"') {
components[i] = components[i].slice(1, -1);
}
}
return components;
}
}
exports.PluggableAuthHandler = PluggableAuthHandler;
;