diff --git a/src/app/credential-internal.ts b/src/app/credential-internal.ts index 28442ba8f7..f456958cb0 100644 --- a/src/app/credential-internal.ts +++ b/src/app/credential-internal.ts @@ -32,6 +32,7 @@ const GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token'; // NOTE: the Google Metadata Service uses HTTP over a vlan const GOOGLE_METADATA_SERVICE_HOST = 'metadata.google.internal'; const GOOGLE_METADATA_SERVICE_TOKEN_PATH = '/computeMetadata/v1/instance/service-accounts/default/token'; +const GOOGLE_METADATA_SERVICE_IDENTITY_PATH = '/computeMetadata/v1/instance/service-accounts/default/identity'; const GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH = '/computeMetadata/v1/project/project-id'; const GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH = '/computeMetadata/v1/instance/service-accounts/default/email'; @@ -209,6 +210,15 @@ export class ComputeEngineCredential implements Credential { return requestAccessToken(this.httpClient, request); } + /** + * getIDToken returns a OIDC token from the compute metadata service that can be used to make authenticated calls to audience + * @param audience the URL the returned ID token will be used to call. + */ + public getIDToken(audience: string): Promise { + const request = this.buildRequest(`${GOOGLE_METADATA_SERVICE_IDENTITY_PATH}?audience=${audience}`); + return requestIDToken(this.httpClient, request); + } + public getProjectId(): Promise { if (this.projectId) { return Promise.resolve(this.projectId); @@ -421,6 +431,17 @@ function requestAccessToken(client: HttpClient, request: HttpRequestConfig): Pro }); } +/** + * Obtain a new OIDC token by making a remote service call. + */ + function requestIDToken(client: HttpClient, request: HttpRequestConfig): Promise { + return client.send(request).then((resp) => { + return resp.data; + }).catch((err) => { + throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, getErrorMessage(err)); + }); +} + /** * Constructs a human-readable error message from the given Error. */ diff --git a/src/app/credential.ts b/src/app/credential.ts index b5857903f8..7b6db7093f 100644 --- a/src/app/credential.ts +++ b/src/app/credential.ts @@ -44,4 +44,13 @@ export interface Credential { * @returns A Google OAuth2 access token object. */ getAccessToken(): Promise; + + /** + * Returns an encoded OIDC token used to authenticate calls to + * private Cloud Functions or other compute services. + * + * @param audience The URL this token will be used to call. + * @returns A base64 encoded OIDC token. + */ + getIDToken(audience: string): Promise; } \ No newline at end of file diff --git a/src/functions/functions-api-client-internal.ts b/src/functions/functions-api-client-internal.ts index 36b7bf99c2..caf78556c6 100644 --- a/src/functions/functions-api-client-internal.ts +++ b/src/functions/functions-api-client-internal.ts @@ -62,7 +62,7 @@ export class FunctionsApiClient { * @param extensionId - Optional canonical ID of the extension. * @param opts - Optional options when enqueuing a new task. */ - public enqueue(data: any, functionName: string, extensionId?: string, opts?: TaskOptions): Promise { + public async enqueue(data: any, functionName: string, extensionId?: string, opts?: TaskOptions): Promise { if (!validator.isNonEmptyString(functionName)) { throw new FirebaseFunctionsError( 'invalid-argument', 'Function name must be a non empty string'); @@ -81,10 +81,10 @@ export class FunctionsApiClient { if (typeof extensionId !== 'undefined' && validator.isNonEmptyString(extensionId)) { resources.resourceId = `ext-${extensionId}-${resources.resourceId}`; } - + return this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT) .then((serviceUrl) => { - return this.updateTaskPayload(task, resources) + return this.updateTaskPayload(task, resources, ) .then((task) => { const request: HttpRequestConfig = { method: 'POST', @@ -224,22 +224,19 @@ export class FunctionsApiClient { return task; } - private updateTaskPayload(task: Task, resources: utils.ParsedResource): Promise { - return Promise.resolve() - .then(() => { - if (validator.isNonEmptyString(task.httpRequest.url)) { - return task.httpRequest.url; - } - return this.getUrl(resources, FIREBASE_FUNCTION_URL_FORMAT); - }) - .then((functionUrl) => { - return this.getServiceAccount() - .then((account) => { - task.httpRequest.oidcToken.serviceAccountEmail = account; - task.httpRequest.url = functionUrl; - return task; - }) - }); + private async updateTaskPayload(task: Task, resources: utils.ParsedResource): Promise { + const functionUrl = validator.isNonEmptyString(task.httpRequest.url) + ? task.httpRequest.url + : await this.getUrl(resources, FIREBASE_FUNCTION_URL_FORMAT); + task.httpRequest.url = functionUrl; + try { + const idToken = await this.app.options.credential.getIDToken(functionUrl) + task.httpRequest.headers = {...task.httpRequest.headers, "Authorization": `Bearer ${idToken}`} + } catch (err: any) { + const serviceAccount = await this.getServiceAccount(); + task.httpRequest.oidcToken.serviceAccountEmail = serviceAccount; + } + return task; } private toFirebaseError(err: HttpError): PrefixedFirebaseError {