Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Blazor WASM | Firestore: Receiving Mixed Content error when using Google.Cloud.Firestore.FirestoreDb.CreateAsync

Trying to create a FirebaseDb object in C# (Blazor WASM) is throwing a Blocked mixed content error after it's deployed to Firebase. Is there a way to force this to use HTTPS?

The errror:

Blocked loading mixed active content “http://169.254.169.254/” dotnet.3.2.0-preview3.20168.1.js:1:163131 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] blazor.webassembly.js:1:36074 Unhandled exception rendering component: TypeError: NetworkError when attempting to fetch resource. blazor.webassembly.js:1:36074 WebAssembly.JSException: TypeError: NetworkError when attempting to fetch resource. blazor.webassembly.js:1:36074 at WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler.doFetch (System.Threading.Tasks.TaskCompletionSource1[TResult] tcs, System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) <0x257c7e0 + 0x00988> in <filename unknown>:0 blazor.webassembly.js:1:36074 at WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler.SendAsync (System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) <0x255e648 + 0x00184> in <filename unknown>:0 blazor.webassembly.js:1:36074 at System.Net.Http.HttpClient.FinishSendAsyncBuffered (System.Threading.Tasks.Task1[TResult] sendTask, System.Net.Http.HttpRequestMessage request, System.Threading.CancellationTokenSource cts, System.Boolean disposeCts) <0x256c970 + 0x00278> in :0 blazor.webassembly.js:1:36074 at Google.Apis.Auth.OAuth2.ComputeCredential.IsRunningOnComputeEngineNoCache () <0x24f5570 + 0x0018c> in :0 blazor.webassembly.js:1:36074 at Google.Apis.Auth.OAuth2.DefaultCredentialProvider.CreateDefaultCredentialAsync () <0x24e22f0 + 0x0020e> in :0 blazor.webassembly.js:1:36074 at Google.Api.Gax.Grpc.ChannelPool.CreateChannelCredentialsUncached () <0x24cf210 + 0x000d8> in :0 blazor.webassembly.js:1:36074 at Google.Api.Gax.Grpc.ChannelPool.GetChannelAsync (Google.Api.Gax.Grpc.ServiceEndpoint endpoint, System.Collections.Generic.IEnumerable`1[T] channelOptions) <0x246fb30 + 0x000f4> in :0 blazor.webassembly.js:1:36074 at Google.Cloud.Firestore.V1.FirestoreClient.CreateAsync (Google.Api.Gax.Grpc.ServiceEndpoint endpoint, Google.Cloud.Firestore.V1.FirestoreSettings settings) <0x246e908 + 0x000ec> in :0 blazor.webassembly.js:1:36074 at Google.Cloud.Firestore.FirestoreDb.CreateAsync (System.String projectId, Google.Cloud.Firestore.V1.FirestoreClient client) <0x2449d00 + 0x001d0> in :0 blazor.webassembly.js:1:36074 at blog.Pages.Index.OnInitializedAsync () <0x2434eb8 + 0x000c8> in :0 blazor.webassembly.js:1:36074 at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync () <0x2330b40 + 0x0014c> in :0 blazor.webassembly.js:1:36074 at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask (System.Threading.Tasks.Task taskToHandle) <0x247d5b8 + 0x000c2> in :0 blazor.webassembly.js:1:36074

The code:

    protected override async Task OnInitializedAsync()
    {
        string projectId = "my-poject-id";
        FirestoreDb db = await FirestoreDb.CreateAsync(projectId);
    }
like image 961
osroc Avatar asked Oct 15 '22 04:10

osroc


1 Answers

TL;DR
The Firestore library is not supposed to be run within the browser context. It's developed to be used server-side or maybe in an admin tool. The library will allow you to do everything in Firestore and shipping the credentials to customers would be a security risk.


Based on your stacktrace I'm assuming you're using the Google.Cloud.Firestore library. Unfortunately, this library is designed to be a "server client library". A client library to be used server side.
These "server client libraries" are designed differently than the "mobile/web client libraries".
The mobile/web libraries will use Firebase's authentication (username/password, Facebook, etc) and the security model will be applied on that user context. The server libraries essentially give you access to everything.

Now the issues you're running into are caused by this SDK not being supported on Blazor. First of all, the GOOGLE_APPLICATION_CREDENTIALS environment variable that the SDK relies on won't be available inside your Blazor application. If it were available, the file wouldn't be accessible from within the browser sandbox.

You can circumvent both issues by using the FirestoreDbBuilder and setting the JsonCredentials and ProjectId manually like this:

var builder = new FirestoreDbBuilder();
builder.JsonCredentials = "{\"type\": \"service_account\", \"project_id\": \"\", \"private_key_id\": \"\", \"private_key\": \"\", \"client_email\": \"\", \"client_id\": \"\", \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\", \"token_uri\": \"https://oauth2.googleapis.com/token\", \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\", \"client_x509_cert_url\": \"\" }";
builder.ProjectId = "";
FirestoreDb db = builder.Build();

This will result into the next issue, the GRPC library used doesn't support Blazor/.NET WASM resulting in this stacktrace:

crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Unsupported platform.
System.InvalidOperationException: Unsupported platform.
  at Grpc.Core.Internal.NativeExtension.GetNativeLibraryFilename () [0x0003f] in T:\src\github\grpc\src\csharp\Grpc.Core\Internal\NativeExtension.cs:231 
  at Grpc.Core.Internal.NativeExtension.LoadUnmanagedLibrary () [0x0000a] in T:\src\github\grpc\src\csharp\Grpc.Core\Internal\NativeExtension.cs:93 
  at Grpc.Core.Internal.NativeExtension.LoadNativeMethods () [0x0001a] in T:\src\github\grpc\src\csharp\Grpc.Core\Internal\NativeExtension.cs:120 
  at Grpc.Core.Internal.NativeExtension..ctor () [0x00006] in T:\src\github\grpc\src\csharp\Grpc.Core\Internal\NativeExtension.cs:40 
  at Grpc.Core.Internal.NativeExtension.Get () [0x00022] in T:\src\github\grpc\src\csharp\Grpc.Core\Internal\NativeExtension.cs:65 
  at Grpc.Core.Internal.NativeMethods.Get () [0x00000] in T:\src\github\grpc\src\csharp\Grpc.Core\Internal\NativeMethods.cs:49 
  at Grpc.Core.GrpcEnvironment.GrpcNativeInit () [0x00016] in T:\src\github\grpc\src\csharp\Grpc.Core\GrpcEnvironment.cs:373 
  at Grpc.Core.GrpcEnvironment..ctor () [0x0001e] in T:\src\github\grpc\src\csharp\Grpc.Core\GrpcEnvironment.cs:302 
  at Grpc.Core.GrpcEnvironment.AddRef () [0x00028] in T:\src\github\grpc\src\csharp\Grpc.Core\GrpcEnvironment.cs:78 
  at Grpc.Core.Channel..ctor (System.String target, Grpc.Core.ChannelCredentials credentials, System.Collections.Generic.IEnumerable`1[T] options) [0x00041] in T:\src\github\grpc\src\csharp\Grpc.Core\Channel.cs:70 
  at Google.Api.Gax.Grpc.GrpcCore.GrpcCoreAdapter.CreateChannelImpl (System.String endpoint, Grpc.Core.ChannelCredentials credentials, Google.Api.Gax.Grpc.GrpcChannelOptions options) [0x00000] in T:\src\github\gax-dotnet\releasebuild\Google.Api.Gax.Grpc.GrpcCore\GrpcCoreAdapter.cs:34 
  at Google.Api.Gax.Grpc.GrpcAdapter.CreateChannel (System.String endpoint, Grpc.Core.ChannelCredentials credentials, Google.Api.Gax.Grpc.GrpcChannelOptions options) [0x00024] in T:\src\github\gax-dotnet\releasebuild\Google.Api.Gax.Grpc\GrpcAdapter.cs:29 
  at Google.Api.Gax.Grpc.ClientBuilderBase`1[TClient].CreateChannel (System.String endpoint, Grpc.Core.ChannelCredentials credentials) [0x00000] in T:\src\github\gax-dotnet\releasebuild\Google.Api.Gax.Grpc\ClientBuilderBase.cs:423 
  at Google.Api.Gax.Grpc.ClientBuilderBase`1[TClient].CreateCallInvokerAsync (System.Threading.CancellationToken cancellationToken) [0x00145] in T:\src\github\gax-dotnet\releasebuild\Google.Api.Gax.Grpc\ClientBuilderBase.cs:313 
  at Google.Cloud.Firestore.V1.FirestoreClientBuilder.BuildAsyncImpl (System.Threading.CancellationToken cancellationToken) [0x00033] in /_/apis/Google.Cloud.Firestore.V1/Google.Cloud.Firestore.V1/FirestoreClient.g.cs:285 
  at Google.Cloud.Firestore.FirestoreDbBuilder.BuildAsync (System.Threading.CancellationToken cancellationToken) [0x0015a] in /_/apis/Google.Cloud.Firestore/Google.Cloud.Firestore/FirestoreDbBuilder.cs:119 
  at BlazorFirestore.Pages.FetchData.OnInitializedAsync () [0x00061] in C:\Users\niels\source\repos\BlazorFirestore\Pages\FetchData.razor:12 
  at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync () <0x2ff3be8 + 0x0013a> in <filename unknown>:0 

This can be resolved by using Grpc.Net.Client.Web which is a Grpc implementation for .NET that works inside Blazor. Create the following class (inspired on Google's Source):

using System;
using Grpc.Core;
using Google.Api.Gax.Grpc;
using Grpc.Net.Client;
using System.Net.Http;
using Grpc.Net.Client.Web;

// you'll need these packages
// Google.Apis.Auth, Google.Cloud.Firestore, Grpc.Net.Client, Grpc.Net.Client.Web
namespace BlazorFirestore
{
    // most of the code was borrowed from https://github.com/googleapis/gax-dotnet/blob/master/Google.Api.Gax.Grpc.GrpcNetClient/GrpcNetClientAdapter.cs
    public sealed class GrpcWebAdapter : GrpcAdapter
    {
        // this HttpClient using the GrpcWebHandler and mode is crucial to get Grpc to work for Firestore
        private static HttpClient httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));

        // Note: this is "Default" rather than "Instance" as we expect to have other factory methods later, e.g. accepting
        // an HTTP client factory.

        /// <summary>
        /// Returns the default instance of this class.
        /// </summary>
        public static GrpcWebAdapter Default { get; } = new GrpcWebAdapter();

        private GrpcWebAdapter()
        {
        }

        /// <inheritdoc />
        protected override ChannelBase CreateChannelImpl(string endpoint, ChannelCredentials credentials, Google.Api.Gax.Grpc.GrpcChannelOptions options)
        {
            var grpcNetClientOptions = ConvertOptions(credentials, options);
            var address = ConvertEndpoint(endpoint);
            return GrpcChannel.ForAddress(address, grpcNetClientOptions);
        }

        // Internal for testing
        internal static global::Grpc.Net.Client.GrpcChannelOptions ConvertOptions(ChannelCredentials credentials, Google.Api.Gax.Grpc.GrpcChannelOptions options)
        {
            // If service config resolution is explicitly enabled, throw - we can't support that,
            // and users may be depending on it.
            if (options.EnableServiceConfigResolution == true)
            {
                throw new ArgumentException($"{nameof(options.EnableServiceConfigResolution)} is not currently supported in {nameof(GrpcWebAdapter)}");
            }

            if (options.CustomOptions.Count > 0)
            {
                throw new ArgumentException($"Custom options are not currently supported in {nameof(GrpcWebAdapter)}");
            }

            // Options we ignore:
            // - PrimaryUserAgent
            // - KeepAliveTime

            return new global::Grpc.Net.Client.GrpcChannelOptions
            {
                Credentials = credentials,
                MaxReceiveMessageSize = options.MaxReceiveMessageSize,
                MaxSendMessageSize = options.MaxSendMessageSize,
                // pass the GrpcWeb version of the httpclient
                HttpClient = httpClient
            };
        }

        // Internal for testing
        internal static string ConvertEndpoint(string endpoint) =>
            // Note that we assume HTTPS for any bare address; this feels like a reasonable assumption for now.
            endpoint.StartsWith("http:", StringComparison.Ordinal) || endpoint.StartsWith("https:", StringComparison.Ordinal)
            ? endpoint : $"https://{endpoint}";
    }
}

Now set GrpcWebAdapter.Default to the FirestoreDbBuilder.GrpcAdapter:

var builder = new FirestoreDbBuilder();
builder.JsonCredentials = "{\"type\": \"service_account\", \"project_id\": \"\", \"private_key_id\": \"\", \"private_key\": \"\", \"client_email\": \"\", \"client_id\": \"\", \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\", \"token_uri\": \"https://oauth2.googleapis.com/token\", \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\", \"client_x509_cert_url\": \"\" }";
builder.ProjectId = "";
builder.GrpcAdapter = GrpcWebAdapter.Default;
FirestoreDb db = builder.Build();

DocumentReference docRef = db.Collection("users").Document("alovelace");
Dictionary<string, object> user = new Dictionary<string, object>
{
    { "First", "Ada" },
    { "Last", "Lovelace" },
    { "Born", 1815 }
};
await docRef.SetAsync(user);

We get one step closer, but now we run into CORS security issues since this way of communicating with Firestore wasn't designed to run from a browser and there's no way to configure CORS combined with this SDK. These are the resulting errors:

Access to fetch at 'https://firestore.googleapis.com/google.firestore.v1.Firestore/Commit' from origin 'https://localhost:5001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

dotnet.3.2.0.js:1 POST https://firestore.googleapis.com/google.firestore.v1.Firestore/Commit net::ERR_FAILED

blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Status(StatusCode="Internal", Detail="Error starting gRPC call. JSException: TypeError: Failed to fetch", DebugException="WebAssembly.JSException: TypeError: Failed to fetch
        at System.Net.Http.WebAssemblyHttpHandler.doFetch (System.Threading.Tasks.TaskCompletionSource`1[TResult] tcs, System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) <0x3c61b60 + 0x00a30> in <filename unknown>:0 
        at System.Net.Http.WebAssemblyHttpHandler.SendAsync (System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) <0x3c24040 + 0x00174> in <filename unknown>:0 
        at Grpc.Net.Client.Web.GrpcWebHandler.SendAsyncCore (System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) [0x00092] in /_/src/Grpc.Net.Client.Web/GrpcWebHandler.cs:137 
        at System.Net.Http.HttpClient.FinishSendAsyncUnbuffered (System.Threading.Tasks.Task`1[TResult] sendTask, System.Net.Http.HttpRequestMessage request, System.Threading.CancellationTokenSource cts, System.Boolean disposeCts) <0x3d9ae50 + 0x00134> in <filename unknown>:0 
        at Grpc.Net.Client.Internal.GrpcCall`2[TRequest,TResponse].RunCall (System.Net.Http.HttpRequestMessage request, System.Nullable`1[T] timeout) [0x0020c] in /_/src/Grpc.Net.Client/Internal/GrpcCall.cs:477 ")
Grpc.Core.RpcException: Status(StatusCode="Internal", Detail="Error starting gRPC call. JSException: TypeError: Failed to fetch", DebugException="WebAssembly.JSException: TypeError: Failed to fetch
  at System.Net.Http.WebAssemblyHttpHandler.doFetch (System.Threading.Tasks.TaskCompletionSource`1[TResult] tcs, System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) <0x3c61b60 + 0x00a30> in <filename unknown>:0 
  at System.Net.Http.WebAssemblyHttpHandler.SendAsync (System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) <0x3c24040 + 0x00174> in <filename unknown>:0 
  at Grpc.Net.Client.Web.GrpcWebHandler.SendAsyncCore (System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) [0x00092] in /_/src/Grpc.Net.Client.Web/GrpcWebHandler.cs:137 
  at System.Net.Http.HttpClient.FinishSendAsyncUnbuffered (System.Threading.Tasks.Task`1[TResult] sendTask, System.Net.Http.HttpRequestMessage request, System.Threading.CancellationTokenSource cts, System.Boolean disposeCts) <0x3d9ae50 + 0x00134> in <filename unknown>:0 
  at Grpc.Net.Client.Internal.GrpcCall`2[TRequest,TResponse].RunCall (System.Net.Http.HttpRequestMessage request, System.Nullable`1[T] timeout) [0x0020c] in /_/src/Grpc.Net.Client/Internal/GrpcCall.cs:477 ")
  at Google.Api.Gax.Grpc.ApiCallRetryExtensions+<>c__DisplayClass0_0`2[TRequest,TResponse].<WithRetry>b__0 (TRequest request, Google.Api.Gax.Grpc.CallSettings callSettings) [0x00051] in T:\src\github\gax-dotnet\releasebuild\Google.Api.Gax.Grpc\ApiCallRetryExtensions.cs:27 
  at Google.Cloud.Firestore.WriteBatch.CommitAsync (Google.Protobuf.ByteString transactionId, System.Threading.CancellationToken cancellationToken) [0x000b5] in /_/apis/Google.Cloud.Firestore/Google.Cloud.Firestore/WriteBatch.cs:231 
  at Google.Cloud.Firestore.DocumentReference.SetAsync (System.Object documentData, Google.Cloud.Firestore.SetOptions options, System.Threading.CancellationToken cancellationToken) [0x0004b] in /_/apis/Google.Cloud.Firestore/Google.Cloud.Firestore/DocumentReference.cs:181 
  at BlazorFirestore.Pages.FetchData.OnInitializedAsync () [0x00166] in C:\Users\niels\source\repos\BlazorFirestore\Pages\FetchData.razor:21 
  at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync () <0x2ff3be8 + 0x0013a> in <filename unknown>:0 
  at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask (System.Threading.Tasks.Task taskToHandle) <0x3905a50 + 0x000b6> in <filename unknown>:0 

When you explicitly disable CORS in your browser using .\chrome.exe --disable-web-security, then the result will be a successful authentication but the actual API's will return 404.
Why it returns 404 is unclear to me. I can sniff the Grpc.Net.Client.Web version with Fiddler, but the default implementation does not get intercepted by Fiddler. It looks like the web-version uses a different protocol/transport that isn't supported or something. Hopefully somebody can resolve the next issue.

Conclusion: Firestore library for .NET isn't supposed to be used in the browser sandbox and isn't supported that way.

like image 131
Swimburger Avatar answered Nov 15 '22 08:11

Swimburger