fix(grpc): return 401 for unauthenticated v1 gateway errors (#11786)

Fixes #11730

The v1 gRPC middleware can return native gRPC status errors such as
`codes.Unauthenticated` when the auth header is missing.

Those errors were being passed through the generic gRPC error converter,
which only handled ZITADEL errors and downgraded raw gRPC statuses to
`codes.Unknown`.

Through grpc-gateway that caused `/auth/v1/users/me` to return HTTP 500
instead of HTTP 401.

So this change preserves native gRPC status errors in
`ZITADELToGRPCError`, keeps `ExtractZITADELError` aligned with those
transport codes for activity reporting, and adds regression coverage for
the converter, middleware, and gateway HTTP behavior.

---------

Co-authored-by: Gayathri Vijayan <[email protected]>
This commit is contained in:
Oluwatobi Mustapha
2026-04-10 12:22:40 +00:00
committed by GitHub
co-authored by Gayathri Vijayan
parent 035ac4b9a2
commit fbd43ced6e
4 changed files with 80 additions and 0 deletions
@@ -23,6 +23,11 @@ func ZITADELToGRPCError(ctx context.Context, err error) error {
if err == nil {
return nil
}
if grpcStatus, ok := status.FromError(err); ok {
// Native gRPC status errors are already transport-safe and may carry details.
logging.Log(ctx, grpcStatusLevel(grpcStatus.Code()), grpcStatus.Message(), "err", err, "code", grpcStatus.Code())
return err
}
code, key, id, lvl := extractError(err)
msg := key
@@ -78,6 +83,9 @@ func extractError(err error) (c codes.Code, msg, id string, lvl slog.Level) {
if ok := errors.As(err, &connErr); ok {
return codes.Internal, "db connection error", "", slog.LevelError
}
if grpcStatus, ok := status.FromError(err); ok {
return grpcStatus.Code(), grpcStatus.Message(), "", grpcStatusLevel(grpcStatus.Code())
}
zitadelErr, ok := zerrors.AsZitadelError(err)
if !ok {
return codes.Unknown, err.Error(), "", slog.LevelError
@@ -126,6 +134,24 @@ func extractError(err error) (c codes.Code, msg, id string, lvl slog.Level) {
return c, msg, id, lvl
}
func grpcStatusLevel(code codes.Code) slog.Level {
switch code { //nolint:exhaustive
case codes.AlreadyExists,
codes.Canceled,
codes.FailedPrecondition,
codes.InvalidArgument,
codes.NotFound,
codes.OutOfRange,
codes.PermissionDenied,
codes.Unauthenticated:
return slog.LevelWarn
case codes.Unimplemented:
return slog.LevelInfo
default:
return slog.LevelError
}
}
func getErrorInfo(id, key string, err error) protoadapt.MessageV1 {
var errorInfo protoadapt.MessageV1