Skip to content

Commit ed98926

Browse files
authored
Async API to determine project ID (#715)
1 parent bba61e1 commit ed98926

File tree

6 files changed

+144
-64
lines changed

6 files changed

+144
-64
lines changed

src/auth/auth.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,8 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> {
105105
constructor(app: FirebaseApp, protected readonly authRequestHandler: T) {
106106
const cryptoSigner = cryptoSignerFromApp(app);
107107
this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner);
108-
109-
const projectId = utils.getProjectId(app);
110-
const httpAgent = app.options.httpAgent;
111-
this.sessionCookieVerifier = createSessionCookieVerifier(projectId, httpAgent);
112-
this.idTokenVerifier = createIdTokenVerifier(projectId, httpAgent);
108+
this.sessionCookieVerifier = createSessionCookieVerifier(app);
109+
this.idTokenVerifier = createIdTokenVerifier(app);
113110
}
114111

115112
/**

src/auth/token-verifier.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616

1717
import {AuthClientErrorCode, FirebaseAuthError, ErrorInfo} from '../utils/error';
1818

19+
import * as util from '../utils/index';
1920
import * as validator from '../utils/validator';
2021
import * as jwt from 'jsonwebtoken';
2122
import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request';
2223
import { DecodedIdToken } from './auth';
23-
import { Agent } from 'http';
24+
import { FirebaseApp } from '../firebase-app';
2425

2526
// Audience to use for Firebase Auth Custom tokens
2627
const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';
@@ -75,9 +76,8 @@ export class FirebaseTokenVerifier {
7576
private readonly shortNameArticle: string;
7677

7778
constructor(private clientCertUrl: string, private algorithm: string,
78-
private issuer: string, private projectId: string | null,
79-
private tokenInfo: FirebaseTokenInfo,
80-
private readonly httpAgent?: Agent) {
79+
private issuer: string, private tokenInfo: FirebaseTokenInfo,
80+
private readonly app: FirebaseApp) {
8181
if (!validator.isURL(clientCertUrl)) {
8282
throw new FirebaseAuthError(
8383
AuthClientErrorCode.INVALID_ARGUMENT,
@@ -144,7 +144,14 @@ export class FirebaseTokenVerifier {
144144
);
145145
}
146146

147-
if (!validator.isNonEmptyString(this.projectId)) {
147+
return util.findProjectId(this.app)
148+
.then((projectId) => {
149+
return this.verifyJWTWithProjectId(jwtToken, projectId);
150+
});
151+
}
152+
153+
private verifyJWTWithProjectId(jwtToken: string, projectId: string | null): Promise<DecodedIdToken> {
154+
if (!validator.isNonEmptyString(projectId)) {
148155
throw new FirebaseAuthError(
149156
AuthClientErrorCode.INVALID_CREDENTIAL,
150157
`Must initialize app with a cert credential or set your Firebase project ID as the ` +
@@ -186,13 +193,13 @@ export class FirebaseTokenVerifier {
186193
} else if (header.alg !== this.algorithm) {
187194
errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + `" but got ` +
188195
`"` + header.alg + `".` + verifyJwtTokenDocsMessage;
189-
} else if (payload.aud !== this.projectId) {
196+
} else if (payload.aud !== projectId) {
190197
errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` +
191-
this.projectId + `" but got "` + payload.aud + `".` + projectIdMatchMessage +
198+
projectId + `" but got "` + payload.aud + `".` + projectIdMatchMessage +
192199
verifyJwtTokenDocsMessage;
193-
} else if (payload.iss !== this.issuer + this.projectId) {
200+
} else if (payload.iss !== this.issuer + projectId) {
194201
errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` +
195-
`"${this.issuer}"` + this.projectId + `" but got "` +
202+
`"${this.issuer}"` + projectId + `" but got "` +
196203
payload.iss + `".` + projectIdMatchMessage + verifyJwtTokenDocsMessage;
197204
} else if (typeof payload.sub !== 'string') {
198205
errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage;
@@ -284,7 +291,7 @@ export class FirebaseTokenVerifier {
284291
const request: HttpRequestConfig = {
285292
method: 'GET',
286293
url: this.clientCertUrl,
287-
httpAgent: this.httpAgent,
294+
httpAgent: this.app.options.httpAgent,
288295
};
289296
return client.send(request).then((resp) => {
290297
if (!resp.isJson() || resp.data.error) {
@@ -327,35 +334,31 @@ export class FirebaseTokenVerifier {
327334
/**
328335
* Creates a new FirebaseTokenVerifier to verify Firebase ID tokens.
329336
*
330-
* @param {string} projectId Project ID string.
331-
* @param {Agent} httpAgent Optional HTTP agent.
337+
* @param {FirebaseApp} app Firebase app instance.
332338
* @return {FirebaseTokenVerifier}
333339
*/
334-
export function createIdTokenVerifier(projectId: string | null, httpAgent?: Agent): FirebaseTokenVerifier {
340+
export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier {
335341
return new FirebaseTokenVerifier(
336-
CLIENT_CERT_URL,
337-
ALGORITHM_RS256,
338-
'https://securetoken.google.com/',
339-
projectId,
340-
ID_TOKEN_INFO,
341-
httpAgent,
342+
CLIENT_CERT_URL,
343+
ALGORITHM_RS256,
344+
'https://securetoken.google.com/',
345+
ID_TOKEN_INFO,
346+
app,
342347
);
343348
}
344349

345350
/**
346351
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
347352
*
348-
* @param {string} projectId Project ID string.
349-
* @param {Agent} httpAgent Optional HTTP agent.
353+
* @param {FirebaseApp} app Firebase app instance.
350354
* @return {FirebaseTokenVerifier}
351355
*/
352-
export function createSessionCookieVerifier(projectId: string | null, httpAgent?: Agent): FirebaseTokenVerifier {
356+
export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier {
353357
return new FirebaseTokenVerifier(
354358
SESSION_COOKIE_CERT_URL,
355359
ALGORITHM_RS256,
356360
'https://session.firebase.google.com/',
357-
projectId,
358361
SESSION_COOKIE_INFO,
359-
httpAgent,
362+
app,
360363
);
361364
}

src/utils/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ export function getProjectId(app: FirebaseApp): string | null {
8181
return null;
8282
}
8383

84+
/**
85+
* Determines the Google Cloud project ID associated with a Firebase app by examining
86+
* the Firebase app options, credentials and the local environment in that order. This
87+
* is an async wrapper of the getProjectId method. This enables us to migrate the rest
88+
* of the SDK into asynchronously determining the current project ID. See b/143090254.
89+
*
90+
* @param {FirebaseApp} app A Firebase app to get the project ID from.
91+
*
92+
* @return {Promise<string | null>} A project ID string or null.
93+
*/
94+
export function findProjectId(app: FirebaseApp): Promise<string | null> {
95+
return Promise.resolve(getProjectId(app));
96+
}
97+
8498
/**
8599
* Encodes data using web-safe-base64.
86100
*

test/unit/auth/auth.spec.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -387,22 +387,20 @@ AUTH_CONFIGS.forEach((testConfig) => {
387387
}
388388
});
389389

390-
it('verifyIdToken() should throw when project ID is not specified', () => {
390+
it('verifyIdToken() should reject when project ID is not specified', () => {
391391
const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp());
392392
const expected = 'Must initialize app with a cert credential or set your Firebase project ID ' +
393393
'as the GOOGLE_CLOUD_PROJECT environment variable to call verifyIdToken().';
394-
expect(() => {
395-
mockCredentialAuth.verifyIdToken(mocks.generateIdToken());
396-
}).to.throw(expected);
394+
return mockCredentialAuth.verifyIdToken(mocks.generateIdToken())
395+
.should.eventually.be.rejectedWith(expected);
397396
});
398397

399-
it('verifySessionCookie() should throw when project ID is not specified', () => {
398+
it('verifySessionCookie() should reject when project ID is not specified', () => {
400399
const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp());
401400
const expected = 'Must initialize app with a cert credential or set your Firebase project ID ' +
402401
'as the GOOGLE_CLOUD_PROJECT environment variable to call verifySessionCookie().';
403-
expect(() => {
404-
mockCredentialAuth.verifySessionCookie(mocks.generateSessionCookie());
405-
}).to.throw(expected);
402+
return mockCredentialAuth.verifySessionCookie(mocks.generateSessionCookie())
403+
.should.eventually.be.rejectedWith(expected);
406404
});
407405

408406
describe('verifyIdToken()', () => {

0 commit comments

Comments
 (0)