Skip to content

Commit 42b5806

Browse files
authored
Support for specifying an http.Agent (#402)
* Experimental http agent support * Added some tests for http agent * More tests for http agent * Fixing refresh token test * Fixed hanging test case * Using http.Agent set in AppOptions when possible * Reverting some unrelated changes * Reverting some unrelated changes * Fixed a typo in d.ts file * Fixed unit tests * Importing Agent from http (not https) * Updated CHANGELOG
1 parent 9e11e18 commit 42b5806

File tree

10 files changed

+1339
-1405
lines changed

10 files changed

+1339
-1405
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Unreleased
22

3+
- [added] `AppOptions` now accepts an optional `http.Agent` object. The
4+
`http.Agent` specified via this API is used when the SDK makes backend
5+
HTTP calls. This can be used when it is required to deploy the Admin SDK
6+
behind a proxy.
7+
- [added] `admin.credential.cert()`, `admin.credential.applicationDefault()`,
8+
and `admin.credential.refreshToken()` methods now accept an `http.Agent`
9+
as an optional argument. If specified, the `http.Agent` will be used
10+
when calling Google backend servers to fetch OAuth2 access tokens.
311
- [added] `messaging.AndroidNotification`type now supports channel_id.
412

513
# v6.3.0

package-lock.json

Lines changed: 1117 additions & 1343 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/auth/credential.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import path = require('path');
2121

2222
import {AppErrorCodes, FirebaseAppError} from '../utils/error';
2323
import {HttpClient, HttpRequestConfig} from '../utils/api-request';
24-
24+
import {Agent} from 'http';
2525

2626
const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token';
2727
const GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com';
@@ -216,13 +216,16 @@ function requestAccessToken(client: HttpClient, request: HttpRequestConfig): Pro
216216
* Implementation of Credential that uses a service account certificate.
217217
*/
218218
export class CertCredential implements Credential {
219+
219220
private readonly certificate: Certificate;
220221
private readonly httpClient: HttpClient;
222+
private readonly httpAgent: Agent;
221223

222-
constructor(serviceAccountPathOrObject: string | object) {
224+
constructor(serviceAccountPathOrObject: string | object, httpAgent?: Agent) {
223225
this.certificate = (typeof serviceAccountPathOrObject === 'string') ?
224226
Certificate.fromPath(serviceAccountPathOrObject) : new Certificate(serviceAccountPathOrObject);
225227
this.httpClient = new HttpClient();
228+
this.httpAgent = httpAgent;
226229
}
227230

228231
public getAccessToken(): Promise<GoogleOAuthAccessToken> {
@@ -236,6 +239,7 @@ export class CertCredential implements Credential {
236239
'Content-Type': 'application/x-www-form-urlencoded',
237240
},
238241
data: postData,
242+
httpAgent: this.httpAgent,
239243
};
240244
return requestAccessToken(this.httpClient, request);
241245
}
@@ -278,13 +282,16 @@ export interface Credential {
278282
* Implementation of Credential that gets access tokens from refresh tokens.
279283
*/
280284
export class RefreshTokenCredential implements Credential {
285+
281286
private readonly refreshToken: RefreshToken;
282287
private readonly httpClient: HttpClient;
288+
private readonly httpAgent: Agent;
283289

284-
constructor(refreshTokenPathOrObject: string | object) {
290+
constructor(refreshTokenPathOrObject: string | object, httpAgent?: Agent) {
285291
this.refreshToken = (typeof refreshTokenPathOrObject === 'string') ?
286292
RefreshToken.fromPath(refreshTokenPathOrObject) : new RefreshToken(refreshTokenPathOrObject);
287293
this.httpClient = new HttpClient();
294+
this.httpAgent = httpAgent;
288295
}
289296

290297
public getAccessToken(): Promise<GoogleOAuthAccessToken> {
@@ -300,6 +307,7 @@ export class RefreshTokenCredential implements Credential {
300307
'Content-Type': 'application/x-www-form-urlencoded',
301308
},
302309
data: postData,
310+
httpAgent: this.httpAgent,
303311
};
304312
return requestAccessToken(this.httpClient, request);
305313
}
@@ -318,11 +326,17 @@ export class RefreshTokenCredential implements Credential {
318326
export class MetadataServiceCredential implements Credential {
319327

320328
private readonly httpClient = new HttpClient();
329+
private readonly httpAgent: Agent;
330+
331+
constructor(httpAgent?: Agent) {
332+
this.httpAgent = httpAgent;
333+
}
321334

322335
public getAccessToken(): Promise<GoogleOAuthAccessToken> {
323336
const request: HttpRequestConfig = {
324337
method: 'GET',
325338
url: `http://${GOOGLE_METADATA_SERVICE_HOST}${GOOGLE_METADATA_SERVICE_PATH}`,
339+
httpAgent: this.httpAgent,
326340
};
327341
return requestAccessToken(this.httpClient, request);
328342
}
@@ -340,21 +354,21 @@ export class MetadataServiceCredential implements Credential {
340354
export class ApplicationDefaultCredential implements Credential {
341355
private credential_: Credential;
342356

343-
constructor() {
357+
constructor(httpAgent?: Agent) {
344358
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
345359
const serviceAccount = Certificate.fromPath(process.env.GOOGLE_APPLICATION_CREDENTIALS);
346-
this.credential_ = new CertCredential(serviceAccount);
360+
this.credential_ = new CertCredential(serviceAccount, httpAgent);
347361
return;
348362
}
349363

350364
// It is OK to not have this file. If it is present, it must be valid.
351365
const refreshToken = RefreshToken.fromPath(GCLOUD_CREDENTIAL_PATH);
352366
if (refreshToken) {
353-
this.credential_ = new RefreshTokenCredential(refreshToken);
367+
this.credential_ = new RefreshTokenCredential(refreshToken, httpAgent);
354368
return;
355369
}
356370

357-
this.credential_ = new MetadataServiceCredential();
371+
this.credential_ = new MetadataServiceCredential(httpAgent);
358372
}
359373

360374
public getAccessToken(): Promise<GoogleOAuthAccessToken> {

src/firebase-app.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {FirestoreService} from './firestore/firestore';
3131
import {InstanceId} from './instance-id/instance-id';
3232
import {ProjectManagement} from './project-management/project-management';
3333

34+
import {Agent} from 'http';
35+
3436
/**
3537
* Type representing a callback which is called every time an app lifecycle event occurs.
3638
*/
@@ -46,6 +48,7 @@ export interface FirebaseAppOptions {
4648
serviceAccountId?: string;
4749
storageBucket?: string;
4850
projectId?: string;
51+
httpAgent?: Agent;
4952
}
5053

5154
/**

src/firebase-namespace.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import fs = require('fs');
18+
import {Agent} from 'http';
1819
import {deepExtend} from './utils/deep-copy';
1920
import {AppErrorCodes, FirebaseAppError} from './utils/error';
2021
import {AppHook, FirebaseApp, FirebaseAppOptions} from './firebase-app';
@@ -270,25 +271,26 @@ export class FirebaseNamespaceInternals {
270271

271272

272273
const firebaseCredential = {
273-
cert: (serviceAccountPathOrObject: string | object): Credential => {
274+
cert: (serviceAccountPathOrObject: string | object, httpAgent?: Agent): Credential => {
274275
const stringifiedServiceAccount = JSON.stringify(serviceAccountPathOrObject);
275276
if (!(stringifiedServiceAccount in globalCertCreds)) {
276-
globalCertCreds[stringifiedServiceAccount] = new CertCredential(serviceAccountPathOrObject);
277+
globalCertCreds[stringifiedServiceAccount] = new CertCredential(serviceAccountPathOrObject, httpAgent);
277278
}
278279
return globalCertCreds[stringifiedServiceAccount];
279280
},
280281

281-
refreshToken: (refreshTokenPathOrObject: string | object): Credential => {
282+
refreshToken: (refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential => {
282283
const stringifiedRefreshToken = JSON.stringify(refreshTokenPathOrObject);
283284
if (!(stringifiedRefreshToken in globalRefreshTokenCreds)) {
284-
globalRefreshTokenCreds[stringifiedRefreshToken] = new RefreshTokenCredential(refreshTokenPathOrObject);
285+
globalRefreshTokenCreds[stringifiedRefreshToken] = new RefreshTokenCredential(
286+
refreshTokenPathOrObject, httpAgent);
285287
}
286288
return globalRefreshTokenCreds[stringifiedRefreshToken];
287289
},
288290

289-
applicationDefault: (): Credential => {
291+
applicationDefault: (httpAgent?: Agent): Credential => {
290292
if (typeof globalAppDefaultCred === 'undefined') {
291-
globalAppDefaultCred = new ApplicationDefaultCredential();
293+
globalAppDefaultCred = new ApplicationDefaultCredential(httpAgent);
292294
}
293295
return globalAppDefaultCred;
294296
},

src/index.d.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import {Bucket} from '@google-cloud/storage';
1818
import * as _firestore from '@google-cloud/firestore';
19+
import {Agent} from 'http';
1920

2021
declare namespace admin {
2122
interface FirebaseError {
@@ -49,6 +50,7 @@ declare namespace admin {
4950
serviceAccountId?: string;
5051
storageBucket?: string;
5152
projectId?: string;
53+
httpAgent?: Agent;
5254
}
5355

5456
var SDK_VERSION: string;
@@ -269,9 +271,9 @@ declare namespace admin.credential {
269271
getAccessToken(): Promise<admin.GoogleOAuthAccessToken>;
270272
}
271273

272-
function applicationDefault(): admin.credential.Credential;
273-
function cert(serviceAccountPathOrObject: string|admin.ServiceAccount): admin.credential.Credential;
274-
function refreshToken(refreshTokenPathOrObject: string|Object): admin.credential.Credential;
274+
function applicationDefault(httpAgent?: Agent): admin.credential.Credential;
275+
function cert(serviceAccountPathOrObject: string|admin.ServiceAccount, httpAgent?: Agent): admin.credential.Credential;
276+
function refreshToken(refreshTokenPathOrObject: string|Object, httpAgent?: Agent): admin.credential.Credential;
275277
}
276278

277279
declare namespace admin.database {

src/utils/api-request.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface HttpRequestConfig {
4242
data?: string | object | Buffer;
4343
/** Connect and read timeout (in milliseconds) for the outgoing request. */
4444
timeout?: number;
45+
httpAgent?: http.Agent;
4546
}
4647

4748
/**
@@ -228,11 +229,16 @@ function sendRequest(httpRequestConfig: HttpRequestConfig): Promise<LowLevelResp
228229
const parsed = url.parse(fullUrl);
229230
const protocol = parsed.protocol || 'https:';
230231
const isHttps = protocol === 'https:';
231-
const options = {
232+
let port: string = parsed.port;
233+
if (!port) {
234+
port = isHttps ? '443' : '80';
235+
}
236+
const options: https.RequestOptions = {
232237
hostname: parsed.hostname,
233-
port: parsed.port,
238+
port,
234239
path: parsed.path,
235240
method: config.method,
241+
agent: config.httpAgent,
236242
headers,
237243
};
238244
const transport: any = isHttps ? https : http;
@@ -360,6 +366,10 @@ export class AuthorizedHttpClient extends HttpClient {
360366
requestCopy.headers = requestCopy.headers || {};
361367
const authHeader = 'Authorization';
362368
requestCopy.headers[authHeader] = `Bearer ${accessTokenObj.accessToken}`;
369+
370+
if (!requestCopy.httpAgent && this.app.options.httpAgent) {
371+
requestCopy.httpAgent = this.app.options.httpAgent;
372+
}
363373
return super.send(requestCopy);
364374
});
365375
}

test/resources/mocks.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ export function mockCredentialApp(): FirebaseApp {
105105
}
106106

107107
export function appWithOptions(options: FirebaseAppOptions): FirebaseApp {
108-
return new FirebaseApp(options, appName, new FirebaseNamespace().INTERNAL);
108+
const namespaceInternals = new FirebaseNamespace().INTERNAL;
109+
namespaceInternals.removeApp = _.noop;
110+
return new FirebaseApp(options, appName, namespaceInternals);
109111
}
110112

111113
export function appReturningNullAccessToken(): FirebaseApp {

test/unit/auth/credential.spec.ts

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
MetadataServiceCredential, RefreshTokenCredential,
3636
} from '../../../src/auth/credential';
3737
import { HttpClient } from '../../../src/utils/api-request';
38+
import {Agent} from 'https';
3839

3940
chai.should();
4041
chai.use(sinonChai);
@@ -45,6 +46,12 @@ const expect = chai.expect;
4546
let TEST_GCLOUD_CREDENTIALS: any;
4647
const GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json';
4748
const GCLOUD_CREDENTIAL_PATH = path.resolve(process.env.HOME, '.config', GCLOUD_CREDENTIAL_SUFFIX);
49+
const MOCK_REFRESH_TOKEN_CONFIG = {
50+
client_id: 'test_client_id',
51+
client_secret: 'test_client_secret',
52+
type: 'authorized_user',
53+
refresh_token: 'test_token',
54+
};
4855
try {
4956
TEST_GCLOUD_CREDENTIALS = JSON.parse(fs.readFileSync(GCLOUD_CREDENTIAL_PATH).toString());
5057
} catch (error) {
@@ -83,7 +90,6 @@ const FIVE_MINUTES_IN_SECONDS = 5 * 60;
8390

8491

8592
describe('Credential', () => {
86-
let mockedRequests: nock.Scope[] = [];
8793
let mockCertificateObject: any;
8894
let oldProcessEnv: NodeJS.ProcessEnv;
8995

@@ -97,8 +103,6 @@ describe('Credential', () => {
97103
});
98104

99105
afterEach(() => {
100-
_.forEach(mockedRequests, (mockedRequest) => mockedRequest.done());
101-
mockedRequests = [];
102106
process.env = oldProcessEnv;
103107
});
104108

@@ -280,11 +284,7 @@ describe('Credential', () => {
280284

281285
describe('RefreshTokenCredential', () => {
282286
it('should not return a certificate', () => {
283-
if (skipAndLogWarningIfNoGcloud()) {
284-
return;
285-
}
286-
287-
const c = new RefreshTokenCredential(TEST_GCLOUD_CREDENTIALS);
287+
const c = new RefreshTokenCredential(MOCK_REFRESH_TOKEN_CONFIG);
288288
expect(c.getCertificate()).to.be.null;
289289
});
290290

@@ -396,4 +396,62 @@ describe('Credential', () => {
396396
});
397397
});
398398
});
399+
400+
describe('HTTP Agent', () => {
401+
const expectedToken = utils.generateRandomAccessToken();
402+
let stub: sinon.SinonStub;
403+
404+
beforeEach(() => {
405+
stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({
406+
access_token: expectedToken,
407+
token_type: 'Bearer',
408+
expires_in: 60 * 60,
409+
}));
410+
});
411+
412+
afterEach(() => {
413+
stub.restore();
414+
});
415+
416+
it('CertCredential should use the provided HTTP Agent', () => {
417+
const agent = new Agent();
418+
const c = new CertCredential(mockCertificateObject, agent);
419+
return c.getAccessToken().then((token) => {
420+
expect(token.access_token).to.equal(expectedToken);
421+
expect(stub).to.have.been.calledOnce;
422+
expect(stub.args[0][0].httpAgent).to.equal(agent);
423+
});
424+
});
425+
426+
it('RefreshTokenCredential should use the provided HTTP Agent', () => {
427+
const agent = new Agent();
428+
const c = new RefreshTokenCredential(MOCK_REFRESH_TOKEN_CONFIG, agent);
429+
return c.getAccessToken().then((token) => {
430+
expect(token.access_token).to.equal(expectedToken);
431+
expect(stub).to.have.been.calledOnce;
432+
expect(stub.args[0][0].httpAgent).to.equal(agent);
433+
});
434+
});
435+
436+
it('MetadataServiceCredential should use the provided HTTP Agent', () => {
437+
const agent = new Agent();
438+
const c = new MetadataServiceCredential(agent);
439+
return c.getAccessToken().then((token) => {
440+
expect(token.access_token).to.equal(expectedToken);
441+
expect(stub).to.have.been.calledOnce;
442+
expect(stub.args[0][0].httpAgent).to.equal(agent);
443+
});
444+
});
445+
446+
it('ApplicationDefaultCredential should use the provided HTTP Agent', () => {
447+
process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json');
448+
const agent = new Agent();
449+
const c = new ApplicationDefaultCredential(agent);
450+
return c.getAccessToken().then((token) => {
451+
expect(token.access_token).to.equal(expectedToken);
452+
expect(stub).to.have.been.calledOnce;
453+
expect(stub.args[0][0].httpAgent).to.equal(agent);
454+
});
455+
});
456+
});
399457
});

0 commit comments

Comments
 (0)