英文:
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("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/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 ["/server", "./config.yaml"]
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论