From 63ab7ccbd26b0c15063b1dcf797d7f43b2990081 Mon Sep 17 00:00:00 2001 From: Maggie Nolan Date: Fri, 12 Jan 2018 10:21:43 -0800 Subject: [PATCH] Make eagerRefreshThresholdMillis an option which can be passed to functions in googleAuth --- src/auth/computeclient.ts | 4 +- src/auth/googleauth.ts | 108 +++++++++++++++++++++----------------- test/test.googleauth.ts | 107 ++++++++++++++++++++++++++++++++++++- 3 files changed, 169 insertions(+), 50 deletions(-) diff --git a/src/auth/computeclient.ts b/src/auth/computeclient.ts index 0c6941ae..f6ba6d70 100644 --- a/src/auth/computeclient.ts +++ b/src/auth/computeclient.ts @@ -20,6 +20,8 @@ import {RequestError} from './../transporters'; import {CredentialRequest, Credentials} from './credentials'; import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client'; +export interface ComputeOptions extends RefreshOptions {} + export class Compute extends OAuth2Client { /** * Google Compute Engine metadata server token endpoint. @@ -33,7 +35,7 @@ export class Compute extends OAuth2Client { * Retrieve access token from the metadata server. * See: https://developers.google.com/compute/docs/authentication */ - constructor(options?: RefreshOptions) { + constructor(options?: ComputeOptions) { super(options); // Start with an expired refresh token, which will automatically be // refreshed before the first API call is made. diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 7c3583be..048ed52f 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -82,8 +82,6 @@ export class GoogleAuth { cachedCredential: OAuth2Client|null = null; - private eagerRefreshThresholdMillis: number|undefined; - /** * Export DefaultTransporter as a static property of the class. */ @@ -167,17 +165,28 @@ export class GoogleAuth { */ getApplicationDefault(): Promise; getApplicationDefault(callback: ADCCallback): void; - getApplicationDefault(callback?: ADCCallback): void|Promise { + getApplicationDefault(options: RefreshOptions): Promise; + getApplicationDefault(options: RefreshOptions, callback: ADCCallback): void; + getApplicationDefault( + optionsOrCallback: ADCCallback|RefreshOptions = {}, + callback?: ADCCallback): void|Promise { + let options: RefreshOptions|undefined; + if (typeof optionsOrCallback === 'function') { + callback = optionsOrCallback; + } else { + options = optionsOrCallback; + } if (callback) { - this.getApplicationDefaultAsync() - .then(r => callback(null, r.credential, r.projectId)) + this.getApplicationDefaultAsync(options) + .then(r => callback!(null, r.credential, r.projectId)) .catch(callback); } else { - return this.getApplicationDefaultAsync(); + return this.getApplicationDefaultAsync(options); } } - private async getApplicationDefaultAsync(): Promise { + private async getApplicationDefaultAsync(options?: RefreshOptions): + Promise { // If we've already got a cached credential, just return it. if (this.cachedCredential) { return { @@ -192,7 +201,8 @@ export class GoogleAuth { // location of the credential file. This is typically used in local // developer scenarios. credential = - await this._tryGetApplicationCredentialsFromEnvironmentVariable(); + await this._tryGetApplicationCredentialsFromEnvironmentVariable( + options); if (credential) { this.cachedCredential = credential; projectId = await this.getDefaultProjectId(); @@ -200,7 +210,8 @@ export class GoogleAuth { } // Look in the well-known credential file location. - credential = await this._tryGetApplicationCredentialsFromWellKnownFile(); + credential = + await this._tryGetApplicationCredentialsFromWellKnownFile(options); if (credential) { this.cachedCredential = credential; projectId = await this.getDefaultProjectId(); @@ -214,11 +225,7 @@ export class GoogleAuth { // For GCE, just return a default ComputeClient. It will take care of // the rest. // TODO: cache the result - return { - projectId: null, - credential: new Compute( - {eagerRefreshThresholdMillis: this.eagerRefreshThresholdMillis}) - }; + return {projectId: null, credential: new Compute(options)}; } else { // We failed to find the default credentials. Bail out with an error. throw new Error( @@ -231,14 +238,6 @@ export class GoogleAuth { } } - /** - * Sets time to eagerly refresh tokens before they are expired. - * @param {number=} eagerRefreshThresholdMillis Time in ms to refresh an unexpired token before it expires. - */ - setEagerRefreshThresholdMillis(eagerRefreshThresholdMillis: number) { - this.eagerRefreshThresholdMillis = eagerRefreshThresholdMillis; - } - /** * Determines whether the auth layer is running on Google Compute Engine. * @returns A promise that resolves with the boolean. @@ -280,14 +279,15 @@ export class GoogleAuth { * @returns Promise that resolves with the OAuth2Client or null. * @api private */ - async _tryGetApplicationCredentialsFromEnvironmentVariable(): - Promise { + async _tryGetApplicationCredentialsFromEnvironmentVariable( + options?: RefreshOptions): Promise { const credentialsPath = this._getEnv('GOOGLE_APPLICATION_CREDENTIALS'); if (!credentialsPath || credentialsPath.length === 0) { return null; } try { - return this._getApplicationCredentialsFromFilePath(credentialsPath); + return this._getApplicationCredentialsFromFilePath( + credentialsPath, options); } catch (e) { throw this.createError( 'Unable to read the credential file specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable.', @@ -300,8 +300,8 @@ export class GoogleAuth { * @return Promise that resolves with the OAuth2Client or null. * @api private */ - async _tryGetApplicationCredentialsFromWellKnownFile(): - Promise { + async _tryGetApplicationCredentialsFromWellKnownFile( + options?: RefreshOptions): Promise { // First, figure out the location of the file, depending upon the OS type. let location = null; if (this._isWindows()) { @@ -330,7 +330,7 @@ export class GoogleAuth { return null; } // The file seems to exist. Try to use it. - return this._getApplicationCredentialsFromFilePath(location); + return this._getApplicationCredentialsFromFilePath(location, options); } /** @@ -339,8 +339,9 @@ export class GoogleAuth { * @returns Promise that resolves with the OAuth2Client * @api private */ - async _getApplicationCredentialsFromFilePath(filePath: string): - Promise { + async _getApplicationCredentialsFromFilePath( + filePath: string, + options: RefreshOptions = {}): Promise { // Make sure the path looks like a string. if (!filePath || filePath.length === 0) { throw new Error('The file path is invalid.'); @@ -366,7 +367,7 @@ export class GoogleAuth { // Now open a read stream on the file, and parse it. try { const readStream = this._createReadStream(filePath); - return this.fromStream(readStream); + return this.fromStream(readStream, options); } catch (err) { throw this.createError( util.format('Unable to read the file at %s.', filePath), err); @@ -378,19 +379,18 @@ export class GoogleAuth { * @param {object=} json The input object. * @returns JWT or UserRefresh Client with data */ - fromJSON(json: JWTInput): JWT|UserRefreshClient { + fromJSON(json: JWTInput, options?: RefreshOptions): JWT|UserRefreshClient { let client: UserRefreshClient|JWT; if (!json) { throw new Error( 'Must pass in a JSON object containing the Google auth settings.'); } this.jsonContent = json; + options = options || {}; if (json.type === 'authorized_user') { - client = new UserRefreshClient( - {eagerRefreshThresholdMillis: this.eagerRefreshThresholdMillis}); + client = new UserRefreshClient(options); } else { - client = new JWT( - {eagerRefreshThresholdMillis: this.eagerRefreshThresholdMillis}); + client = new JWT(options); } client.fromJSON(json); return client; @@ -403,19 +403,33 @@ export class GoogleAuth { */ fromStream(inputStream: stream.Readable): Promise; fromStream(inputStream: stream.Readable, callback: CredentialCallback): void; - fromStream(inputStream: stream.Readable, callback?: CredentialCallback): - Promise|void { + fromStream(inputStream: stream.Readable, options: RefreshOptions): + Promise; + fromStream( + inputStream: stream.Readable, options: RefreshOptions, + callback: CredentialCallback): void; + fromStream( + inputStream: stream.Readable, + optionsOrCallback: RefreshOptions|CredentialCallback = {}, + callback?: CredentialCallback): Promise|void { + let options: RefreshOptions = {}; + if (typeof optionsOrCallback === 'function') { + callback = optionsOrCallback; + } else { + options = optionsOrCallback; + } if (callback) { - this.fromStreamAsync(inputStream) - .then(r => callback(null, r)) + this.fromStreamAsync(inputStream, options) + .then(r => callback!(null, r)) .catch(callback); } else { - return this.fromStreamAsync(inputStream); + return this.fromStreamAsync(inputStream, options); } } - private fromStreamAsync(inputStream: stream.Readable): - Promise { + private fromStreamAsync( + inputStream: stream.Readable, + options?: RefreshOptions): Promise { return new Promise((resolve, reject) => { if (!inputStream) { throw new Error( @@ -429,7 +443,7 @@ export class GoogleAuth { inputStream.on('end', () => { try { const data = JSON.parse(s); - const r = this.fromJSON(data); + const r = this.fromJSON(data, options); return resolve(r); } catch (err) { return reject(err); @@ -443,9 +457,9 @@ export class GoogleAuth { * @param {string} - The API key string * @returns A JWT loaded from the key */ - fromAPIKey(apiKey: string): JWT { - const client = new JWT( - {eagerRefreshThresholdMillis: this.eagerRefreshThresholdMillis}); + fromAPIKey(apiKey: string, options?: RefreshOptions): JWT { + options = options || {}; + const client = new JWT(options); client.fromAPIKey(apiKey); return client; } diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 925870bc..6da90e30 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -258,6 +258,14 @@ describe('GoogleAuth', () => { }); }); }); + + describe('With eager retry', () => { + it('should make client with eagerRetryThresholdMillis set', () => { + const client = + auth.fromAPIKey(API_KEY, {eagerRefreshThresholdMillis: 100}); + assert.equal(100, client.eagerRefreshThresholdMillis); + }); + }); }); }); @@ -329,8 +337,8 @@ describe('GoogleAuth', () => { () => { const json = createJwtJSON(); const auth = new GoogleAuth(); - auth.setEagerRefreshThresholdMillis(5000); - const result = auth.fromJSON(json); + const result = + auth.fromJSON(json, {eagerRefreshThresholdMillis: 5000}); assert.equal(5000, (result as JWT).eagerRefreshThresholdMillis); }); @@ -415,6 +423,31 @@ describe('GoogleAuth', () => { }); }); + + it('should read the stream and create a jwt with eager refresh', + async () => { + // Read the contents of the file into a json object. + const fileContents = + fs.readFileSync('./test/fixtures/private.json', 'utf-8'); + const json = JSON.parse(fileContents); + + // Now open a stream on the same file. + const stream = fs.createReadStream('./test/fixtures/private.json'); + + // And pass it into the fromStream method. + const auth = new GoogleAuth(); + const result = await auth.fromStream( + stream, {eagerRefreshThresholdMillis: 1000 * 60 * 60}); + const jwt = result as JWT; + // Ensure that the correct bits were pulled from the stream. + assert.equal(json.private_key, jwt.key); + assert.equal(json.client_email, jwt.email); + assert.equal(null, jwt.keyFile); + assert.equal(null, jwt.subject); + assert.equal(null, jwt.scope); + assert.equal(1000 * 60 * 60, jwt.eagerRefreshThresholdMillis); + }); + it('should read another stream and create a UserRefreshClient', (done) => { // Read the contents of the file into a json object. const fileContents = @@ -436,6 +469,28 @@ describe('GoogleAuth', () => { done(); }); }); + + it('should read another stream and create a UserRefreshClient with eager refresh', + async () => { + // Read the contents of the file into a json object. + const fileContents = + fs.readFileSync('./test/fixtures/refresh.json', 'utf-8'); + const json = JSON.parse(fileContents); + + // Now open a stream on the same file. + const stream = fs.createReadStream('./test/fixtures/refresh.json'); + + // And pass it into the fromStream method. + const auth = new GoogleAuth(); + const result = + await auth.fromStream(stream, {eagerRefreshThresholdMillis: 100}); + // Ensure that the correct bits were pulled from the stream. + const rc = result as UserRefreshClient; + assert.equal(json.client_id, rc._clientId); + assert.equal(json.client_secret, rc._clientSecret); + assert.equal(json.refresh_token, rc._refreshToken); + assert.equal(100, rc.eagerRefreshThresholdMillis); + }); }); describe('._getApplicationCredentialsFromFilePath', () => { @@ -593,6 +648,28 @@ describe('GoogleAuth', () => { assert.equal(null, jwt.subject); assert.equal(null, jwt.scope); }); + + it('should correctly read the file and create a valid JWT with eager refresh', + async () => { + // Read the contents of the file into a json object. + const fileContents = + fs.readFileSync('./test/fixtures/private.json', 'utf-8'); + const json = JSON.parse(fileContents); + + // Now pass the same path to the auth loader. + const auth = new GoogleAuth(); + const result = await auth._getApplicationCredentialsFromFilePath( + './test/fixtures/private.json', + {eagerRefreshThresholdMillis: 7000}); + assert(result); + const jwt = result as JWT; + assert.equal(json.private_key, jwt.key); + assert.equal(json.client_email, jwt.email); + assert.equal(null, jwt.keyFile); + assert.equal(null, jwt.subject); + assert.equal(null, jwt.scope); + assert.equal(7000, jwt.eagerRefreshThresholdMillis); + }); }); describe('._tryGetApplicationCredentialsFromEnvironmentVariable', () => { @@ -651,6 +728,32 @@ describe('GoogleAuth', () => { assert.equal(null, jwt.subject); assert.equal(null, jwt.scope); }); + + it('should handle valid environment variable when there is eager refresh set', + async () => { + // Set up a mock to return path to a valid credentials file. + const auth = new GoogleAuth(); + insertEnvironmentVariableIntoAuth( + auth, 'GOOGLE_APPLICATION_CREDENTIALS', + './test/fixtures/private.json'); + + // Read the contents of the file into a json object. + const fileContents = + fs.readFileSync('./test/fixtures/private.json', 'utf-8'); + const json = JSON.parse(fileContents); + + // Execute. + const result = + await auth._tryGetApplicationCredentialsFromEnvironmentVariable( + {eagerRefreshThresholdMillis: 60 * 60 * 1000}); + const jwt = result as JWT; + assert.equal(json.private_key, jwt.key); + assert.equal(json.client_email, jwt.email); + assert.equal(null, jwt.keyFile); + assert.equal(null, jwt.subject); + assert.equal(null, jwt.scope); + assert.equal(60 * 60 * 1000, jwt.eagerRefreshThresholdMillis); + }); }); describe('._tryGetApplicationCredentialsFromWellKnownFile', () => {