Files
zitadel/internal/api/grpc/gerrors/zitadel_errors.go
T
fbd43ced6e 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]>
2026-04-10 12:22:40 +00:00

167 lines
4.8 KiB
Go

package gerrors
import (
"context"
"errors"
"log/slog"
"connectrpc.com/connect"
"github.com/jackc/pgx/v5/pgconn"
"github.com/zitadel/sloggcp"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/protoadapt"
"github.com/zitadel/zitadel/backend/v3/instrumentation/logging"
commandErrors "github.com/zitadel/zitadel/internal/command/errors"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/message"
)
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
msg += " (" + id + ")"
logging.Log(ctx, lvl, msg, "err", err, "code", code)
errorInfo := getErrorInfo(id, key, err)
s, err := status.New(code, msg).WithDetails(errorInfo)
if err != nil {
logging.WithError(ctx, err).Debug("unable to add error detail")
return status.New(code, msg).Err()
}
return s.Err()
}
func ZITADELToConnectError(ctx context.Context, err error) error {
if err == nil {
return nil
}
connectError := new(connect.Error)
if errors.As(err, &connectError) {
// Connect error may be returned by other middlewares,
// so we assume it's a client error and log as warning.
logging.Warn(ctx, connectError.Message(), "err", connectError.Unwrap(), "code", connectError.Code())
return err
}
code, key, id, lvl := extractError(err)
msg := key
msg += " (" + id + ")"
logging.Log(ctx, lvl, msg, "err", err)
errorInfo := getErrorInfo(id, key, err)
cErr := connect.NewError(connect.Code(code), errors.New(msg))
if detail, detailErr := connect.NewErrorDetail(errorInfo.(proto.Message)); detailErr == nil {
cErr.AddDetail(detail)
}
return cErr
}
func ExtractZITADELError(err error) (code codes.Code, msg, id string) {
if err == nil {
return codes.OK, "", ""
}
code, msg, id, _ = extractError(err)
return code, msg, id
}
func extractError(err error) (c codes.Code, msg, id string, lvl slog.Level) {
connErr := new(pgconn.ConnectError)
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
}
msg, id = zitadelErr.GetMessage(), zitadelErr.GetID()
switch zitadelErr.Kind {
case zerrors.KindAlreadyExists:
c, lvl = codes.AlreadyExists, slog.LevelWarn
case zerrors.KindDeadlineExceeded:
c, lvl = codes.DeadlineExceeded, slog.LevelError
case zerrors.KindInternal:
c, lvl = codes.Internal, slog.LevelError
case zerrors.KindInvalidArgument:
c, lvl = codes.InvalidArgument, slog.LevelWarn
case zerrors.KindNotFound:
c, lvl = codes.NotFound, slog.LevelWarn
case zerrors.KindPermissionDenied:
c, lvl = codes.PermissionDenied, slog.LevelWarn
case zerrors.KindPreconditionFailed:
c, lvl = codes.FailedPrecondition, slog.LevelWarn
case zerrors.KindUnauthenticated:
c, lvl = codes.Unauthenticated, slog.LevelWarn
case zerrors.KindUnavailable:
c, lvl = codes.Unavailable, slog.LevelError
case zerrors.KindUnimplemented:
c, lvl = codes.Unimplemented, slog.LevelInfo
case zerrors.KindResourceExhausted:
c, lvl = codes.ResourceExhausted, slog.LevelError
case zerrors.KindDataLoss:
c, lvl = codes.DataLoss, slog.LevelError
case zerrors.KindCanceled:
c, lvl = codes.Canceled, slog.LevelWarn
case zerrors.KindAborted:
c, lvl = codes.Aborted, slog.LevelError
case zerrors.KindOutOfRange:
c, lvl = codes.OutOfRange, slog.LevelWarn
case zerrors.KindUnknown:
c, lvl = codes.Unknown, slog.LevelError
default:
c, lvl = codes.Unknown, slog.LevelError
}
if id == zerrors.IDRecover {
lvl = sloggcp.LevelAlert
}
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
var wpe *commandErrors.WrongPasswordError
if err != nil && errors.As(err, &wpe) {
errorInfo = &message.CredentialsCheckError{Id: id, Message: key, FailedAttempts: wpe.FailedAttempts}
} else {
errorInfo = &message.ErrorDetail{Id: id, Message: key}
}
return errorInfo
}