Louis Beaumont
Louis Beaumont

Reputation: 338

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

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.

Upvotes: 4

Views: 838

Answers (1)

Louis Beaumont
Louis Beaumont

Reputation: 338

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"]

Upvotes: 3

Related Questions