Skip to content

Commit b410895

Browse files
authored
Using the new HttpClient API in Credentials (#318)
* Using new HTTP client in credentials * Re-enabled old refresh token test * Fixing unit tests by getting the access token before all tests * Renamed member variables
1 parent f718023 commit b410895

File tree

6 files changed

+70
-86
lines changed

6 files changed

+70
-86
lines changed

src/auth/credential.ts

Lines changed: 52 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ import os = require('os');
2020
import path = require('path');
2121

2222
import {AppErrorCodes, FirebaseAppError} from '../utils/error';
23+
import {HttpClient, HttpRequestConfig} from '../utils/api-request';
2324

2425

2526
const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token';
2627
const GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com';
2728
const GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token';
28-
const GOOGLE_AUTH_TOKEN_PORT = 443;
2929

3030
// NOTE: the Google Metadata Service uses HTTP over a vlan
3131
const GOOGLE_METADATA_SERVICE_HOST = 'metadata.google.internal';
@@ -46,7 +46,6 @@ const GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json';
4646
const GCLOUD_CREDENTIAL_PATH = configDir && path.resolve(configDir, GCLOUD_CREDENTIAL_SUFFIX);
4747

4848
const REFRESH_TOKEN_HOST = 'www.googleapis.com';
49-
const REFRESH_TOKEN_PORT = 443;
5049
const REFRESH_TOKEN_PATH = '/oauth2/v4/token';
5150

5251
const ONE_HOUR_IN_SECONDS = 60 * 60;
@@ -186,79 +185,63 @@ export interface GoogleOAuthAccessToken {
186185
}
187186

188187
/**
189-
* A wrapper around the http and https request libraries to simplify & promisify JSON requests.
190-
* TODO(inlined): Create a type for "transit".
188+
* Obtain a new OAuth2 token by making a remote service call.
191189
*/
192-
function requestAccessToken(transit, options: object, data?: any): Promise<GoogleOAuthAccessToken> {
193-
return new Promise((resolve, reject) => {
194-
const req = transit.request(options, (res) => {
195-
const buffers: Buffer[] = [];
196-
res.on('data', (buffer) => buffers.push(buffer));
197-
res.on('end', () => {
198-
try {
199-
const json = JSON.parse(Buffer.concat(buffers).toString());
200-
if (json.error) {
201-
let errorMessage = 'Error fetching access token: ' + json.error;
202-
if (json.error_description) {
203-
errorMessage += ' (' + json.error_description + ')';
204-
}
205-
reject(new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage));
206-
} else if (!json.access_token || !json.expires_in) {
207-
reject(new FirebaseAppError(
208-
AppErrorCodes.INVALID_CREDENTIAL,
209-
`Unexpected response while fetching access token: ${ JSON.stringify(json) }`,
210-
));
211-
} else {
212-
resolve(json);
213-
}
214-
} catch (err) {
215-
reject(new FirebaseAppError(
216-
AppErrorCodes.INVALID_CREDENTIAL,
217-
`Failed to parse access token response: ${err.toString()}`,
218-
));
219-
}
220-
});
221-
});
222-
req.on('error', reject);
223-
if (data) {
224-
req.write(data);
190+
function requestAccessToken(client: HttpClient, request: HttpRequestConfig): Promise<GoogleOAuthAccessToken> {
191+
return client.send(request).then((resp) => {
192+
const json = resp.data;
193+
if (json.error) {
194+
let errorMessage = 'Error fetching access token: ' + json.error;
195+
if (json.error_description) {
196+
errorMessage += ' (' + json.error_description + ')';
197+
}
198+
throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage);
199+
} else if (!json.access_token || !json.expires_in) {
200+
throw new FirebaseAppError(
201+
AppErrorCodes.INVALID_CREDENTIAL,
202+
`Unexpected response while fetching access token: ${ JSON.stringify(json) }`,
203+
);
204+
} else {
205+
return json;
225206
}
226-
req.end();
207+
}).catch((err) => {
208+
throw new FirebaseAppError(
209+
AppErrorCodes.INVALID_CREDENTIAL,
210+
`Failed to parse access token response: ${err.toString()}`,
211+
);
227212
});
228213
}
229214

230215
/**
231216
* Implementation of Credential that uses a service account certificate.
232217
*/
233218
export class CertCredential implements Credential {
234-
private certificate_: Certificate;
219+
private readonly certificate: Certificate;
220+
private readonly httpClient: HttpClient;
235221

236222
constructor(serviceAccountPathOrObject: string | object) {
237-
this.certificate_ = (typeof serviceAccountPathOrObject === 'string') ?
223+
this.certificate = (typeof serviceAccountPathOrObject === 'string') ?
238224
Certificate.fromPath(serviceAccountPathOrObject) : new Certificate(serviceAccountPathOrObject);
225+
this.httpClient = new HttpClient();
239226
}
240227

241228
public getAccessToken(): Promise<GoogleOAuthAccessToken> {
242229
const token = this.createAuthJwt_();
243230
const postData = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3A' +
244-
'grant-type%3Ajwt-bearer&assertion=' +
245-
token;
246-
const options = {
231+
'grant-type%3Ajwt-bearer&assertion=' + token;
232+
const request: HttpRequestConfig = {
247233
method: 'POST',
248-
host: GOOGLE_AUTH_TOKEN_HOST,
249-
port: GOOGLE_AUTH_TOKEN_PORT,
250-
path: GOOGLE_AUTH_TOKEN_PATH,
234+
url: `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`,
251235
headers: {
252236
'Content-Type': 'application/x-www-form-urlencoded',
253-
'Content-Length': postData.length,
254237
},
238+
data: postData,
255239
};
256-
const https = require('https');
257-
return requestAccessToken(https, options, postData);
240+
return requestAccessToken(this.httpClient, request);
258241
}
259242

260243
public getCertificate(): Certificate {
261-
return this.certificate_;
244+
return this.certificate;
262245
}
263246

264247
private createAuthJwt_(): string {
@@ -274,10 +257,10 @@ export class CertCredential implements Credential {
274257

275258
const jwt = require('jsonwebtoken');
276259
// This method is actually synchronous so we can capture and return the buffer.
277-
return jwt.sign(claims, this.certificate_.privateKey, {
260+
return jwt.sign(claims, this.certificate.privateKey, {
278261
audience: GOOGLE_TOKEN_AUDIENCE,
279262
expiresIn: ONE_HOUR_IN_SECONDS,
280-
issuer: this.certificate_.clientEmail,
263+
issuer: this.certificate.clientEmail,
281264
algorithm: JWT_ALGORITHM,
282265
});
283266
}
@@ -295,32 +278,30 @@ export interface Credential {
295278
* Implementation of Credential that gets access tokens from refresh tokens.
296279
*/
297280
export class RefreshTokenCredential implements Credential {
298-
private refreshToken_: RefreshToken;
281+
private readonly refreshToken: RefreshToken;
282+
private readonly httpClient: HttpClient;
299283

300284
constructor(refreshTokenPathOrObject: string | object) {
301-
this.refreshToken_ = (typeof refreshTokenPathOrObject === 'string') ?
285+
this.refreshToken = (typeof refreshTokenPathOrObject === 'string') ?
302286
RefreshToken.fromPath(refreshTokenPathOrObject) : new RefreshToken(refreshTokenPathOrObject);
287+
this.httpClient = new HttpClient();
303288
}
304289

305290
public getAccessToken(): Promise<GoogleOAuthAccessToken> {
306291
const postData =
307-
'client_id=' + this.refreshToken_.clientId + '&' +
308-
'client_secret=' + this.refreshToken_.clientSecret + '&' +
309-
'refresh_token=' + this.refreshToken_.refreshToken + '&' +
292+
'client_id=' + this.refreshToken.clientId + '&' +
293+
'client_secret=' + this.refreshToken.clientSecret + '&' +
294+
'refresh_token=' + this.refreshToken.refreshToken + '&' +
310295
'grant_type=refresh_token';
311-
312-
const options = {
296+
const request: HttpRequestConfig = {
313297
method: 'POST',
314-
host: REFRESH_TOKEN_HOST,
315-
port: REFRESH_TOKEN_PORT,
316-
path: REFRESH_TOKEN_PATH,
298+
url: `https://${REFRESH_TOKEN_HOST}${REFRESH_TOKEN_PATH}`,
317299
headers: {
318300
'Content-Type': 'application/x-www-form-urlencoded',
319-
'Content-Length': postData.length,
320301
},
302+
data: postData,
321303
};
322-
const https = require('https');
323-
return requestAccessToken(https, options, postData);
304+
return requestAccessToken(this.httpClient, request);
324305
}
325306

326307
public getCertificate(): Certificate {
@@ -335,17 +316,15 @@ export class RefreshTokenCredential implements Credential {
335316
* of an App Engine instance or Google Compute Engine machine.
336317
*/
337318
export class MetadataServiceCredential implements Credential {
319+
320+
private readonly httpClient = new HttpClient();
321+
338322
public getAccessToken(): Promise<GoogleOAuthAccessToken> {
339-
const options = {
323+
const request: HttpRequestConfig = {
340324
method: 'GET',
341-
host: GOOGLE_METADATA_SERVICE_HOST,
342-
path: GOOGLE_METADATA_SERVICE_PATH,
343-
headers: {
344-
'Content-Length': 0,
345-
},
325+
url: `http://${GOOGLE_METADATA_SERVICE_HOST}${GOOGLE_METADATA_SERVICE_PATH}`,
346326
};
347-
const http = require('http');
348-
return requestAccessToken(http, options);
327+
return requestAccessToken(this.httpClient, request);
349328
}
350329

351330
public getCertificate(): Certificate {

test/unit/auth/auth-api-request.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,7 @@ describe('FirebaseAuthRequestHandler', () => {
751751

752752
beforeEach(() => {
753753
mockApp = mocks.app();
754+
return mockApp.INTERNAL.getToken();
754755
});
755756

756757
afterEach(() => {

test/unit/auth/credential.spec.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
ApplicationDefaultCredential, CertCredential, Certificate, GoogleOAuthAccessToken,
3737
MetadataServiceCredential, RefreshToken, RefreshTokenCredential,
3838
} from '../../../src/auth/credential';
39+
import { HttpClient } from '../../../src/utils/api-request';
3940

4041
chai.should();
4142
chai.use(sinonChai);
@@ -304,7 +305,7 @@ describe('Credential', () => {
304305

305306
describe('MetadataServiceCredential', () => {
306307
let httpStub;
307-
before(() => httpStub = sinon.stub(http, 'request'));
308+
before(() => httpStub = sinon.stub(HttpClient.prototype, 'send'));
308309
after(() => httpStub.restore());
309310

310311
it('should not return a certificate', () => {
@@ -317,14 +318,8 @@ describe('Credential', () => {
317318
access_token: 'anAccessToken',
318319
expires_in: 42,
319320
};
320-
const response = new stream.PassThrough();
321-
response.write(JSON.stringify(expected));
322-
response.end();
323-
324-
const request = new stream.PassThrough();
325-
326-
httpStub.callsArgWith(1, response)
327-
.returns(request);
321+
const response = utils.responseFrom(expected);
322+
httpStub.resolves(response);
328323

329324
const c = new MetadataServiceCredential();
330325
return c.getAccessToken().then((token) => {

test/unit/auth/token-generator.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ describe('CryptoSigner', () => {
116116

117117
beforeEach(() => {
118118
mockApp = mocks.app();
119+
return mockApp.INTERNAL.getToken();
119120
});
120121

121122
afterEach(() => {

test/unit/firebase.spec.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,17 @@ describe('Firebase', () => {
143143
}).should.eventually.have.keys(['accessToken', 'expirationTime']);
144144
});
145145

146-
// TODO(jwenger): mock out the refresh token endpoint so this test will work
147-
xit('should initialize SDK given a refresh token credential', () => {
148-
nock.recorder.rec();
146+
it('should initialize SDK given a refresh token credential', () => {
147+
const scope = nock('https://www.googleapis.com')
148+
.post('/oauth2/v4/token')
149+
.reply(200, {
150+
access_token: 'token',
151+
token_type: 'Bearer',
152+
expires_in: 60 * 60,
153+
}, {
154+
'cache-control': 'no-cache, no-store, max-age=0, must-revalidate',
155+
});
156+
mockedRequests.push(scope);
149157
firebaseAdmin.initializeApp({
150158
credential: firebaseAdmin.credential.refreshToken(mocks.refreshToken),
151159
});

test/unit/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function mockFetchAccessTokenRequests(
5050
token: string = generateRandomAccessToken(),
5151
expiresIn: number = 60 * 60,
5252
): nock.Scope {
53-
return nock('https://accounts.google.com:443')
53+
return nock('https://accounts.google.com')
5454
.persist()
5555
.post('/o/oauth2/token')
5656
.reply(200, {

0 commit comments

Comments
 (0)