Calling Google Cloud Run gRPC from Dart with Firebase authentication: certificate signed by unknown authority

257

Indeed, the backend was missing certificates...
Solved by using:

COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

In Dockerfile

FROM golang as build

WORKDIR /all

COPY . .

# Build static binary
RUN CGO_ENABLED=0 GOOS=linux \
    go build -a -installsuffix cgo \
    -o /go/bin/server \
    cmd/main/main.go

FROM scratch

COPY --from=build /go/bin/server /server
COPY --from=build /all/config.yaml /config.yaml
COPY --from=build /all/svc.dev.json /svc.dev.json

### THIS SOLVED
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
###

ENV GOOGLE_APPLICATION_CREDENTIALS /svc.dev.json

ENTRYPOINT ["/server", "./config.yaml"]
Share:
257
Louis Beaumont
Author by

Louis Beaumont

Updated on January 01, 2023

Comments

  • Louis Beaumont
    Louis Beaumont over 1 year

    Server

    I use a gRPC middleware to check the Firebase authentication token in streams:

    package main
    ...
    func main() {
        port := os.Getenv("PORT")
        if port == "" {
            port = "8080"
        }
    
        grpcEndpoint := fmt.Sprintf(":%s", port)
        log.Printf("gRPC endpoint [%s]", grpcEndpoint)
    
        logger, err := zap.NewProduction()
        if err != nil {
            log.Fatalf("Failed to init logger: %v", err)
        }
        defer logger.Sync() // flushes buffer, if any
    
        grpcServer := grpc.NewServer(
            grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
                grpc_ctxtags.StreamServerInterceptor(),
                grpc_zap.StreamServerInterceptor(logger),
                grpc_auth.StreamServerInterceptor(server.AuthFunc))),
        )
        ctx := context.Background()
        fb, err := firebase.NewApp(ctx, &firebase.Config{
            ProjectID: "my-firebase-project",
        })
        server.App = fb
        if err != nil {
            panic(fmt.Sprintf("Failed to init firebase: %v", err))
        }
        pb.RegisterMyAwesomeServer(grpcServer, server.NewServer())
    
        listen, err := net.Listen("tcp", grpcEndpoint)
        if err != nil {
            log.Fatal(err)
        }
        log.Printf("Starting: gRPC Listener [%s]\n", grpcEndpoint)
        log.Fatal(grpcServer.Serve(listen))
    }
    
    package server
    ...
    func parseToken(ctx context.Context, token string) (*auth.Token, error) {
        client, err := App.Auth(ctx)
        if err != nil {
            return nil, err
        }
    
        nt, err := client.VerifyIDToken(ctx, token)
        if err != nil {
            return nil, err
        }
    
        return nt, nil
    }
    
    type AuthToken string
    func AuthFunc(ctx context.Context) (context.Context, error) {
        token, err := grpc_auth.AuthFromMD(ctx, "bearer")
        if err != nil {
            return nil, err
        }
    
        tokenInfo, err := parseToken(ctx, token)
        if err != nil {
            return nil, status.Errorf(codes.Unauthenticated, "invalid auth token: %v", err)
        }
        grpc_ctxtags.Extract(ctx).Set("auth.uid", tokenInfo.UID)
    
        newCtx := context.WithValue(ctx, AuthToken("tokenInfo"), tokenInfo)
    
        return newCtx, nil
    }
    

    Client

    The client simply pass his Firebase authentication token to every stream requests:

    class ClientFirebaseAuthInterceptor implements ClientInterceptor {
      final String _authToken;
      ClientFirebaseAuthInterceptor(this._authToken);
      @override
      ResponseStream<R> interceptStreaming<Q, R>(
          ClientMethod<Q, R> method,
          Stream<Q> requests,
          CallOptions options,
          ClientStreamingInvoker<Q, R> invoker) {
        return invoker(
          method,
          requests,
          options = options.mergedWith(
            CallOptions(metadata: {'authorization': 'bearer $_authToken'}),
          ),
        );
      }
    }
    
    final token = await firebase.auth!.currentUser!.getIdToken();
    final apiUrl = "my.gcp.run.url"
    final channelOptions = ChannelOptions(ChannelCredentials.secure(
        authority: apiUrl,
    ));
        
    final channel = ClientChannel(
        apiUrl,
        options: channelOptions,
        port: 443,
    );
    final client = MyAwesomeClient(
        channel!,
        options: CallOptions(
          timeout: Duration(seconds: 30),
        ),
        interceptors: [
          ClientFirebaseAuthInterceptor(token),
        ],
    );
    
    client.myAwesomeStream(Stream.value(MyAwesomeRequest(foo: 'bar')))
    

    It works fine when running the server locally (and turning to insecure mode). When deployed I should use ChannelCredentials.secure() in the client right? As GCP run manage the SSL by itself? Somehow I get this error:

    gRPC Error (code: 16, codeName: UNAUTHENTICATED, message: invalid auth token: Get "https://www.googleapis.com/robot/v1/metadata/x509/[email protected]": x509: certificate signed by unknown authority, details: [], rawResponse: null, trailers: ...})

    Should I pass some additional arguments to ChannelCredentials.secure()?

    My GCP run has HTTP2 enabled and "Allow unauthenticated invocations Check this if you are creating a public API or website."

    Thanks a lot.