From ae19b9868325be5ced2338e169d5c4e322eba23f Mon Sep 17 00:00:00 2001
From: ko_oler <kooler89@gmail.com>
Date: Mon, 28 Oct 2024 18:31:27 +0300
Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?=
 =?UTF-8?q?=D0=BD=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE?=
 =?UTF-8?q?=D1=81=D1=82=D1=8C=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7?=
 =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?=
 =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20=D1=81=20=D1=80=D0=B0?=
 =?UTF-8?q?=D0=B7=D0=BD=D1=8B=D0=BC=D0=B8=20Identity=20=D1=81=20=D0=BE?=
 =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D0=B0=D0=BA=D0=BE=D0=B2=D1=8B=D0=BC=20email?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 go.mod                                        |  1 +
 go.sum                                        |  2 +
 logs/zap/example_test.go                      |  2 +-
 pkg/auth/context.go                           | 14 +++++
 pkg/auth/factory.go                           | 42 ++++++++++++-
 pkg/auth/factory_internal_test.go             | 62 +++++++++++++++++++
 pkg/auth/grpc.go                              | 23 +++----
 pkg/auth/user.go                              |  4 +-
 .../middleware/access_logging_middleware.go   | 20 ++++++
 pkg/users/middleware/caching_middleware.go    | 21 +++++--
 .../middleware/error_logging_middleware.go    | 10 +++
 pkg/users/middleware/logging_middleware.go    | 14 +++++
 pkg/users/middleware/recovering_middleware.go | 12 ++++
 pkg/users/middleware/telemetry_middleware.go  | 41 ++++++++++++
 pkg/users/mocks/Users.go                      | 30 +++++++++
 pkg/users/service.go                          |  1 +
 pkg/users/transport/client.go                 |  9 +++
 pkg/users/transport/endpoints.microgen.go     |  1 +
 pkg/users/transport/exchanges.microgen.go     |  8 +++
 pkg/users/transport/grpc/client.go            |  1 +
 pkg/users/transport/grpc/client.microgen.go   |  7 +++
 .../protobuf_endpoint_converters.microgen.go  | 40 ++++++++++++
 pkg/users/transport/grpc/server.go            |  1 +
 pkg/users/transport/grpc/server.microgen.go   | 15 +++++
 pkg/users/transport/server.microgen.go        |  9 +++
 25 files changed, 365 insertions(+), 25 deletions(-)
 create mode 100644 pkg/auth/factory_internal_test.go

diff --git a/go.mod b/go.mod
index 1f94424c..8fb26873 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
 	github.com/bep/gowebp v0.4.0
 	github.com/expr-lang/expr v1.16.9
 	github.com/go-kit/kit v0.13.0
+	github.com/golang-jwt/jwt/v5 v5.2.1
 	github.com/gosimple/slug v1.14.0
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hashicorp/golang-lru/v2 v2.0.7
diff --git a/go.sum b/go.sum
index 4b2f278b..737f85da 100644
--- a/go.sum
+++ b/go.sum
@@ -23,6 +23,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
diff --git a/logs/zap/example_test.go b/logs/zap/example_test.go
index dc182fb0..6ab0f02d 100644
--- a/logs/zap/example_test.go
+++ b/logs/zap/example_test.go
@@ -61,7 +61,7 @@ func TestExample(t *testing.T) {
 		Once()
 
 	usersService := &usersmocks.Users{}
-	usersService.On("GetByIdentity", mock.Anything, "74d90aaf").Return(user, nil).Once()
+	usersService.On("Login", mock.Anything, "74d90aaf", mock.Anything).Return(user, nil).Once()
 
 	factory := auth.PrincipalFactory{Users: usersService}
 
diff --git a/pkg/auth/context.go b/pkg/auth/context.go
index d4476810..a7d78b33 100644
--- a/pkg/auth/context.go
+++ b/pkg/auth/context.go
@@ -25,3 +25,17 @@ func WithPrincipal(ctx context.Context, p Principal) context.Context {
 func WithSystem(ctx context.Context) context.Context {
 	return WithPrincipal(ctx, &SystemPrincipal{})
 }
+
+type authToken struct{}
+
+func GetAuthToken(ctx context.Context) string {
+	t, _ := ctx.Value(authToken{}).(string)
+	return t
+}
+
+func WithAuthToken(ctx context.Context, token string) context.Context {
+	if ctx == nil {
+		ctx = context.Background()
+	}
+	return context.WithValue(ctx, authToken{}, token)
+}
diff --git a/pkg/auth/factory.go b/pkg/auth/factory.go
index 2394c62a..f1086b5f 100644
--- a/pkg/auth/factory.go
+++ b/pkg/auth/factory.go
@@ -1,15 +1,18 @@
 package auth
 
 import (
+	"fmt"
 	"strings"
 
 	"git.perx.ru/perxis/perxis-go/pkg/clients"
 	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
 	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
 	"git.perx.ru/perxis/perxis-go/pkg/members"
 	"git.perx.ru/perxis/perxis-go/pkg/roles"
 	"git.perx.ru/perxis/perxis-go/pkg/spaces"
 	"git.perx.ru/perxis/perxis-go/pkg/users"
+	"github.com/golang-jwt/jwt/v5"
 )
 
 type PrincipalFactory struct {
@@ -22,9 +25,31 @@ type PrincipalFactory struct {
 	environments.Environments
 }
 
-func (f PrincipalFactory) User(identity string) Principal {
-	return &UserPrincipal{
-		identity:      identity,
+func getValueFromToken(tokenString, name string) (string, error) {
+	var value string
+
+	t := strings.Split(tokenString, "Bearer ")
+	if len(t) == 2 { //nolint:mnd //not mnd
+		tokenString = t[1]
+	}
+	// Используем ParseUnverified, так как считаем, что токен был уже проверен до получения
+	token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
+	if err != nil {
+		return "", err
+	}
+	if claims, ok := token.Claims.(jwt.MapClaims); ok {
+		value = fmt.Sprint(claims[name])
+	}
+	if value == "" {
+		return "", errors.New("invalid token payload")
+	}
+
+	return value, nil
+}
+
+func (f PrincipalFactory) User(identity ...string) Principal {
+	p := &UserPrincipal{
+		identity:      identity[0],
 		users:         f.Users,
 		members:       f.Members,
 		roles:         f.Roles,
@@ -32,6 +57,12 @@ func (f PrincipalFactory) User(identity string) Principal {
 		spaces:        f.Spaces,
 		environments:  f.Environments,
 	}
+
+	if len(identity) > 1 {
+		p.email = identity[1]
+	}
+
+	return p
 }
 
 func (f PrincipalFactory) Client(param *clients.GetByParams) Principal {
@@ -65,6 +96,11 @@ func (f PrincipalFactory) Principal(principalId string) Principal {
 		return f.Client(&clients.GetByParams{OAuthClientID: strings.TrimSuffix(principalId, "@clients")})
 	case strings.HasPrefix(principalId, "API-Key"):
 		return f.Client(&clients.GetByParams{APIKey: strings.TrimPrefix(principalId, "API-Key ")})
+	case strings.HasPrefix(principalId, "Bearer "):
+		var email string
+		email, _ = getValueFromToken(principalId, "email")
+		principalId, _ = getValueFromToken(principalId, "sub")
+		return f.User(principalId, email)
 	default:
 		return f.User(principalId)
 	}
diff --git a/pkg/auth/factory_internal_test.go b/pkg/auth/factory_internal_test.go
new file mode 100644
index 00000000..bbe79af9
--- /dev/null
+++ b/pkg/auth/factory_internal_test.go
@@ -0,0 +1,62 @@
+package auth
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_getValueFromToken(t *testing.T) {
+	tests := []struct {
+		name        string
+		tokenString string
+		field       string
+		want        string
+		wantErr     bool
+	}{
+		{
+			"With Bearer",
+			"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfaWRlbnRfMiIsImVtYWlsIjoidGVzdEB" +
+				"0ZXN0LnJ1IiwiaWF0IjoxNTE2MjM5MDIyfQ.MLo310mkPmZdJlIRo3POhevFwd-O_UyxE-1opbQMVVs",
+			"email",
+			"test@test.ru",
+			false,
+		},
+		{
+			"Without Bearer",
+			"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfaWRlbnRfMiIsImVtYWlsIjoidGVzdEB0ZXN0Ln" +
+				"J1IiwiaWF0IjoxNTE2MjM5MDIyfQ.MLo310mkPmZdJlIRo3POhevFwd-O_UyxE-1opbQMVVs",
+			"email",
+			"test@test.ru",
+			false,
+		},
+		{
+			"Sub",
+			"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfaWRlbnRfMiIsImVtYWlsIjoidGVzdEB0ZXN0Ln" +
+				"J1IiwiaWF0IjoxNTE2MjM5MDIyfQ.MLo310mkPmZdJlIRo3POhevFwd-O_UyxE-1opbQMVVs",
+			"sub",
+			"usr_ident_2",
+			false,
+		},
+		{
+			"Invalid token",
+			"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.zdWIiOiJ1c3JfaWRlbnRfMiIsImVtYWlsIjoidGVzdEB0ZXN0LnJ1I" +
+				"iwiaWF0IjoxNTE2MjM5MDIyfQ.MLo310mkPmZdJlIRo3POhevFwd-O_UyxE-1opbQMVVs",
+			"email",
+			"test@test.ru",
+			true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := getValueFromToken(tt.tokenString, tt.field)
+			if !tt.wantErr {
+				require.NoError(t, err)
+				assert.Equal(t, tt.want, got)
+			} else {
+				assert.Error(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/auth/grpc.go b/pkg/auth/grpc.go
index a947a33b..508d5480 100644
--- a/pkg/auth/grpc.go
+++ b/pkg/auth/grpc.go
@@ -16,9 +16,8 @@ import (
 )
 
 const (
-	OAuth2IdentityMetadata = "x-perxis-identity"
-	TLSIdentityMetadata    = "x-forwarded-client-cert"
-	AccessMetadata         = "x-perxis-access"
+	TLSIdentityMetadata = "x-forwarded-client-cert"
+	AccessMetadata      = "x-perxis-access"
 
 	AuthorizationMetadata = "authorization"
 )
@@ -28,15 +27,9 @@ func GRPCToContext(factory *PrincipalFactory) kitgrpc.ServerRequestFunc {
 		if identity := md.Get(TLSIdentityMetadata); len(identity) > 0 {
 			return WithPrincipal(ctx, factory.Principal(identity[0]))
 		}
-
-		if identity := md.Get(OAuth2IdentityMetadata); len(identity) > 0 {
-			return WithPrincipal(ctx, factory.Principal(identity[0]))
+		if token := md.Get(AuthorizationMetadata); len(token) > 0 {
+			return WithPrincipal(WithAuthToken(ctx, token[0]), factory.Principal(token[0]))
 		}
-
-		if identity := md.Get(AuthorizationMetadata); len(identity) > 0 {
-			return WithPrincipal(ctx, factory.Principal(identity[0]))
-		}
-
 		if access := md.Get(AccessMetadata); len(access) > 0 {
 			return WithPrincipal(ctx, factory.System())
 		}
@@ -51,19 +44,19 @@ func ContextToGRPC() kitgrpc.ClientRequestFunc {
 
 		switch p := p.(type) {
 		case *UserPrincipal:
-			if p.GetIdentity(ctx) != "" {
-				(*md)[OAuth2IdentityMetadata] = []string{p.GetIdentity(ctx)}
+			(*md)[AuthorizationMetadata] = []string{p.GetIdentity(ctx)}
+			if GetAuthToken(ctx) != "" {
+				(*md)[AuthorizationMetadata] = []string{GetAuthToken(ctx)}
 			}
 		case *ClientPrincipal:
 			if ident := p.GetIdentity(ctx); ident != nil {
 				switch {
 				case ident.OAuthClientID != "":
-					(*md)[OAuth2IdentityMetadata] = []string{ident.OAuthClientID + "@clients"}
+					(*md)[AuthorizationMetadata] = []string{ident.OAuthClientID + "@clients"}
 				case ident.TLSSubject != "":
 					(*md)[TLSIdentityMetadata] = []string{ident.TLSSubject}
 				case ident.APIKey != "":
 					(*md)[AuthorizationMetadata] = []string{"API-Key " + ident.APIKey}
-
 				}
 			}
 		case *SystemPrincipal:
diff --git a/pkg/auth/user.go b/pkg/auth/user.go
index 29e87945..37d32f1b 100644
--- a/pkg/auth/user.go
+++ b/pkg/auth/user.go
@@ -18,6 +18,7 @@ import (
 type UserPrincipal struct {
 	id       string
 	identity string
+	email    string
 
 	user    *users.User
 	invalid bool
@@ -128,8 +129,7 @@ func (u *UserPrincipal) User(ctx context.Context) *users.User {
 	case u.id != "":
 		user, err = u.users.Get(WithSystem(ctx), u.id)
 	case u.identity != "":
-		ctx = WithSystem(ctx)
-		user, err = u.users.GetByIdentity(WithSystem(ctx), u.identity)
+		user, err = u.users.Login(WithSystem(ctx), u.identity, u.email)
 	}
 
 	if err != nil || user == nil {
diff --git a/pkg/users/middleware/access_logging_middleware.go b/pkg/users/middleware/access_logging_middleware.go
index fa830272..8875212d 100644
--- a/pkg/users/middleware/access_logging_middleware.go
+++ b/pkg/users/middleware/access_logging_middleware.go
@@ -128,6 +128,26 @@ func (m *accessLoggingMiddleware) GetByIdentity(ctx context.Context, identity st
 	return user, err
 }
 
+func (m *accessLoggingMiddleware) Login(ctx context.Context, identity string, email string) (user *users.User, err error) {
+	begin := time.Now()
+
+	m.logger.Debug("Login.Request",
+		zap.Reflect("principal", auth.GetPrincipal(ctx)),
+		zap.Reflect("identity", identity),
+		zap.Reflect("email", email),
+	)
+
+	user, err = m.next.Login(ctx, identity, email)
+
+	m.logger.Debug("Login.Response",
+		zap.Duration("time", time.Since(begin)),
+		zap.Reflect("user", user),
+		zap.Error(err),
+	)
+
+	return user, err
+}
+
 func (m *accessLoggingMiddleware) Update(ctx context.Context, user *users.User) (err error) {
 	begin := time.Now()
 
diff --git a/pkg/users/middleware/caching_middleware.go b/pkg/users/middleware/caching_middleware.go
index 966da731..6faba7c3 100644
--- a/pkg/users/middleware/caching_middleware.go
+++ b/pkg/users/middleware/caching_middleware.go
@@ -27,7 +27,6 @@ func (m cachingMiddleware) Create(ctx context.Context, create *service.User) (us
 }
 
 func (m cachingMiddleware) Get(ctx context.Context, id string) (user *service.User, err error) {
-
 	value, e := m.cache.Get(id)
 	if e == nil {
 		return value.(*service.User).Clone(), nil
@@ -48,7 +47,6 @@ func (m cachingMiddleware) Find(ctx context.Context, filter *service.Filter, opt
 }
 
 func (m cachingMiddleware) Update(ctx context.Context, update *service.User) (err error) {
-
 	err = m.next.Update(ctx, update)
 	value, e := m.cache.Get(update.ID)
 	if err == nil && e == nil {
@@ -62,7 +60,6 @@ func (m cachingMiddleware) Update(ctx context.Context, update *service.User) (er
 }
 
 func (m cachingMiddleware) Delete(ctx context.Context, id string) (err error) {
-
 	err = m.next.Delete(ctx, id)
 	value, e := m.cache.Get(id)
 	if err == nil && e == nil {
@@ -76,7 +73,6 @@ func (m cachingMiddleware) Delete(ctx context.Context, id string) (err error) {
 }
 
 func (m cachingMiddleware) GetByIdentity(ctx context.Context, identity string) (user *service.User, err error) {
-
 	value, e := m.cache.Get(identity)
 	if e == nil {
 		return value.(*service.User).Clone(), nil
@@ -91,3 +87,20 @@ func (m cachingMiddleware) GetByIdentity(ctx context.Context, identity string) (
 	}
 	return nil, err
 }
+
+//nolint:nonamedreturns //generated
+func (m cachingMiddleware) Login(ctx context.Context, identity string, email string) (user *service.User, err error) {
+	value, e := m.cache.Get(identity)
+	if e == nil {
+		return value.(*service.User).Clone(), nil //nolint:errcheck //generated
+	}
+	user, err = m.next.Login(ctx, identity, email)
+	if err == nil {
+		_ = m.cache.Set(user.ID, user)
+		for _, i := range user.Identities {
+			_ = m.cache.Set(i, user)
+		}
+		return user.Clone(), nil
+	}
+	return nil, err
+}
diff --git a/pkg/users/middleware/error_logging_middleware.go b/pkg/users/middleware/error_logging_middleware.go
index b8abb51a..8355e238 100644
--- a/pkg/users/middleware/error_logging_middleware.go
+++ b/pkg/users/middleware/error_logging_middleware.go
@@ -80,6 +80,16 @@ func (m *errorLoggingMiddleware) GetByIdentity(ctx context.Context, identity str
 	return m.next.GetByIdentity(ctx, identity)
 }
 
+func (m *errorLoggingMiddleware) Login(ctx context.Context, identity string, email string) (user *users.User, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Login(ctx, identity, email)
+}
+
 func (m *errorLoggingMiddleware) Update(ctx context.Context, user *users.User) (err error) {
 	logger := m.logger
 	defer func() {
diff --git a/pkg/users/middleware/logging_middleware.go b/pkg/users/middleware/logging_middleware.go
index f6a0b9b6..fa0137f3 100644
--- a/pkg/users/middleware/logging_middleware.go
+++ b/pkg/users/middleware/logging_middleware.go
@@ -119,3 +119,17 @@ func (m *loggingMiddleware) GetByIdentity(ctx context.Context, identity string)
 	}
 	return user, err
 }
+
+//nolint:nonamedreturns //generated
+func (m *loggingMiddleware) Login(ctx context.Context, identity string, email string) (user *users.User, err error) {
+	logger := m.logger.With(
+		logzap.Caller(ctx),
+		logzap.Object(pkgId.NewUserId(identity)),
+	)
+
+	user, err = m.next.Login(ctx, identity, email)
+	if err != nil {
+		logger.Error("Failed to login", zap.Error(err))
+	}
+	return user, err
+}
diff --git a/pkg/users/middleware/recovering_middleware.go b/pkg/users/middleware/recovering_middleware.go
index e58c5b91..e3d196eb 100644
--- a/pkg/users/middleware/recovering_middleware.go
+++ b/pkg/users/middleware/recovering_middleware.go
@@ -91,6 +91,18 @@ func (m *recoveringMiddleware) GetByIdentity(ctx context.Context, identity strin
 	return m.next.GetByIdentity(ctx, identity)
 }
 
+func (m *recoveringMiddleware) Login(ctx context.Context, identity string, email string) (user *users.User, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Login(ctx, identity, email)
+}
+
 func (m *recoveringMiddleware) Update(ctx context.Context, user *users.User) (err error) {
 	logger := m.logger
 	defer func() {
diff --git a/pkg/users/middleware/telemetry_middleware.go b/pkg/users/middleware/telemetry_middleware.go
index 25088bfd..d122366e 100644
--- a/pkg/users/middleware/telemetry_middleware.go
+++ b/pkg/users/middleware/telemetry_middleware.go
@@ -254,6 +254,47 @@ func (_d telemetryMiddleware) GetByIdentity(ctx context.Context, identity string
 	return user, err
 }
 
+// Login implements users.Users
+func (_d telemetryMiddleware) Login(ctx context.Context, identity string, email string) (user *users.User, err error) {
+	var att = []attribute.KeyValue{
+		attribute.String("service", "Users"),
+		attribute.String("method", "Login"),
+	}
+	attributes := otelmetric.WithAttributeSet(attribute.NewSet(att...))
+
+	start := time.Now()
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Users.Login")
+	defer _span.End()
+
+	user, err = _d.Users.Login(ctx, identity, email)
+
+	_d.requestMetrics.DurationMilliseconds.Record(ctx, time.Since(start).Milliseconds(), attributes)
+
+	caller, _ := pkgId.NewObjectId(auth.GetPrincipal(ctx))
+	if caller != nil {
+		att = append(att, attribute.String("caller", caller.String()))
+	}
+
+	_d.requestMetrics.Total.Add(ctx, 1, otelmetric.WithAttributeSet(attribute.NewSet(att...)))
+
+	if _d._spanDecorator != nil {
+		_d._spanDecorator(_span, map[string]interface{}{
+			"ctx":      ctx,
+			"identity": identity,
+			"email":    email}, map[string]interface{}{
+			"user": user,
+			"err":  err})
+	} else if err != nil {
+		_d.requestMetrics.FailedTotal.Add(ctx, 1, attributes)
+
+		_span.RecordError(err)
+		_span.SetAttributes(attribute.String("event", "error"))
+		_span.SetAttributes(attribute.String("message", err.Error()))
+	}
+
+	return user, err
+}
+
 // Update implements users.Users
 func (_d telemetryMiddleware) Update(ctx context.Context, user *users.User) (err error) {
 	var att = []attribute.KeyValue{
diff --git a/pkg/users/mocks/Users.go b/pkg/users/mocks/Users.go
index 989c6e68..ba8feef5 100644
--- a/pkg/users/mocks/Users.go
+++ b/pkg/users/mocks/Users.go
@@ -161,6 +161,36 @@ func (_m *Users) GetByIdentity(ctx context.Context, identity string) (*users.Use
 	return r0, r1
 }
 
+// Login provides a mock function with given fields: ctx, identity, email
+func (_m *Users) Login(ctx context.Context, identity string, email string) (*users.User, error) {
+	ret := _m.Called(ctx, identity, email)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Login")
+	}
+
+	var r0 *users.User
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) (*users.User, error)); ok {
+		return rf(ctx, identity, email)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) *users.User); ok {
+		r0 = rf(ctx, identity, email)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*users.User)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
+		r1 = rf(ctx, identity, email)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
 // Update provides a mock function with given fields: ctx, user
 func (_m *Users) Update(ctx context.Context, user *users.User) error {
 	ret := _m.Called(ctx, user)
diff --git a/pkg/users/service.go b/pkg/users/service.go
index a244c517..9f839c3a 100644
--- a/pkg/users/service.go
+++ b/pkg/users/service.go
@@ -16,6 +16,7 @@ type Users interface {
 	Update(ctx context.Context, user *User) (err error)
 	Delete(ctx context.Context, id string) (err error)
 	GetByIdentity(ctx context.Context, identity string) (user *User, err error)
+	Login(ctx context.Context, identity string, email string) (user *User, err error)
 }
 
 type Filter struct {
diff --git a/pkg/users/transport/client.go b/pkg/users/transport/client.go
index 01537b11..c06e4add 100644
--- a/pkg/users/transport/client.go
+++ b/pkg/users/transport/client.go
@@ -65,3 +65,12 @@ func (set EndpointsSet) GetByIdentity(arg0 context.Context, arg1 string) (res0 *
 	}
 	return response.(*GetByIdentityResponse).User, res1
 }
+
+func (set EndpointsSet) Login(arg0 context.Context, arg1 string, arg2 string) (res0 *users.User, res1 error) {
+	request := LoginRequest{Identity: arg1, Email: arg2}
+	response, res1 := set.LoginEndpoint(arg0, &request)
+	if res1 != nil {
+		return
+	}
+	return response.(*LoginResponse).User, res1
+}
diff --git a/pkg/users/transport/endpoints.microgen.go b/pkg/users/transport/endpoints.microgen.go
index 264025bf..c65d3390 100644
--- a/pkg/users/transport/endpoints.microgen.go
+++ b/pkg/users/transport/endpoints.microgen.go
@@ -12,4 +12,5 @@ type EndpointsSet struct {
 	UpdateEndpoint        endpoint.Endpoint
 	DeleteEndpoint        endpoint.Endpoint
 	GetByIdentityEndpoint endpoint.Endpoint
+	LoginEndpoint         endpoint.Endpoint
 }
diff --git a/pkg/users/transport/exchanges.microgen.go b/pkg/users/transport/exchanges.microgen.go
index f70b8cdf..8a06c45c 100644
--- a/pkg/users/transport/exchanges.microgen.go
+++ b/pkg/users/transport/exchanges.microgen.go
@@ -49,4 +49,12 @@ type (
 	GetByIdentityResponse struct {
 		User *users.User `json:"user"`
 	}
+
+	LoginRequest struct {
+		Identity string `json:"identity"`
+		Email    string `json:"email"`
+	}
+	LoginResponse struct {
+		User *users.User `json:"user"`
+	}
 )
diff --git a/pkg/users/transport/grpc/client.go b/pkg/users/transport/grpc/client.go
index 7364d5fc..7ead5f93 100644
--- a/pkg/users/transport/grpc/client.go
+++ b/pkg/users/transport/grpc/client.go
@@ -18,5 +18,6 @@ func NewClient(conn *grpc.ClientConn, opts ...grpckit.ClientOption) transport.En
 		GetByIdentityEndpoint: grpcerr.ClientMiddleware(c.GetByIdentityEndpoint),
 		GetEndpoint:           grpcerr.ClientMiddleware(c.GetEndpoint),
 		UpdateEndpoint:        grpcerr.ClientMiddleware(c.UpdateEndpoint),
+		LoginEndpoint:         grpcerr.ClientMiddleware(c.LoginEndpoint),
 	}
 }
diff --git a/pkg/users/transport/grpc/client.microgen.go b/pkg/users/transport/grpc/client.microgen.go
index 4fcd2868..433483e0 100644
--- a/pkg/users/transport/grpc/client.microgen.go
+++ b/pkg/users/transport/grpc/client.microgen.go
@@ -50,6 +50,13 @@ func NewGRPCClient(conn *grpc.ClientConn, addr string, opts ...grpckit.ClientOpt
 			pb.GetResponse{},
 			opts...,
 		).Endpoint(),
+		LoginEndpoint: grpckit.NewClient(
+			conn, addr, "Login",
+			_Encode_Login_Request,
+			_Decode_Login_Response,
+			pb.LoginResponse{},
+			opts...,
+		).Endpoint(),
 		UpdateEndpoint: grpckit.NewClient(
 			conn, addr, "Update",
 			_Encode_Update_Request,
diff --git a/pkg/users/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/users/transport/grpc/protobuf_endpoint_converters.microgen.go
index 2b45912b..8c120ca4 100644
--- a/pkg/users/transport/grpc/protobuf_endpoint_converters.microgen.go
+++ b/pkg/users/transport/grpc/protobuf_endpoint_converters.microgen.go
@@ -63,6 +63,14 @@ func _Encode_Update_Request(ctx context.Context, request interface{}) (interface
 	return &pb.UpdateRequest{Update: reqUpdate}, nil
 }
 
+func _Encode_Login_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil LoginRequest")
+	}
+	req := request.(*transport.LoginRequest)
+	return &pb.LoginRequest{Email: req.Email, Identity: req.Identity}, nil
+}
+
 func _Encode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) {
 	if request == nil {
 		return nil, errors.New("nil DeleteRequest")
@@ -95,6 +103,18 @@ func _Encode_Get_Response(ctx context.Context, response interface{}) (interface{
 	return &pb.GetResponse{User: respUser}, nil
 }
 
+func _Encode_Login_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil LoginResponse")
+	}
+	resp := response.(*transport.LoginResponse)
+	respUser, err := PtrUserToProto(resp.User)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.LoginResponse{User: respUser}, nil
+}
+
 func _Encode_Find_Response(ctx context.Context, response interface{}) (interface{}, error) {
 	if response == nil {
 		return nil, errors.New("nil FindResponse")
@@ -165,6 +185,14 @@ func _Decode_Update_Request(ctx context.Context, request interface{}) (interface
 	return &transport.UpdateRequest{Update: reqUpdate}, nil
 }
 
+func _Decode_Login_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil LoginRequest")
+	}
+	req := request.(*pb.LoginRequest)
+	return &transport.LoginRequest{Email: req.Email, Identity: req.Identity}, nil
+}
+
 func _Decode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) {
 	if request == nil {
 		return nil, errors.New("nil DeleteRequest")
@@ -197,6 +225,18 @@ func _Decode_Get_Response(ctx context.Context, response interface{}) (interface{
 	return &transport.GetResponse{User: respUser}, nil
 }
 
+func _Decode_Login_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil LoginResponse")
+	}
+	resp := response.(*pb.LoginResponse)
+	respUser, err := ProtoToPtrUser(resp.User)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.LoginResponse{User: respUser}, nil
+}
+
 func _Decode_Find_Response(ctx context.Context, response interface{}) (interface{}, error) {
 	if response == nil {
 		return nil, errors.New("nil FindResponse")
diff --git a/pkg/users/transport/grpc/server.go b/pkg/users/transport/grpc/server.go
index 07211008..d14583cc 100644
--- a/pkg/users/transport/grpc/server.go
+++ b/pkg/users/transport/grpc/server.go
@@ -17,6 +17,7 @@ func NewServer(svc users.Users, opts ...grpckit.ServerOption) pb.UsersServer {
 		GetByIdentityEndpoint: grpcerr.ServerMiddleware(eps.GetByIdentityEndpoint),
 		GetEndpoint:           grpcerr.ServerMiddleware(eps.GetEndpoint),
 		UpdateEndpoint:        grpcerr.ServerMiddleware(eps.UpdateEndpoint),
+		LoginEndpoint:         grpcerr.ServerMiddleware(eps.LoginEndpoint),
 	}
 	return NewGRPCServer(&eps, opts...)
 }
diff --git a/pkg/users/transport/grpc/server.microgen.go b/pkg/users/transport/grpc/server.microgen.go
index 8817f6d8..678f6ef1 100644
--- a/pkg/users/transport/grpc/server.microgen.go
+++ b/pkg/users/transport/grpc/server.microgen.go
@@ -18,6 +18,7 @@ type usersServer struct {
 	update        grpc.Handler
 	delete        grpc.Handler
 	getByIdentity grpc.Handler
+	login         grpc.Handler
 
 	pb.UnimplementedUsersServer
 }
@@ -54,6 +55,12 @@ func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption)
 			_Encode_GetByIdentity_Response,
 			opts...,
 		),
+		login: grpc.NewServer(
+			endpoints.LoginEndpoint,
+			_Decode_Login_Request,
+			_Encode_Login_Response,
+			opts...,
+		),
 		update: grpc.NewServer(
 			endpoints.UpdateEndpoint,
 			_Decode_Update_Request,
@@ -110,3 +117,11 @@ func (S *usersServer) GetByIdentity(ctx context.Context, req *pb.GetByIdentityRe
 	}
 	return resp.(*pb.GetByIdentityResponse), nil
 }
+
+func (S *usersServer) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
+	_, resp, err := S.login.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.LoginResponse), nil
+}
diff --git a/pkg/users/transport/server.microgen.go b/pkg/users/transport/server.microgen.go
index e12645ef..6f5c3727 100644
--- a/pkg/users/transport/server.microgen.go
+++ b/pkg/users/transport/server.microgen.go
@@ -16,6 +16,7 @@ func Endpoints(svc users.Users) EndpointsSet {
 		FindEndpoint:          FindEndpoint(svc),
 		GetByIdentityEndpoint: GetByIdentityEndpoint(svc),
 		GetEndpoint:           GetEndpoint(svc),
+		LoginEndpoint:         LoginEndpoint(svc),
 		UpdateEndpoint:        UpdateEndpoint(svc),
 	}
 }
@@ -70,3 +71,11 @@ func GetByIdentityEndpoint(svc users.Users) endpoint.Endpoint {
 		return &GetByIdentityResponse{User: res0}, res1
 	}
 }
+
+func LoginEndpoint(svc users.Users) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*LoginRequest)
+		res0, res1 := svc.Login(arg0, req.Identity, req.Email)
+		return &LoginResponse{User: res0}, res1
+	}
+}
-- 
GitLab