调用带有 Firebase 认证的 Dart 中的 Google Cloud Run gRPC:证书由未知机构签署

huangapple go评论89阅读模式
英文:

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

问题

服务器

我使用 gRPC 中间件来检查流中的 Firebase 认证令牌:

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
}

客户端

客户端只需将其 Firebase 认证令牌传递给每个流请求:

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')))

在本地运行服务器时(并切换到不安全模式),它可以正常工作。
在部署时,客户端应该使用 ChannelCredentials.secure(),因为 GCP run 会自己管理 SSL。但是我遇到了这个错误:

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

我应该向 ChannelCredentials.secure() 传递一些额外的参数吗?

我的 GCP run 启用了 HTTP2,并且选择了 "Allow unauthenticated invocations"。

非常感谢。

英文:

Server

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

package main
...
func main() {
	port := os.Getenv(&quot;PORT&quot;)
	if port == &quot;&quot; {
		port = &quot;8080&quot;
	}

	grpcEndpoint := fmt.Sprintf(&quot;:%s&quot;, port)
	log.Printf(&quot;gRPC endpoint [%s]&quot;, grpcEndpoint)

	logger, err := zap.NewProduction()
	if err != nil {
		log.Fatalf(&quot;Failed to init logger: %v&quot;, 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, &amp;firebase.Config{
		ProjectID: &quot;my-firebase-project&quot;,
	})
	server.App = fb
	if err != nil {
		panic(fmt.Sprintf(&quot;Failed to init firebase: %v&quot;, err))
	}
	pb.RegisterMyAwesomeServer(grpcServer, server.NewServer())

	listen, err := net.Listen(&quot;tcp&quot;, grpcEndpoint)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf(&quot;Starting: gRPC Listener [%s]\n&quot;, 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, &quot;bearer&quot;)
	if err != nil {
		return nil, err
	}

	tokenInfo, err := parseToken(ctx, token)
	if err != nil {
		return nil, status.Errorf(codes.Unauthenticated, &quot;invalid auth token: %v&quot;, err)
	}
	grpc_ctxtags.Extract(ctx).Set(&quot;auth.uid&quot;, tokenInfo.UID)

	newCtx := context.WithValue(ctx, AuthToken(&quot;tokenInfo&quot;), 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&lt;R&gt; interceptStreaming&lt;Q, R&gt;(
      ClientMethod&lt;Q, R&gt; method,
      Stream&lt;Q&gt; requests,
      CallOptions options,
      ClientStreamingInvoker&lt;Q, R&gt; invoker) {
    return invoker(
      method,
      requests,
      options = options.mergedWith(
        CallOptions(metadata: {&#39;authorization&#39;: &#39;bearer $_authToken&#39;}),
      ),
    );
  }
}
final token = await firebase.auth!.currentUser!.getIdToken();
final apiUrl = &quot;my.gcp.run.url&quot;
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: &#39;bar&#39;)))

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/securetoken@system.gserviceaccount.com": 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.

答案1

得分: 3

确实,后端缺少证书... 通过使用以下方法解决:

在 Dockerfile 中添加:

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

WORKDIR /all

COPY . .

# 构建静态二进制文件
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

### 这个解决了问题
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
###

ENV GOOGLE_APPLICATION_CREDENTIALS /svc.dev.json

ENTRYPOINT ["/server", "./config.yaml"]

希望这能帮到你!

英文:

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 [&quot;/server&quot;, &quot;./config.yaml&quot;]

huangapple
  • 本文由 发表于 2021年10月13日 21:07:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/69556144.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定