Skip to content

Commit a18dc71

Browse files
authored
Using async project ID discovery API in Auth and FCM (#724)
* Using async project ID discovery API in Auth and FCM * Fixed indentation
1 parent a9249a6 commit a18dc71

File tree

4 files changed

+117
-72
lines changed

4 files changed

+117
-72
lines changed

src/auth/auth-api-request.ts

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ const MAX_LIST_TENANT_PAGE_SIZE = 1000;
8888

8989
/** Defines a base utility to help with resource URL construction. */
9090
class AuthResourceUrlBuilder {
91+
9192
protected urlFormat: string;
93+
private projectId: string;
9294

9395
/**
9496
* The resource URL builder constructor.
@@ -97,7 +99,7 @@ class AuthResourceUrlBuilder {
9799
* @param {string} version The endpoint API version.
98100
* @constructor
99101
*/
100-
constructor(protected projectId: string | null, protected version: string = 'v1') {
102+
constructor(protected app: FirebaseApp, protected version: string = 'v1') {
101103
this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT;
102104
}
103105

@@ -107,17 +109,41 @@ class AuthResourceUrlBuilder {
107109
* @param {string=} api The backend API name.
108110
* @param {object=} params The optional additional parameters to substitute in the
109111
* URL path.
110-
* @return {string} The corresponding resource URL.
112+
* @return {Promise<string>} The corresponding resource URL.
111113
*/
112-
public getUrl(api?: string, params?: object): string {
113-
const baseParams = {
114-
version: this.version,
115-
projectId: this.projectId,
116-
api: api || '',
117-
};
118-
const baseUrl = utils.formatString(this.urlFormat, baseParams);
119-
// Substitute additional api related parameters.
120-
return utils.formatString(baseUrl, params || {});
114+
public getUrl(api?: string, params?: object): Promise<string> {
115+
return this.getProjectId()
116+
.then((projectId) => {
117+
const baseParams = {
118+
version: this.version,
119+
projectId,
120+
api: api || '',
121+
};
122+
const baseUrl = utils.formatString(this.urlFormat, baseParams);
123+
// Substitute additional api related parameters.
124+
return utils.formatString(baseUrl, params || {});
125+
});
126+
}
127+
128+
private getProjectId(): Promise<string> {
129+
if (this.projectId) {
130+
return Promise.resolve(this.projectId);
131+
}
132+
133+
return utils.findProjectId(this.app)
134+
.then((projectId) => {
135+
if (!validator.isNonEmptyString(projectId)) {
136+
throw new FirebaseAuthError(
137+
AuthClientErrorCode.INVALID_CREDENTIAL,
138+
'Failed to determine project ID for Auth. Initialize the '
139+
+ 'SDK with service account credentials or set project ID as an app option. '
140+
+ 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
141+
);
142+
}
143+
144+
this.projectId = projectId;
145+
return projectId;
146+
});
121147
}
122148
}
123149

@@ -132,8 +158,8 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder {
132158
* @param {string} tenantId The tenant ID.
133159
* @constructor
134160
*/
135-
constructor(protected projectId: string | null, protected version: string, protected tenantId: string) {
136-
super(projectId, version);
161+
constructor(protected app: FirebaseApp, protected version: string, protected tenantId: string) {
162+
super(app, version);
137163
this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT;
138164
}
139165

@@ -143,10 +169,13 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder {
143169
* @param {string=} api The backend API name.
144170
* @param {object=} params The optional additional parameters to substitute in the
145171
* URL path.
146-
* @return {string} The corresponding resource URL.
172+
* @return {Promise<string>} The corresponding resource URL.
147173
*/
148-
public getUrl(api?: string, params?: object) {
149-
return utils.formatString(super.getUrl(api, params), {tenantId: this.tenantId});
174+
public getUrl(api?: string, params?: object): Promise<string> {
175+
return super.getUrl(api, params)
176+
.then((url) => {
177+
return utils.formatString(url, {tenantId: this.tenantId});
178+
});
150179
}
151180
}
152181

@@ -683,7 +712,7 @@ const LIST_INBOUND_SAML_CONFIGS = new ApiSettings('/inboundSamlConfigs', 'GET')
683712
* Class that provides the mechanism to send requests to the Firebase Auth backend endpoints.
684713
*/
685714
export abstract class AbstractAuthRequestHandler {
686-
protected readonly projectId: string | null;
715+
687716
protected readonly httpClient: AuthorizedHttpClient;
688717
private authUrlBuilder: AuthResourceUrlBuilder;
689718
private projectConfigUrlBuilder: AuthResourceUrlBuilder;
@@ -700,17 +729,14 @@ export abstract class AbstractAuthRequestHandler {
700729
* @param {FirebaseApp} app The app used to fetch access tokens to sign API requests.
701730
* @constructor
702731
*/
703-
constructor(app: FirebaseApp) {
732+
constructor(protected readonly app: FirebaseApp) {
704733
if (typeof app !== 'object' || app === null || !('options' in app)) {
705734
throw new FirebaseAuthError(
706735
AuthClientErrorCode.INVALID_ARGUMENT,
707736
'First argument passed to admin.auth() must be a valid Firebase app instance.',
708737
);
709738
}
710739

711-
// TODO(rsgowman): Trace utils.getProjectId() throughout and figure out where a null return
712-
// value will cause troubles. (Such as AuthResourceUrlBuilder::getUrl()).
713-
this.projectId = utils.getProjectId(app);
714740
this.httpClient = new AuthorizedHttpClient(app);
715741
}
716742

@@ -1357,15 +1383,15 @@ export abstract class AbstractAuthRequestHandler {
13571383
protected invokeRequestHandler(
13581384
urlBuilder: AuthResourceUrlBuilder, apiSettings: ApiSettings,
13591385
requestData: object, additionalResourceParams?: object): Promise<object> {
1360-
return Promise.resolve()
1361-
.then(() => {
1386+
return urlBuilder.getUrl(apiSettings.getEndpoint(), additionalResourceParams)
1387+
.then((url) => {
13621388
// Validate request.
13631389
const requestValidator = apiSettings.getRequestValidator();
13641390
requestValidator(requestData);
13651391
// Process request.
13661392
const req: HttpRequestConfig = {
13671393
method: apiSettings.getHttpMethod(),
1368-
url: urlBuilder.getUrl(apiSettings.getEndpoint(), additionalResourceParams),
1394+
url,
13691395
headers: FIREBASE_AUTH_HEADER,
13701396
data: requestData,
13711397
timeout: FIREBASE_AUTH_TIMEOUT,
@@ -1512,21 +1538,21 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
15121538
*/
15131539
constructor(app: FirebaseApp) {
15141540
super(app);
1515-
this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(utils.getProjectId(app), 'v2');
1541+
this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(app, 'v2');
15161542
}
15171543

15181544
/**
15191545
* @return {AuthResourceUrlBuilder} A new Auth user management resource URL builder instance.
15201546
*/
15211547
protected newAuthUrlBuilder(): AuthResourceUrlBuilder {
1522-
return new AuthResourceUrlBuilder(this.projectId, 'v1');
1548+
return new AuthResourceUrlBuilder(this.app, 'v1');
15231549
}
15241550

15251551
/**
15261552
* @return {AuthResourceUrlBuilder} A new project config resource URL builder instance.
15271553
*/
15281554
protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder {
1529-
return new AuthResourceUrlBuilder(this.projectId, 'v2');
1555+
return new AuthResourceUrlBuilder(this.app, 'v2');
15301556
}
15311557

15321558
/**
@@ -1662,14 +1688,14 @@ export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler {
16621688
* @return {AuthResourceUrlBuilder} A new Auth user management resource URL builder instance.
16631689
*/
16641690
protected newAuthUrlBuilder(): AuthResourceUrlBuilder {
1665-
return new TenantAwareAuthResourceUrlBuilder(this.projectId, 'v1', this.tenantId);
1691+
return new TenantAwareAuthResourceUrlBuilder(this.app, 'v1', this.tenantId);
16661692
}
16671693

16681694
/**
16691695
* @return {AuthResourceUrlBuilder} A new project config resource URL builder instance.
16701696
*/
16711697
protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder {
1672-
return new TenantAwareAuthResourceUrlBuilder(this.projectId, 'v2', this.tenantId);
1698+
return new TenantAwareAuthResourceUrlBuilder(this.app, 'v2', this.tenantId);
16731699
}
16741700

16751701
/**

src/messaging/messaging.ts

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ export class Messaging implements FirebaseServiceInterface {
206206
public INTERNAL: MessagingInternals = new MessagingInternals();
207207

208208
private urlPath: string;
209-
private appInternal: FirebaseApp;
210-
private messagingRequestHandler: FirebaseMessagingRequestHandler;
209+
private readonly appInternal: FirebaseApp;
210+
private readonly messagingRequestHandler: FirebaseMessagingRequestHandler;
211211

212212
/**
213213
* @param {FirebaseApp} app The app for this Messaging service.
@@ -221,18 +221,6 @@ export class Messaging implements FirebaseServiceInterface {
221221
);
222222
}
223223

224-
const projectId: string | null = utils.getProjectId(app);
225-
if (!validator.isNonEmptyString(projectId)) {
226-
// Assert for an explicit project ID (either via AppOptions or the cert itself).
227-
throw new FirebaseMessagingError(
228-
MessagingClientErrorCode.INVALID_ARGUMENT,
229-
'Failed to determine project ID for Messaging. Initialize the '
230-
+ 'SDK with service account credentials or set project ID as an app option. '
231-
+ 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
232-
);
233-
}
234-
235-
this.urlPath = `/v1/projects/${projectId}/messages:send`;
236224
this.appInternal = app;
237225
this.messagingRequestHandler = new FirebaseMessagingRequestHandler(app);
238226
}
@@ -261,13 +249,13 @@ export class Messaging implements FirebaseServiceInterface {
261249
throw new FirebaseMessagingError(
262250
MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean');
263251
}
264-
return Promise.resolve()
265-
.then(() => {
252+
return this.getUrlPath()
253+
.then((urlPath) => {
266254
const request: {message: Message, validate_only?: boolean} = {message: copy};
267255
if (dryRun) {
268256
request.validate_only = true;
269257
}
270-
return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, this.urlPath, request);
258+
return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, urlPath, request);
271259
})
272260
.then((response) => {
273261
return (response as any).name;
@@ -312,18 +300,21 @@ export class Messaging implements FirebaseServiceInterface {
312300
MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean');
313301
}
314302

315-
const requests: SubRequest[] = copy.map((message) => {
316-
validateMessage(message);
317-
const request: {message: Message, validate_only?: boolean} = {message};
318-
if (dryRun) {
319-
request.validate_only = true;
320-
}
321-
return {
322-
url: `https://${FCM_SEND_HOST}${this.urlPath}`,
323-
body: request,
324-
};
325-
});
326-
return this.messagingRequestHandler.sendBatchRequest(requests);
303+
return this.getUrlPath()
304+
.then((urlPath) => {
305+
const requests: SubRequest[] = copy.map((message) => {
306+
validateMessage(message);
307+
const request: {message: Message, validate_only?: boolean} = {message};
308+
if (dryRun) {
309+
request.validate_only = true;
310+
}
311+
return {
312+
url: `https://${FCM_SEND_HOST}${urlPath}`,
313+
body: request,
314+
};
315+
});
316+
return this.messagingRequestHandler.sendBatchRequest(requests);
317+
});
327318
}
328319

329320
/**
@@ -645,6 +636,28 @@ export class Messaging implements FirebaseServiceInterface {
645636
);
646637
}
647638

639+
private getUrlPath(): Promise<string> {
640+
if (this.urlPath) {
641+
return Promise.resolve(this.urlPath);
642+
}
643+
644+
return utils.findProjectId(this.app)
645+
.then((projectId) => {
646+
if (!validator.isNonEmptyString(projectId)) {
647+
// Assert for an explicit project ID (either via AppOptions or the cert itself).
648+
throw new FirebaseMessagingError(
649+
MessagingClientErrorCode.INVALID_ARGUMENT,
650+
'Failed to determine project ID for Messaging. Initialize the '
651+
+ 'SDK with service account credentials or set project ID as an app option. '
652+
+ 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
653+
);
654+
}
655+
656+
this.urlPath = `/v1/projects/${projectId}/messages:send`;
657+
return this.urlPath;
658+
});
659+
}
660+
648661
/**
649662
* Helper method which sends and handles topic subscription management requests.
650663
*

test/unit/auth/auth.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,15 @@ AUTH_CONFIGS.forEach((testConfig) => {
292292
}).to.throw('First argument passed to admin.auth() must be a valid Firebase app instance.');
293293
});
294294

295+
it('should reject given no project ID', () => {
296+
const authWithoutProjectId = new Auth(mocks.mockCredentialApp());
297+
authWithoutProjectId.getUser('uid')
298+
.should.eventually.be.rejectedWith(
299+
'Failed to determine project ID for Auth. Initialize the SDK with service '
300+
+ 'account credentials or set project ID as an app option. Alternatively set the '
301+
+ 'GOOGLE_CLOUD_PROJECT environment variable.');
302+
});
303+
295304
it('should not throw given a valid app', () => {
296305
expect(() => {
297306
return new Auth(mockApp);
@@ -1626,8 +1635,6 @@ AUTH_CONFIGS.forEach((testConfig) => {
16261635
})
16271636
.catch((error) => {
16281637
expect(error).to.have.property('code', 'auth/invalid-page-token');
1629-
expect(validator.isNonEmptyString)
1630-
.to.have.been.calledOnce.and.calledWith(invalidToken);
16311638
});
16321639
});
16331640

@@ -1968,7 +1975,6 @@ AUTH_CONFIGS.forEach((testConfig) => {
19681975
})
19691976
.catch((error) => {
19701977
expect(error).to.have.property('code', 'auth/invalid-id-token');
1971-
expect(validator.isNonEmptyString).to.have.been.calledOnce.and.calledWith(invalidIdToken);
19721978
});
19731979
});
19741980

test/unit/messaging/messaging.spec.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -366,13 +366,14 @@ describe('Messaging', () => {
366366
}).to.throw('First argument passed to admin.messaging() must be a valid Firebase app instance.');
367367
});
368368

369-
it('should throw given app without project ID', () => {
370-
expect(() => {
371-
const appWithoutProhectId = mocks.mockCredentialApp();
372-
return new Messaging(appWithoutProhectId);
373-
}).to.throw('Failed to determine project ID for Messaging. Initialize the SDK with service '
374-
+ 'account credentials or set project ID as an app option. Alternatively set the '
375-
+ 'GOOGLE_CLOUD_PROJECT environment variable.');
369+
it('should reject given app without project ID', () => {
370+
const appWithoutProjectId = mocks.mockCredentialApp();
371+
const messagingWithoutProjectId = new Messaging(appWithoutProjectId);
372+
messagingWithoutProjectId.send({topic: 'test'})
373+
.should.eventually.be.rejectedWith(
374+
'Failed to determine project ID for Messaging. Initialize the SDK with service '
375+
+ 'account credentials or set project ID as an app option. Alternatively set the '
376+
+ 'GOOGLE_CLOUD_PROJECT environment variable.');
376377
});
377378

378379
it('should not throw given a valid app', () => {
@@ -597,11 +598,10 @@ describe('Messaging', () => {
597598
}).to.throw('messages list must not contain more than 500 items');
598599
});
599600

600-
it('should throw when a message is invalid', () => {
601+
it('should reject when a message is invalid', () => {
601602
const invalidMessage: Message = {} as any;
602-
expect(() => {
603-
messaging.sendAll([validMessage, invalidMessage]);
604-
}).to.throw('Exactly one of topic, token or condition is required');
603+
messaging.sendAll([validMessage, invalidMessage])
604+
.should.eventually.be.rejectedWith('Exactly one of topic, token or condition is required');
605605
});
606606

607607
const invalidDryRun = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop];

0 commit comments

Comments
 (0)