GetProjectId returns null on Compute Engine / Cloud Run with application default credentials

Đang mở
#588 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@lahirumaramba đang làm issue này rồi.

Từ ngày 2/11/2026.

  • #590 của @lahirumaramba — đang mở

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
78/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
csharp, google-cloud
Lĩnh vực
api, backend

Hướng nghiên cứu

Start with FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs, especially GetProjectId, and read the supplied ProjectIdDiscoveryTest.cs test and its command for running it. The issue suggests consulting Google.Api.Gax.Platform for ComputeCredential after existing lookups. Done when the metadata-server test discovers metadata-project without explicit project-ID settings and the relevant tests pass.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

[READ] Step 1: Are you in the right place?

This is about the code in this repository (FirebaseApp.GetProjectId).

[REQUIRED] Step 2: Describe your environment
  • Operating System version: Linux x64
  • Firebase SDK version: 3.7.0 (main, c08a653)
  • Firebase Product: auth, messaging (anything that needs the project ID)
  • .NET version: .NET 6.0 test target (SDK 10.0.403)
  • OS: Linux (reproduced against a local metadata server emulator, not on a real Compute Engine instance)
[REQUIRED] Step 3: Describe the problem

Location (firebase/firebase-admin-dotnet @ c08a653):

        internal string GetProjectId()
        {
            if (!string.IsNullOrEmpty(this.Options.ProjectId))
            {
                return this.Options.ProjectId;
            }

            var projectId = this.Options.Credential.ToServiceAccountCredential()?.ProjectId;
            if (!string.IsNullOrEmpty(projectId))
            {
                return projectId;
            }

            foreach (var variableName in new[] { "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT" })
            {
                projectId = Environment.GetEnvironmentVariable(variableName);
                if (!string.IsNullOrEmpty(projectId))
                {
                    return projectId;
                }
            }

            return null;
        }

Problem

On Google Cloud managed environments, GoogleCredential.GetApplicationDefault() gives a ComputeCredential, and ToServiceAccountCredential() returns null for it. Unless AppOptions.ProjectId or GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT is set, GetProjectId() returns null, although the metadata server knows the project. Services then fail: FirebaseMessaging with Project ID is required to access messaging service..., ID token verification with Must initialize FirebaseApp with a project ID to verify ID tokens.

Setting the project ID explicitly works, and the messaging error message says so; this is about not having to when the environment already provides it, as firebase-admin-node does. The doc comment of GetProjectId describes the current lookup order, so this is a gap rather than a broken promise.

Steps to reproduce:
  1. Run on a Compute Engine instance (or Cloud Run, GKE) with no GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT.
  2. Initialize with application default credentials and no project ID, then use a service that needs it.

Output of the test below (the metadata server is emulated with a local HttpListener through GCE_METADATA_HOST, which Google.Apis.Auth and Google.Api.Gax both honour). Run from FirebaseAdmin/: dotnet test FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj -f net6.0 --filter "FullyQualifiedName~ProjectIdDiscoveryTest":

  Failed FirebaseAdmin.Tests.ProjectIdDiscoveryTest.GetProjectIdFromMetadataServerForComputeCredential [56 ms]
  Error Message:
   Assert.Equal() Failure: Strings differ
Expected: "metadata-project"
Actual:   null
Failed!  - Failed:     1, Passed:     0, Skipped:     0, Total:     1, Duration: 56 ms - FirebaseAdmin.Tests.dll (net6.0)
Relevant Code:
var app = FirebaseApp.Create(new AppOptions()
{
    Credential = GoogleCredential.GetApplicationDefault(), // ComputeCredential on GCE/Cloud Run
});

// internal; what the services use. Returns null here.
var projectId = app.GetProjectId();

// ArgumentException: Project ID is required to access messaging service. ...
var messaging = FirebaseMessaging.GetMessaging(app);
ProjectIdDiscoveryTest.cs
// Copyright 2026, Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Xunit;

namespace FirebaseAdmin.Tests
{
    public class ProjectIdDiscoveryTest : IDisposable
    {
        private const string MetadataJson = @"{
            ""instance"": {
                ""id"": ""1234567890"",
                ""zone"": ""projects/1234567890/zones/us-central1-a""
            },
            ""project"": {
                ""numericProjectId"": 1234567890,
                ""projectId"": ""metadata-project""
            }
        }";

        private readonly string savedGoogleCloudProject;
        private readonly string savedGcloudProject;
        private readonly string savedMetadataHost;
        private readonly HttpListener listener;
        private readonly ConcurrentQueue<string> requests = new ConcurrentQueue<string>();

        public ProjectIdDiscoveryTest()
        {
            this.savedGoogleCloudProject = Environment.GetEnvironmentVariable("GOOGLE_CLOUD_PROJECT");
            this.savedGcloudProject = Environment.GetEnvironmentVariable("GCLOUD_PROJECT");
            this.savedMetadataHost = Environment.GetEnvironmentVariable("GCE_METADATA_HOST");
            Environment.SetEnvironmentVariable("GOOGLE_CLOUD_PROJECT", null);
            Environment.SetEnvironmentVariable("GCLOUD_PROJECT", null);

            var port = GetFreePort();
            this.listener = new HttpListener();
            this.listener.Prefixes.Add($"http://localhost:{port}/");
            this.listener.Start();
            Task.Run(this.ServeMetadataAsync);
            Environment.SetEnvironmentVariable("GCE_METADATA_HOST", $"localhost:{port}");
        }

        [Fact]
        public void GetProjectIdFromMetadataServerForComputeCredential()
        {
            var app = FirebaseApp.Create(new AppOptions()
            {
                Credential = GoogleCredential.FromComputeCredential(new ComputeCredential()),
            });

            Assert.Equal("metadata-project", app.GetProjectId());
            Assert.NotEmpty(this.requests);
        }

        public void Dispose()
        {
            Environment.SetEnvironmentVariable("GOOGLE_CLOUD_PROJECT", this.savedGoogleCloudProject);
            Environment.SetEnvironmentVariable("GCLOUD_PROJECT", this.savedGcloudProject);
            Environment.SetEnvironmentVariable("GCE_METADATA_HOST", this.savedMetadataHost);
            this.listener.Close();
            FirebaseApp.DeleteAll();
        }

        private static int GetFreePort()
        {
            var socket = new TcpListener(IPAddress.Loopback, 0);
            socket.Start();
            var port = ((IPEndPoint)socket.LocalEndpoint).Port;
            socket.Stop();
            return port;
        }

        private async Task ServeMetadataAsync()
        {
            while (this.listener.IsListening)
            {
                HttpListenerContext context;
                try
                {
                    context = await this.listener.GetContextAsync().ConfigureAwait(false);
                }
                catch (Exception)
                {
                    return;
                }

                this.requests.Enqueue(context.Request.RawUrl);
                var body = Encoding.UTF8.GetBytes(
                    context.Request.RawUrl.Contains("project-id") ? "metadata-project" : MetadataJson);
                context.Response.AddHeader("Metadata-Flavor", "Google");
                context.Response.ContentType = "application/json";
                context.Response.OutputStream.Write(body, 0, body.Length);
                context.Response.Close();
            }
        }
    }
}

Already fixed in firebase/firebase-admin-node

PR: https://github.com/firebase/firebase-admin-node/pull/755 — "fix(auth): Fixed the ability to discover the project ID when running on GCP managed environments" (merged 2020-01-10T22:19:46Z, merge commit 2cfa51add9a). When no project ID is configured explicitly and the app uses ComputeEngineCredential, findProjectId asks the metadata service and caches the result.

How firebase-admin-node fixed it (relevant hunks, verbatim):

--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -82,17 +83,28 @@ export function getProjectId(app: FirebaseApp): string | null {
...
 export function findProjectId(app: FirebaseApp): Promise<string | null> {
-  return Promise.resolve(getProjectId(app));
+  const projectId = getExplicitProjectId(app);
+  if (projectId) {
+    return Promise.resolve(projectId);
+  }
+
+  const credential = app.options.credential;
+  if (credential instanceof ComputeEngineCredential) {
+    return credential.getProjectId();
+  }
+
+  return Promise.resolve(null);
 }
--- a/src/auth/credential.ts
+++ b/src/auth/credential.ts
@@ -209,10 +210,21 @@ export class ComputeEngineCredential implements Credential {
   }
 
   public getProjectId(): Promise<string> {
+    if (this.projectId) {
+      return Promise.resolve(this.projectId);
+    }
+
     const request = this.buildRequest(GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH);
     return this.httpClient.send(request)
       .then((resp) => {
-        return resp.text!;
+        this.projectId = resp.text!;
+        return this.projectId;
+      })
+      .catch((err) => {
+        const detail: string = (err instanceof HttpError) ? getDetailFromResponse(err.response) : err.message;
+        throw new FirebaseAppError(
+          AppErrorCodes.INVALID_CREDENTIAL,
+          `Failed to determine project ID: ${detail}`);
       });
   }
 

Suggested fix

After the existing lookups, and only for a ComputeCredential, take the project ID from Google.Api.Gax.Platform, which is already a dependency through Google.Api.Gax.Rest, reads the metadata server and caches the result:

diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs
--- a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs
+++ b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs
@@ -18,6 +18,7 @@ using System.Collections.Immutable;
 using System.Reflection;
 using System.Runtime.CompilerServices;
 using Google;
+using Google.Api.Gax;
 using Google.Apis.Auth.OAuth2;
 using Google.Apis.Logging;
 
@@ -302,8 +303,9 @@ namespace FirebaseAdmin
         /// Returns the Google Cloud Platform project ID associated with this Firebase app. If a
         /// project ID is specified in <see cref="AppOptions"/>, that value is returned. If not
         /// attempts to determine a project ID from the <see cref="GoogleCredential"/> used to
-        /// initialize the app. Looks up the GOOGLE_CLOUD_PROJECT environment variable when all
-        /// else fails.
+        /// initialize the app, then from the GOOGLE_CLOUD_PROJECT environment variable. When the
+        /// app uses a <see cref="ComputeCredential"/>, falls back to the project ID reported by
+        /// the metadata server of the Google Cloud environment.
         /// </summary>
         /// <returns>A project ID string or null.</returns>
         internal string GetProjectId()
@@ -328,6 +330,11 @@ namespace FirebaseAdmin
                 }
             }
 
+            if (this.Options.Credential.UnderlyingCredential is ComputeCredential)
+            {
+                return Platform.Instance().ProjectId;
+            }
+
             return null;
         }
 

Verified on c08a653 (.NET SDK 10.0.403, net6.0): the test above fails without this patch and passes with it; no other test results change (FirebaseAdmin.Tests on net6.0; net462 not run). Not yet tried on a real Google Cloud instance.

I can submit a PR for this.

Checked: #246 (FIREBASE_CONFIG), #292 (firebase-tools tokens), #339 (env-var credentials) and #439 (401 on topic subscribe) are other credential questions; searches for project id metadata, GetProjectId null, project ID Compute Engine and ComputeCredential project found no report of this.

Ngôn ngữ chính
C#
Star
407
Fork
147
Merge trung bình
1 ngày 3 giờ
Pull request đã merge (30 ngày)
10

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của firebase/firebase-admin-dotnet

Tất cả issue của firebase/firebase-admin-dotnet

Issue tương tự

Thêm issue về C#

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.