From e22689c125ab3338b18e15ee87a78ecd50e6b430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 20 Dec 2023 14:21:08 +0200 Subject: [PATCH] feat(oidc): id token for device authorization (#7088) * cleanup todo * pass id token details to oidc * feat(oidc): id token for device authorization This changes updates to the newest oidc version, so the Device Authorization grant can return ID tokens when the scope `openid` is set. There is also some refactoring done, so that the eventstore can be queried directly when polling for state. The projection is cleaned up to a minimum with only data required for the login UI. * try to be explicit wit hthe timezone to fix github * pin oidc v3.8.0 * remove TBD entry --- go.mod | 4 +- go.sum | 10 +- internal/api/oidc/device_auth.go | 56 +---- internal/api/ui/login/device_auth.go | 15 +- internal/command/command.go | 2 + internal/command/device_auth.go | 48 +--- internal/command/device_auth_model.go | 32 ++- internal/command/device_auth_test.go | 224 ++++------------- internal/command/main_test.go | 2 + internal/domain/auth_request.go | 28 +++ internal/domain/auth_request_test.go | 108 ++++++++ internal/domain/device_auth.go | 26 +- internal/domain/device_auth_test.go | 10 +- internal/domain/deviceauthstate_string.go | 4 +- internal/domain/request.go | 4 +- internal/query/device_auth.go | 156 ++++++------ internal/query/device_auth_model.go | 58 +++++ internal/query/device_auth_test.go | 234 +++++++++++++----- internal/query/projection/device_auth.go | 151 ++++------- internal/query/query.go | 2 + internal/query/query_test.go | 2 + internal/repository/deviceauth/constraints.go | 14 +- internal/repository/deviceauth/device_auth.go | 44 +--- internal/repository/deviceauth/eventstore.go | 9 + internal/repository/org/eventstore.go | 7 +- 25 files changed, 629 insertions(+), 621 deletions(-) create mode 100644 internal/domain/auth_request_test.go create mode 100644 internal/query/device_auth_model.go create mode 100644 internal/repository/deviceauth/eventstore.go diff --git a/go.mod b/go.mod index 07c63754c..d316c9ebc 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 github.com/zitadel/logging v0.5.0 - github.com/zitadel/oidc/v3 v3.5.0 + github.com/zitadel/oidc/v3 v3.8.0 github.com/zitadel/passwap v0.4.0 github.com/zitadel/saml v0.1.3 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 @@ -155,7 +155,7 @@ require ( github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/uuid v1.4.0 + github.com/google/uuid v1.5.0 github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect diff --git a/go.sum b/go.sum index 4bb3a5fd5..c7da6b8f9 100644 --- a/go.sum +++ b/go.sum @@ -410,8 +410,8 @@ github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -867,12 +867,10 @@ github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA= github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= -github.com/zitadel/oidc/v3 v3.5.0 h1:z51AN6FPo5UuwYJ1r9nLvHlxpTGYd8QXg5MrtYm/dgM= -github.com/zitadel/oidc/v3 v3.5.0/go.mod h1:R8sF5DPR98QQnOoyySsaNqI4NcF/VFMkf/XoYiBUuXQ= +github.com/zitadel/oidc/v3 v3.8.0 h1:4Nvok+e6o3FDpqrf14JOg4EVBvwXNFOI1lFHPZU75iA= +github.com/zitadel/oidc/v3 v3.8.0/go.mod h1:v+aHyg4lBAUuuUHINwXqHtKunPJZo8kPvMpRRBYEKHY= github.com/zitadel/passwap v0.4.0 h1:cMaISx+Ve7ilgG7Q8xOli4Z6IWr8Gndss+jeBk5A3O0= github.com/zitadel/passwap v0.4.0/go.mod h1:yHaDM4A68yRkdic5BZ4iUNoc19hT+kYt8n1/Nz+I87g= -github.com/zitadel/saml v0.1.2 h1:RICwNTuP2upX4A1sZ8iq1rv4/x3DhZHzFx1e5bTKoTo= -github.com/zitadel/saml v0.1.2/go.mod h1:M+X+3vMUulpoLofKeH/W1/qjQQ3owitc2GuGDu3oYpM= github.com/zitadel/saml v0.1.3 h1:LI4DOCVyyU1qKPkzs3vrGcA5J3H4pH3+CL9zr9ShkpM= github.com/zitadel/saml v0.1.3/go.mod h1:MdkjyU3mwnTuh4lNnhPG+RyZL/VfzD72wUG/eWWBaXc= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= diff --git a/internal/api/oidc/device_auth.go b/internal/api/oidc/device_auth.go index 42747ca49..775ec6f9e 100644 --- a/internal/api/oidc/device_auth.go +++ b/internal/api/oidc/device_auth.go @@ -5,11 +5,11 @@ import ( "time" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -78,47 +78,39 @@ func (o *OPStorage) StoreDeviceAuthorization(ctx context.Context, clientID, devi span.EndWithError(err) }() - // TODO(muhlemmer): Remove the following code block with oidc v3 - // https://github.com/zitadel/oidc/issues/370 - client, err := o.GetClientByClientID(ctx, clientID) - if err != nil { - return err - } - if !op.ValidateGrantType(client, oidc.GrantTypeDeviceCode) { - return zerrors.ThrowPermissionDeniedf(nil, "OIDC-et1Ae", "grant type %q not allowed for client", oidc.GrantTypeDeviceCode) - } - scopes, err = o.assertProjectRoleScopes(ctx, clientID, scopes) if err != nil { return zerrors.ThrowPreconditionFailed(err, "OIDC-She4t", "Errors.Internal") } - aggrID, details, err := o.command.AddDeviceAuth(ctx, clientID, deviceCode, userCode, expires, scopes) + details, err := o.command.AddDeviceAuth(ctx, clientID, deviceCode, userCode, expires, scopes) if err == nil { - logger.SetFields("aggregate_id", aggrID, "details", details).Debug(logMsg) + logger.SetFields("details", details).Debug(logMsg) } return err } -func newDeviceAuthorizationState(d *domain.DeviceAuth) *op.DeviceAuthorizationState { +func newDeviceAuthorizationState(d *query.DeviceAuth) *op.DeviceAuthorizationState { return &op.DeviceAuthorizationState{ ClientID: d.ClientID, Scopes: d.Scopes, Expires: d.Expires, Done: d.State.Done(), - Subject: d.Subject, Denied: d.State.Denied(), + Subject: d.Subject, + AMR: AuthMethodTypesToAMR(d.UserAuthMethods), + AuthTime: d.AuthTime, } } -// GetDeviceAuthorizatonState retieves the current state of the Device Authorization process. +// GetDeviceAuthorizatonState retrieves the current state of the Device Authorization process. // It implements the [op.DeviceAuthorizationStorage] interface and is used by devices that // are polling until they successfully receive a token or we indicate a denied or expired state. // As generated user codes are of low entropy, this implementation also takes care or // device authorization request cleanup, when it has been Approved, Denied or Expired. func (o *OPStorage) GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (state *op.DeviceAuthorizationState, err error) { const logMsg = "get device authorization state" - logger := logging.WithFields("client_id", clientID, "device_code", deviceCode) + logger := logging.WithFields("device_code", deviceCode) ctx, span := tracing.NewSpan(ctx) defer func() { @@ -128,7 +120,7 @@ func (o *OPStorage) GetDeviceAuthorizatonState(ctx context.Context, clientID, de span.EndWithError(err) }() - deviceAuth, err := o.query.DeviceAuthByDeviceCode(ctx, clientID, deviceCode) + deviceAuth, err := o.query.DeviceAuthByDeviceCode(ctx, deviceCode) if err != nil { return nil, err } @@ -139,38 +131,12 @@ func (o *OPStorage) GetDeviceAuthorizatonState(ctx context.Context, clientID, de // Cancel the request if it is expired, only if it wasn't Done meanwhile if !deviceAuth.State.Done() && deviceAuth.Expires.Before(time.Now()) { - _, err = o.command.CancelDeviceAuth(ctx, deviceAuth.AggregateID, domain.DeviceAuthCanceledExpired) + _, err = o.command.CancelDeviceAuth(ctx, deviceAuth.DeviceCode, domain.DeviceAuthCanceledExpired) if err != nil { return nil, err } deviceAuth.State = domain.DeviceAuthStateExpired } - // When the request is more then initiated, it has been either Approved, Denied or Expired. - // At this point we should remove it from the DB to avoid user code conflicts. - if deviceAuth.State > domain.DeviceAuthStateInitiated { - _, err = o.command.RemoveDeviceAuth(ctx, deviceAuth.AggregateID) - if err != nil { - return nil, err - } - } - return newDeviceAuthorizationState(deviceAuth), nil } - -// TODO(muhlemmer): remove the following methods with oidc v3. -// They are actually not used, but are required by the oidc device storage interface. -// https://github.com/zitadel/oidc/issues/371 -func (o *OPStorage) GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error) { - return nil, nil -} - -func (o *OPStorage) CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) (err error) { - return nil -} - -func (o *OPStorage) DenyDeviceAuthorization(ctx context.Context, userCode string) (err error) { - return nil -} - -// TODO end. diff --git a/internal/api/ui/login/device_auth.go b/internal/api/ui/login/device_auth.go index 150843dea..2135772e4 100644 --- a/internal/api/ui/login/device_auth.go +++ b/internal/api/ui/login/device_auth.go @@ -100,7 +100,7 @@ func (l *Login) handleDeviceAuthUserCode(w http.ResponseWriter, r *http.Request) l.renderDeviceAuthUserCode(w, r, err) return } - deviceAuth, err := l.query.DeviceAuthByUserCode(ctx, userCode) + deviceAuthReq, err := l.query.DeviceAuthRequestByUserCode(ctx, userCode) if err != nil { l.renderDeviceAuthUserCode(w, r, err) return @@ -113,14 +113,9 @@ func (l *Login) handleDeviceAuthUserCode(w http.ResponseWriter, r *http.Request) authRequest, err := l.authRepo.CreateAuthRequest(ctx, &domain.AuthRequest{ CreationDate: time.Now(), AgentID: userAgentID, - ApplicationID: deviceAuth.ClientID, + ApplicationID: deviceAuthReq.ClientID, InstanceID: authz.GetInstance(ctx).InstanceID(), - Request: &domain.AuthRequestDevice{ - ID: deviceAuth.AggregateID, - DeviceCode: deviceAuth.DeviceCode, - UserCode: deviceAuth.UserCode, - Scopes: deviceAuth.Scopes, - }, + Request: deviceAuthReq, }) if err != nil { l.renderDeviceAuthUserCode(w, r, err) @@ -168,9 +163,9 @@ func (l *Login) handleDeviceAuthAction(w http.ResponseWriter, r *http.Request) { action := mux.Vars(r)["action"] switch action { case deviceAuthAllowed: - _, err = l.command.ApproveDeviceAuth(r.Context(), authDev.ID, authReq.UserID) + _, err = l.command.ApproveDeviceAuth(r.Context(), authDev.DeviceCode, authReq.UserID, authReq.UserAuthMethodTypes(), authReq.AuthTime) case deviceAuthDenied: - _, err = l.command.CancelDeviceAuth(r.Context(), authDev.ID, domain.DeviceAuthCanceledDenied) + _, err = l.command.CancelDeviceAuth(r.Context(), authDev.DeviceCode, domain.DeviceAuthCanceledDenied) default: l.renderDeviceAuthAction(w, r, authReq, authDev.Scopes) return diff --git a/internal/command/command.go b/internal/command/command.go index 1cdf544d7..d43bd550d 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -24,6 +24,7 @@ import ( "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/repository/action" "github.com/zitadel/zitadel/internal/repository/authrequest" + "github.com/zitadel/zitadel/internal/repository/deviceauth" "github.com/zitadel/zitadel/internal/repository/feature" "github.com/zitadel/zitadel/internal/repository/idpintent" instance_repo "github.com/zitadel/zitadel/internal/repository/instance" @@ -166,6 +167,7 @@ func StartCommands( oidcsession.RegisterEventMappers(repo.eventstore) milestone.RegisterEventMappers(repo.eventstore) feature.RegisterEventMappers(repo.eventstore) + deviceauth.RegisterEventMappers(repo.eventstore) repo.codeAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost) repo.userPasswordHasher, err = defaults.PasswordHasher.PasswordHasher() diff --git a/internal/command/device_auth.go b/internal/command/device_auth.go index fcbe2fc37..7abe85801 100644 --- a/internal/command/device_auth.go +++ b/internal/command/device_auth.go @@ -11,14 +11,9 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) (string, *domain.ObjectDetails, error) { - aggrID, err := c.idGenerator.Next() - if err != nil { - return "", nil, err - } - - aggr := deviceauth.NewAggregate(aggrID, authz.GetInstance(ctx).InstanceID()) - model := NewDeviceAuthWriteModel(aggrID, aggr.ResourceOwner) +func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) (*domain.ObjectDetails, error) { + aggr := deviceauth.NewAggregate(deviceCode, authz.GetInstance(ctx).InstanceID()) + model := NewDeviceAuthWriteModel(deviceCode, aggr.ResourceOwner) pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewAddedEvent( ctx, @@ -30,18 +25,18 @@ func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, user scopes, )) if err != nil { - return "", nil, err + return nil, err } err = AppendAndReduce(model, pushedEvents...) if err != nil { - return "", nil, err + return nil, err } - return model.AggregateID, writeModelToObjectDetails(&model.WriteModel), nil + return writeModelToObjectDetails(&model.WriteModel), nil } -func (c *Commands) ApproveDeviceAuth(ctx context.Context, id, subject string) (*domain.ObjectDetails, error) { - model, err := c.getDeviceAuthWriteModelByID(ctx, id) +func (c *Commands) ApproveDeviceAuth(ctx context.Context, deviceCode, subject string, authMethods []domain.UserAuthMethodType, authTime time.Time) (*domain.ObjectDetails, error) { + model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, deviceCode) if err != nil { return nil, err } @@ -50,7 +45,7 @@ func (c *Commands) ApproveDeviceAuth(ctx context.Context, id, subject string) (* } aggr := deviceauth.NewAggregate(model.AggregateID, model.InstanceID) - pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, aggr, subject)) + pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, aggr, subject, authMethods, authTime)) if err != nil { return nil, err } @@ -63,7 +58,7 @@ func (c *Commands) ApproveDeviceAuth(ctx context.Context, id, subject string) (* } func (c *Commands) CancelDeviceAuth(ctx context.Context, id string, reason domain.DeviceAuthCanceled) (*domain.ObjectDetails, error) { - model, err := c.getDeviceAuthWriteModelByID(ctx, id) + model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, id) if err != nil { return nil, err } @@ -84,27 +79,8 @@ func (c *Commands) CancelDeviceAuth(ctx context.Context, id string, reason domai return writeModelToObjectDetails(&model.WriteModel), nil } -func (c *Commands) RemoveDeviceAuth(ctx context.Context, id string) (*domain.ObjectDetails, error) { - model, err := c.getDeviceAuthWriteModelByID(ctx, id) - if err != nil { - return nil, err - } - aggr := deviceauth.NewAggregate(model.AggregateID, model.InstanceID) - - pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewRemovedEvent(ctx, aggr, model.ClientID, model.DeviceCode, model.UserCode)) - if err != nil { - return nil, err - } - err = AppendAndReduce(model, pushedEvents...) - if err != nil { - return nil, err - } - - return writeModelToObjectDetails(&model.WriteModel), nil -} - -func (c *Commands) getDeviceAuthWriteModelByID(ctx context.Context, id string) (*DeviceAuthWriteModel, error) { - model := &DeviceAuthWriteModel{WriteModel: eventstore.WriteModel{AggregateID: id}} +func (c *Commands) getDeviceAuthWriteModelByDeviceCode(ctx context.Context, deviceCode string) (*DeviceAuthWriteModel, error) { + model := &DeviceAuthWriteModel{WriteModel: eventstore.WriteModel{AggregateID: deviceCode}} err := c.eventstore.FilterToQueryReducer(ctx, model) if err != nil { return nil, err diff --git a/internal/command/device_auth_model.go b/internal/command/device_auth_model.go index 2ea52a39a..69a457c51 100644 --- a/internal/command/device_auth_model.go +++ b/internal/command/device_auth_model.go @@ -11,19 +11,21 @@ import ( type DeviceAuthWriteModel struct { eventstore.WriteModel - ClientID string - DeviceCode string - UserCode string - Expires time.Time - Scopes []string - Subject string - State domain.DeviceAuthState + ClientID string + DeviceCode string + UserCode string + Expires time.Time + Scopes []string + State domain.DeviceAuthState + Subject string + UserAuthMethods []domain.UserAuthMethodType + AuthTime time.Time } -func NewDeviceAuthWriteModel(aggrID, resourceOwner string) *DeviceAuthWriteModel { +func NewDeviceAuthWriteModel(deviceCode, resourceOwner string) *DeviceAuthWriteModel { return &DeviceAuthWriteModel{ WriteModel: eventstore.WriteModel{ - AggregateID: aggrID, + AggregateID: deviceCode, ResourceOwner: resourceOwner, }, } @@ -40,12 +42,12 @@ func (m *DeviceAuthWriteModel) Reduce() error { m.Scopes = e.Scopes m.State = e.State case *deviceauth.ApprovedEvent: - m.Subject = e.Subject m.State = domain.DeviceAuthStateApproved + m.Subject = e.Subject + m.UserAuthMethods = e.UserAuthMethods + m.AuthTime = e.AuthTime case *deviceauth.CanceledEvent: m.State = e.Reason.State() - case *deviceauth.RemovedEvent: - m.State = domain.DeviceAuthStateRemoved } } @@ -54,8 +56,14 @@ func (m *DeviceAuthWriteModel) Reduce() error { func (m *DeviceAuthWriteModel) Query() *eventstore.SearchQueryBuilder { return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(m.ResourceOwner). AddQuery(). AggregateTypes(deviceauth.AggregateType). AggregateIDs(m.AggregateID). + EventTypes( + deviceauth.AddedEventType, + deviceauth.ApprovedEventType, + deviceauth.CanceledEventType, + ). Builder() } diff --git a/internal/command/device_auth_test.go b/internal/command/device_auth_test.go index a21a64f84..584f44f13 100644 --- a/internal/command/device_auth_test.go +++ b/internal/command/device_auth_test.go @@ -8,29 +8,24 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/id" - id_mock "github.com/zitadel/zitadel/internal/id/mock" "github.com/zitadel/zitadel/internal/repository/deviceauth" "github.com/zitadel/zitadel/internal/zerrors" ) func TestCommands_AddDeviceAuth(t *testing.T) { ctx := authz.WithInstanceID(context.Background(), "instance1") - idErr := errors.New("idErr") pushErr := errors.New("pushErr") now := time.Now() - unique := deviceauth.NewAddUniqueConstraints("client_id", "123", "456") + unique := deviceauth.NewAddUniqueConstraints("123", "456") require.Len(t, unique, 2) type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator + eventstore *eventstore.Eventstore } type args struct { ctx context.Context @@ -44,42 +39,20 @@ func TestCommands_AddDeviceAuth(t *testing.T) { name string fields fields args args - wantID string wantDetails *domain.ObjectDetails wantErr error }{ - { - name: "idGenerator error", - fields: fields{ - eventstore: eventstoreExpect(t), - idGenerator: func() id.Generator { - m := id_mock.NewMockGenerator(gomock.NewController(t)) - m.EXPECT().Next().Return("", idErr) - return m - }(), - }, - args: args{ - ctx: ctx, - clientID: "client_id", - deviceCode: "123", - userCode: "456", - expires: now, - scopes: []string{"a", "b", "c"}, - }, - wantErr: idErr, - }, { name: "success", fields: fields{ eventstore: eventstoreExpect(t, expectPush( deviceauth.NewAddedEvent( ctx, - deviceauth.NewAggregate("1999", "instance1"), + deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, ), )), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "1999"), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instance1"), @@ -89,7 +62,6 @@ func TestCommands_AddDeviceAuth(t *testing.T) { expires: now, scopes: []string{"a", "b", "c"}, }, - wantID: "1999", wantDetails: &domain.ObjectDetails{ ResourceOwner: "instance1", }, @@ -100,12 +72,11 @@ func TestCommands_AddDeviceAuth(t *testing.T) { eventstore: eventstoreExpect(t, expectPushFailed(pushErr, deviceauth.NewAddedEvent( ctx, - deviceauth.NewAggregate("1999", "instance1"), + deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, )), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "1999"), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instance1"), @@ -121,12 +92,10 @@ func TestCommands_AddDeviceAuth(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore, } - gotID, gotDetails, err := c.AddDeviceAuth(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, tt.args.userCode, tt.args.expires, tt.args.scopes) + gotDetails, err := c.AddDeviceAuth(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, tt.args.userCode, tt.args.expires, tt.args.scopes) require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, tt.wantID, gotID) assert.Equal(t, tt.wantDetails, gotDetails) }) } @@ -141,9 +110,11 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - id string - subject string + ctx context.Context + id string + subject string + authMethods []domain.UserAuthMethodType + authTime time.Time } tests := []struct { name string @@ -156,26 +127,14 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { name: "not found error", fields: fields{ eventstore: eventstoreExpect(t, - expectFilter( - eventFromEventPusherWithInstanceID("instance1", - deviceauth.NewAddedEvent( - ctx, - deviceauth.NewAggregate("1999", "instance1"), - "client_id", "123", "456", now, - []string{"a", "b", "c"}, - ), - ), - eventFromEventPusherWithInstanceID("instance1", - deviceauth.NewRemovedEvent( - ctx, - deviceauth.NewAggregate("1999", "instance1"), - "client_id", "123", "456", - ), - ), - ), + expectFilter(), ), }, - args: args{ctx, "1999", "subj"}, + args: args{ + ctx, "123", "subj", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + time.Unix(123, 456), + }, wantErr: zerrors.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound"), }, { @@ -186,19 +145,25 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { "instance1", deviceauth.NewAddedEvent( ctx, - deviceauth.NewAggregate("1999", "instance1"), + deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, ), )), expectPushFailed(pushErr, deviceauth.NewApprovedEvent( - ctx, deviceauth.NewAggregate("1999", "instance1"), "subj", + ctx, deviceauth.NewAggregate("123", "instance1"), "subj", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + time.Unix(123, 456), ), ), ), }, - args: args{ctx, "1999", "subj"}, + args: args{ + ctx, "123", "subj", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + time.Unix(123, 456), + }, wantErr: pushErr, }, { @@ -209,19 +174,25 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { "instance1", deviceauth.NewAddedEvent( ctx, - deviceauth.NewAggregate("1999", "instance1"), + deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, ), )), expectPush( deviceauth.NewApprovedEvent( - ctx, deviceauth.NewAggregate("1999", "instance1"), "subj", + ctx, deviceauth.NewAggregate("123", "instance1"), "subj", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + time.Unix(123, 456), ), ), ), }, - args: args{ctx, "1999", "subj"}, + args: args{ + ctx, "123", "subj", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + time.Unix(123, 456), + }, wantDetails: &domain.ObjectDetails{ ResourceOwner: "instance1", }, @@ -232,7 +203,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore, } - gotDetails, err := c.ApproveDeviceAuth(tt.args.ctx, tt.args.id, tt.args.subject) + gotDetails, err := c.ApproveDeviceAuth(tt.args.ctx, tt.args.id, tt.args.subject, tt.args.authMethods, tt.args.authTime) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, gotDetails, tt.wantDetails) }) @@ -263,26 +234,10 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { name: "not found error", fields: fields{ eventstore: eventstoreExpect(t, - expectFilter( - eventFromEventPusherWithInstanceID("instance1", - deviceauth.NewAddedEvent( - ctx, - deviceauth.NewAggregate("1999", "instance1"), - "client_id", "123", "456", now, - []string{"a", "b", "c"}, - ), - ), - eventFromEventPusherWithInstanceID("instance1", - deviceauth.NewRemovedEvent( - ctx, - deviceauth.NewAggregate("1999", "instance1"), - "client_id", "123", "456", - ), - ), - ), + expectFilter(), ), }, - args: args{ctx, "1999", domain.DeviceAuthCanceledDenied}, + args: args{ctx, "123", domain.DeviceAuthCanceledDenied}, wantErr: zerrors.ThrowNotFound(nil, "COMMAND-gee5A", "Errors.DeviceAuth.NotFound"), }, { @@ -293,20 +248,20 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { "instance1", deviceauth.NewAddedEvent( ctx, - deviceauth.NewAggregate("1999", "instance1"), + deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, ), )), expectPushFailed(pushErr, deviceauth.NewCanceledEvent( - ctx, deviceauth.NewAggregate("1999", "instance1"), + ctx, deviceauth.NewAggregate("123", "instance1"), domain.DeviceAuthCanceledDenied, ), ), ), }, - args: args{ctx, "1999", domain.DeviceAuthCanceledDenied}, + args: args{ctx, "123", domain.DeviceAuthCanceledDenied}, wantErr: pushErr, }, { @@ -317,20 +272,20 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { "instance1", deviceauth.NewAddedEvent( ctx, - deviceauth.NewAggregate("1999", "instance1"), + deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, ), )), expectPush( deviceauth.NewCanceledEvent( - ctx, deviceauth.NewAggregate("1999", "instance1"), + ctx, deviceauth.NewAggregate("123", "instance1"), domain.DeviceAuthCanceledDenied, ), ), ), }, - args: args{ctx, "1999", domain.DeviceAuthCanceledDenied}, + args: args{ctx, "123", domain.DeviceAuthCanceledDenied}, wantDetails: &domain.ObjectDetails{ ResourceOwner: "instance1", }, @@ -343,20 +298,20 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { "instance1", deviceauth.NewAddedEvent( ctx, - deviceauth.NewAggregate("1999", "instance1"), + deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, ), )), expectPush( deviceauth.NewCanceledEvent( - ctx, deviceauth.NewAggregate("1999", "instance1"), + ctx, deviceauth.NewAggregate("123", "instance1"), domain.DeviceAuthCanceledExpired, ), ), ), }, - args: args{ctx, "1999", domain.DeviceAuthCanceledExpired}, + args: args{ctx, "123", domain.DeviceAuthCanceledExpired}, wantDetails: &domain.ObjectDetails{ ResourceOwner: "instance1", }, @@ -373,88 +328,3 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { }) } } - -func TestCommands_RemoveDeviceAuth(t *testing.T) { - ctx := authz.WithInstanceID(context.Background(), "instance1") - now := time.Now() - pushErr := errors.New("pushErr") - - unique := deviceauth.NewRemoveUniqueConstraints("client_id", "123", "456") - require.Len(t, unique, 2) - - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - id string - } - tests := []struct { - name string - fields fields - args args - wantDetails *domain.ObjectDetails - wantErr error - }{ - { - name: "push error", - fields: fields{ - eventstore: eventstoreExpect(t, - expectFilter(eventFromEventPusherWithInstanceID( - "instance1", - deviceauth.NewAddedEvent( - ctx, - deviceauth.NewAggregate("1999", "instance1"), - "client_id", "123", "456", now, - []string{"a", "b", "c"}, - ), - )), - expectPushFailed(pushErr, - deviceauth.NewRemovedEvent( - ctx, deviceauth.NewAggregate("1999", "instance1"), - "client_id", "123", "456", - ), - ), - ), - }, - args: args{ctx, "1999"}, - wantErr: pushErr, - }, - { - name: "success", - fields: fields{ - eventstore: eventstoreExpect(t, - expectFilter(eventFromEventPusherWithInstanceID( - "instance1", - deviceauth.NewAddedEvent( - ctx, - deviceauth.NewAggregate("1999", "instance1"), - "client_id", "123", "456", now, - []string{"a", "b", "c"}, - ), - )), - expectPush( - deviceauth.NewRemovedEvent( - ctx, deviceauth.NewAggregate("1999", "instance1"), - "client_id", "123", "456", - ), - ), - ), - }, - args: args{ctx, "1999"}, - wantDetails: &domain.ObjectDetails{ - ResourceOwner: "instance1", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.fields.eventstore, - } - gotDetails, err := c.RemoveDeviceAuth(tt.args.ctx, tt.args.id) - require.ErrorIs(t, err, tt.wantErr) - assert.Equal(t, gotDetails, tt.wantDetails) - }) - } -} diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 23c1e54a1..649a0085d 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -19,6 +19,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/repository/mock" action_repo "github.com/zitadel/zitadel/internal/repository/action" "github.com/zitadel/zitadel/internal/repository/authrequest" + "github.com/zitadel/zitadel/internal/repository/deviceauth" "github.com/zitadel/zitadel/internal/repository/feature" "github.com/zitadel/zitadel/internal/repository/idpintent" iam_repo "github.com/zitadel/zitadel/internal/repository/instance" @@ -63,6 +64,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore { limits.RegisterEventMappers(es) restrictions.RegisterEventMappers(es) feature.RegisterEventMappers(es) + deviceauth.RegisterEventMappers(es) return es } diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go index 7607495d8..cf406c762 100644 --- a/internal/domain/auth_request.go +++ b/internal/domain/auth_request.go @@ -110,6 +110,23 @@ const ( MFATypeOTPEmail ) +func (m MFAType) UserAuthMethodType() UserAuthMethodType { + switch m { + case MFATypeTOTP: + return UserAuthMethodTypeTOTP + case MFATypeU2F: + return UserAuthMethodTypeU2F + case MFATypeU2FUserVerification: + return UserAuthMethodTypePasswordless + case MFATypeOTPSMS: + return UserAuthMethodTypeOTPSMS + case MFATypeOTPEmail: + return UserAuthMethodTypeOTPEmail + default: + return UserAuthMethodTypeUnspecified + } +} + type MFALevel int const ( @@ -223,3 +240,14 @@ func (a *AuthRequest) PrivateLabelingOrgID(defaultID string) string { } return defaultID } + +func (a *AuthRequest) UserAuthMethodTypes() []UserAuthMethodType { + list := make([]UserAuthMethodType, 0, len(a.MFAsVerified)+1) + if a.PasswordVerified { + list = append(list, UserAuthMethodTypePassword) + } + for _, mfa := range a.MFAsVerified { + list = append(list, mfa.UserAuthMethodType()) + } + return list +} diff --git a/internal/domain/auth_request_test.go b/internal/domain/auth_request_test.go new file mode 100644 index 000000000..0a37e8f9e --- /dev/null +++ b/internal/domain/auth_request_test.go @@ -0,0 +1,108 @@ +package domain + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMFAType_UserAuthMethodType(t *testing.T) { + tests := []struct { + name string + m MFAType + want UserAuthMethodType + }{ + { + name: "totp", + m: MFATypeTOTP, + want: UserAuthMethodTypeTOTP, + }, + { + name: "u2f", + m: MFATypeU2F, + want: UserAuthMethodTypeU2F, + }, + { + name: "passwordless", + m: MFATypeU2FUserVerification, + want: UserAuthMethodTypePasswordless, + }, + { + name: "otp sms", + m: MFATypeOTPSMS, + want: UserAuthMethodTypeOTPSMS, + }, + { + name: "otp email", + m: MFATypeOTPEmail, + want: UserAuthMethodTypeOTPEmail, + }, + { + name: "unspecified", + m: 99, + want: UserAuthMethodTypeUnspecified, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.m.UserAuthMethodType() + assert.Equal(t, tt.want, got) + }) + } +} + +func TestAuthRequest_UserAuthMethodTypes(t *testing.T) { + type fields struct { + PasswordVerified bool + MFAsVerified []MFAType + } + tests := []struct { + name string + fields fields + want []UserAuthMethodType + }{ + { + name: "no auth methods", + fields: fields{ + PasswordVerified: false, + MFAsVerified: nil, + }, + want: []UserAuthMethodType{}, + }, + { + name: "only password", + fields: fields{ + PasswordVerified: true, + MFAsVerified: nil, + }, + want: []UserAuthMethodType{ + UserAuthMethodTypePassword, + }, + }, + { + name: "password, with mfa", + fields: fields{ + PasswordVerified: true, + MFAsVerified: []MFAType{ + MFATypeTOTP, + MFATypeU2F, + }, + }, + want: []UserAuthMethodType{ + UserAuthMethodTypePassword, + UserAuthMethodTypeTOTP, + UserAuthMethodTypeU2F, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &AuthRequest{ + PasswordVerified: tt.fields.PasswordVerified, + MFAsVerified: tt.fields.MFAsVerified, + } + got := a.UserAuthMethodTypes() + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/domain/device_auth.go b/internal/domain/device_auth.go index da076663a..d8e4bb387 100644 --- a/internal/domain/device_auth.go +++ b/internal/domain/device_auth.go @@ -2,28 +2,11 @@ package domain import ( "strconv" - "time" - - "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) -// DeviceAuth describes a Device Authorization request. -// It is used as input and output model in the command and query packages. -type DeviceAuth struct { - models.ObjectRoot - - ClientID string - DeviceCode string - UserCode string - Expires time.Time - Scopes []string - Subject string - State DeviceAuthState -} - // DeviceAuthState describes the step the // the device authorization process is in. -// We generate the Stringer implemntation for pretier +// We generate the Stringer implementation for prettier // log output. // //go:generate stringer -type=DeviceAuthState -linecomment @@ -35,13 +18,14 @@ const ( DeviceAuthStateApproved // approved DeviceAuthStateDenied // denied DeviceAuthStateExpired // expired - DeviceAuthStateRemoved // removed + + deviceAuthStateCount // invalid ) // Exists returns true when not Undefined and -// any status lower than Removed. +// any status lower than deviceAuthStateCount. func (s DeviceAuthState) Exists() bool { - return s > DeviceAuthStateUndefined && s < DeviceAuthStateRemoved + return s > DeviceAuthStateUndefined && s < deviceAuthStateCount } // Done returns true when DeviceAuthState is Approved. diff --git a/internal/domain/device_auth_test.go b/internal/domain/device_auth_test.go index c3fcf359d..bd0bf925e 100644 --- a/internal/domain/device_auth_test.go +++ b/internal/domain/device_auth_test.go @@ -30,7 +30,7 @@ func TestDeviceAuthState_Exists(t *testing.T) { want: true, }, { - s: DeviceAuthStateRemoved, + s: deviceAuthStateCount, want: false, }, } @@ -68,10 +68,6 @@ func TestDeviceAuthState_Done(t *testing.T) { s: DeviceAuthStateExpired, want: false, }, - { - s: DeviceAuthStateRemoved, - want: false, - }, } for _, tt := range tests { t.Run(tt.s.String(), func(t *testing.T) { @@ -108,10 +104,6 @@ func TestDeviceAuthState_Denied(t *testing.T) { s: DeviceAuthStateExpired, want: true, }, - { - s: DeviceAuthStateRemoved, - want: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/domain/deviceauthstate_string.go b/internal/domain/deviceauthstate_string.go index b47a6bc7e..0381287c7 100644 --- a/internal/domain/deviceauthstate_string.go +++ b/internal/domain/deviceauthstate_string.go @@ -13,10 +13,10 @@ func _() { _ = x[DeviceAuthStateApproved-2] _ = x[DeviceAuthStateDenied-3] _ = x[DeviceAuthStateExpired-4] - _ = x[DeviceAuthStateRemoved-5] + _ = x[deviceAuthStateCount-5] } -const _DeviceAuthState_name = "undefinedinitiatedapproveddeniedexpiredremoved" +const _DeviceAuthState_name = "undefinedinitiatedapproveddeniedexpiredinvalid" var _DeviceAuthState_index = [...]uint8{0, 9, 18, 26, 32, 39, 46} diff --git a/internal/domain/request.go b/internal/domain/request.go index 7f9146392..bbacddc75 100644 --- a/internal/domain/request.go +++ b/internal/domain/request.go @@ -59,7 +59,7 @@ func (a *AuthRequestSAML) IsValid() bool { } type AuthRequestDevice struct { - ID string + ClientID string DeviceCode string UserCode string Scopes []string @@ -70,5 +70,5 @@ func (*AuthRequestDevice) Type() AuthRequestType { } func (a *AuthRequestDevice) IsValid() bool { - return a.DeviceCode != "" && a.UserCode != "" && len(a.Scopes) > 0 + return a.DeviceCode != "" && a.UserCode != "" } diff --git a/internal/query/device_auth.go b/internal/query/device_auth.go index 8f1c91458..17e1b8e94 100644 --- a/internal/query/device_auth.go +++ b/internal/query/device_auth.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "time" sq "github.com/Masterminds/squirrel" @@ -16,90 +17,80 @@ import ( ) var ( - deviceAuthTable = table{ - name: projection.DeviceAuthProjectionTable, - instanceIDCol: projection.DeviceAuthColumnInstanceID, + deviceAuthRequestTable = table{ + name: projection.DeviceAuthRequestProjectionTable, + instanceIDCol: projection.DeviceAuthRequestColumnInstanceID, } - DeviceAuthColumnID = Column{ - name: projection.DeviceAuthColumnID, - table: deviceAuthTable, + DeviceAuthRequestColumnClientID = Column{ + name: projection.DeviceAuthRequestColumnClientID, + table: deviceAuthRequestTable, } - DeviceAuthColumnClientID = Column{ - name: projection.DeviceAuthColumnClientID, - table: deviceAuthTable, + DeviceAuthRequestColumnDeviceCode = Column{ + name: projection.DeviceAuthRequestColumnDeviceCode, + table: deviceAuthRequestTable, } - DeviceAuthColumnDeviceCode = Column{ - name: projection.DeviceAuthColumnDeviceCode, - table: deviceAuthTable, + DeviceAuthRequestColumnUserCode = Column{ + name: projection.DeviceAuthRequestColumnUserCode, + table: deviceAuthRequestTable, } - DeviceAuthColumnUserCode = Column{ - name: projection.DeviceAuthColumnUserCode, - table: deviceAuthTable, + DeviceAuthRequestColumnScopes = Column{ + name: projection.DeviceAuthRequestColumnScopes, + table: deviceAuthRequestTable, } - DeviceAuthColumnExpires = Column{ - name: projection.DeviceAuthColumnExpires, - table: deviceAuthTable, + DeviceAuthRequestColumnCreationDate = Column{ + name: projection.DeviceAuthRequestColumnCreationDate, + table: deviceAuthRequestTable, } - DeviceAuthColumnScopes = Column{ - name: projection.DeviceAuthColumnScopes, - table: deviceAuthTable, + DeviceAuthRequestColumnChangeDate = Column{ + name: projection.DeviceAuthRequestColumnChangeDate, + table: deviceAuthRequestTable, } - DeviceAuthColumnState = Column{ - name: projection.DeviceAuthColumnState, - table: deviceAuthTable, + DeviceAuthRequestColumnSequence = Column{ + name: projection.DeviceAuthRequestColumnSequence, + table: deviceAuthRequestTable, } - DeviceAuthColumnSubject = Column{ - name: projection.DeviceAuthColumnSubject, - table: deviceAuthTable, - } - DeviceAuthColumnCreationDate = Column{ - name: projection.DeviceAuthColumnCreationDate, - table: deviceAuthTable, - } - DeviceAuthColumnChangeDate = Column{ - name: projection.DeviceAuthColumnChangeDate, - table: deviceAuthTable, - } - DeviceAuthColumnSequence = Column{ - name: projection.DeviceAuthColumnSequence, - table: deviceAuthTable, - } - DeviceAuthColumnInstanceID = Column{ - name: projection.DeviceAuthColumnInstanceID, - table: deviceAuthTable, + DeviceAuthRequestColumnInstanceID = Column{ + name: projection.DeviceAuthRequestColumnInstanceID, + table: deviceAuthRequestTable, } ) -func (q *Queries) DeviceAuthByDeviceCode(ctx context.Context, clientID, deviceCode string) (deviceAuth *domain.DeviceAuth, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - stmt, scan := prepareDeviceAuthQuery(ctx, q.client) - eq := sq.Eq{ - DeviceAuthColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), - DeviceAuthColumnClientID.identifier(): clientID, - DeviceAuthColumnDeviceCode.identifier(): deviceCode, - } - query, args, err := stmt.Where(eq).ToSql() - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-uk1Oh", "Errors.Query.SQLStatement") - } - - err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - deviceAuth, err = scan(row) - return err - }, query, args...) - return deviceAuth, err +type DeviceAuth struct { + ClientID string + DeviceCode string + UserCode string + Expires time.Time + Scopes []string + State domain.DeviceAuthState + Subject string + UserAuthMethods []domain.UserAuthMethodType + AuthTime time.Time } -func (q *Queries) DeviceAuthByUserCode(ctx context.Context, userCode string) (deviceAuth *domain.DeviceAuth, err error) { +// DeviceAuthByDeviceCode gets the current state of a Device Authorization directly from the eventstore. +func (q *Queries) DeviceAuthByDeviceCode(ctx context.Context, deviceCode string) (deviceAuth *DeviceAuth, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + model := NewDeviceAuthReadModel(deviceCode, authz.GetInstance(ctx).InstanceID()) + if err := q.eventstore.FilterToQueryReducer(ctx, model); err != nil { + return nil, err + } + if !model.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "QUERY-eeR0e", "Errors.DeviceAuth.NotExisting") + } + return &model.DeviceAuth, nil +} + +// DeviceAuthRequestByUserCode finds a Device Authorization request by User-Code from the `device_auth_requests` projection. +func (q *Queries) DeviceAuthRequestByUserCode(ctx context.Context, userCode string) (authReq *domain.AuthRequestDevice, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() stmt, scan := prepareDeviceAuthQuery(ctx, q.client) eq := sq.Eq{ - DeviceAuthColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), - DeviceAuthColumnUserCode.identifier(): userCode, + DeviceAuthRequestColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + DeviceAuthRequestColumnUserCode.identifier(): userCode, } query, args, err := stmt.Where(eq).ToSql() if err != nil { @@ -107,34 +98,32 @@ func (q *Queries) DeviceAuthByUserCode(ctx context.Context, userCode string) (de } err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - deviceAuth, err = scan(row) + authReq, err = scan(row) return err }, query, args...) - return deviceAuth, err + return authReq, err } var deviceAuthSelectColumns = []string{ - DeviceAuthColumnID.identifier(), - DeviceAuthColumnClientID.identifier(), - DeviceAuthColumnScopes.identifier(), - DeviceAuthColumnExpires.identifier(), - DeviceAuthColumnState.identifier(), - DeviceAuthColumnSubject.identifier(), + DeviceAuthRequestColumnClientID.identifier(), + DeviceAuthRequestColumnDeviceCode.identifier(), + DeviceAuthRequestColumnUserCode.identifier(), + DeviceAuthRequestColumnScopes.identifier(), } -func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*domain.DeviceAuth, error)) { - return sq.Select(deviceAuthSelectColumns...).From(deviceAuthTable.identifier()).PlaceholderFormat(sq.Dollar), - func(row *sql.Row) (*domain.DeviceAuth, error) { - dst := new(domain.DeviceAuth) - var scopes database.TextArray[string] +func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*domain.AuthRequestDevice, error)) { + return sq.Select(deviceAuthSelectColumns...).From(deviceAuthRequestTable.identifier()).PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*domain.AuthRequestDevice, error) { + dst := new(domain.AuthRequestDevice) + var ( + scopes database.TextArray[string] + ) err := row.Scan( - &dst.AggregateID, &dst.ClientID, + &dst.DeviceCode, + &dst.UserCode, &scopes, - &dst.Expires, - &dst.State, - &dst.Subject, ) if errors.Is(err, sql.ErrNoRows) { return nil, zerrors.ThrowNotFound(err, "QUERY-Sah9a", "Errors.DeviceAuth.NotExisting") @@ -142,7 +131,6 @@ func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectB if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Voo3o", "Errors.Internal") } - dst.Scopes = scopes return dst, nil } diff --git a/internal/query/device_auth_model.go b/internal/query/device_auth_model.go new file mode 100644 index 000000000..23fc0775e --- /dev/null +++ b/internal/query/device_auth_model.go @@ -0,0 +1,58 @@ +package query + +import ( + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/deviceauth" +) + +type DeviceAuthReadModel struct { + eventstore.ReadModel + DeviceAuth +} + +func NewDeviceAuthReadModel(deviceCode, resourceOwner string) *DeviceAuthReadModel { + return &DeviceAuthReadModel{ + ReadModel: eventstore.ReadModel{ + AggregateID: deviceCode, + ResourceOwner: resourceOwner, + }, + } +} + +func (m *DeviceAuthReadModel) Reduce() error { + for _, event := range m.Events { + switch e := event.(type) { + case *deviceauth.AddedEvent: + m.ClientID = e.ClientID + m.DeviceCode = e.DeviceCode + m.UserCode = e.UserCode + m.Expires = e.Expires + m.Scopes = e.Scopes + m.State = e.State + case *deviceauth.ApprovedEvent: + m.State = domain.DeviceAuthStateApproved + m.Subject = e.Subject + m.UserAuthMethods = e.UserAuthMethods + m.AuthTime = e.AuthTime + case *deviceauth.CanceledEvent: + m.State = e.Reason.State() + } + } + + return m.ReadModel.Reduce() +} + +func (m *DeviceAuthReadModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(m.ResourceOwner). + AddQuery(). + AggregateTypes(deviceauth.AggregateType). + AggregateIDs(m.AggregateID). + EventTypes( + deviceauth.AddedEventType, + deviceauth.ApprovedEventType, + deviceauth.CanceledEventType, + ). + Builder() +} diff --git a/internal/query/device_auth_test.go b/internal/query/device_auth_test.go index a9f5efcf2..c909694ae 100644 --- a/internal/query/device_auth_test.go +++ b/internal/query/device_auth_test.go @@ -6,82 +6,188 @@ import ( "database/sql/driver" "errors" "fmt" + "io" "regexp" "testing" + "time" "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" -) - -const ( - expectedDeviceAuthQueryC = `SELECT` + - ` projections.device_authorizations.id,` + - ` projections.device_authorizations.client_id,` + - ` projections.device_authorizations.scopes,` + - ` projections.device_authorizations.expires,` + - ` projections.device_authorizations.state,` + - ` projections.device_authorizations.subject` + - ` FROM projections.device_authorizations` - expectedDeviceAuthWhereDeviceCodeQueryC = expectedDeviceAuthQueryC + - ` WHERE projections.device_authorizations.client_id = $1` + - ` AND projections.device_authorizations.device_code = $2` + - ` AND projections.device_authorizations.instance_id = $3` - expectedDeviceAuthWhereUserCodeQueryC = expectedDeviceAuthQueryC + - ` WHERE projections.device_authorizations.instance_id = $1` + - ` AND projections.device_authorizations.user_code = $2` -) - -var ( - expectedDeviceAuthQuery = regexp.QuoteMeta(expectedDeviceAuthQueryC) - expectedDeviceAuthWhereDeviceCodeQuery = regexp.QuoteMeta(expectedDeviceAuthWhereDeviceCodeQueryC) - expectedDeviceAuthWhereUserCodeQuery = regexp.QuoteMeta(expectedDeviceAuthWhereUserCodeQueryC) - expectedDeviceAuthValues = []driver.Value{ - "primary-id", - "client-id", - database.TextArray[string]{"a", "b", "c"}, - testNow, - domain.DeviceAuthStateApproved, - "subject", - } - expectedDeviceAuth = &domain.DeviceAuth{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "primary-id", - }, - ClientID: "client-id", - Scopes: []string{"a", "b", "c"}, - Expires: testNow, - State: domain.DeviceAuthStateApproved, - Subject: "subject", - } + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/deviceauth" + "github.com/zitadel/zitadel/internal/zerrors" ) func TestQueries_DeviceAuthByDeviceCode(t *testing.T) { - client, mock, err := sqlmock.New() - if err != nil { - t.Fatalf("failed to build mock client: %v", err) + ctx := authz.NewMockContext("inst1", "org1", "user1") + timestamp := time.Date(2015, 12, 15, 22, 13, 45, 0, time.UTC) + tests := []struct { + name string + eventstore func(t *testing.T) *eventstore.Eventstore + want *DeviceAuth + wantErr error + }{ + { + name: "filter error", + eventstore: expectEventstore( + expectFilterError(io.ErrClosedPipe), + ), + wantErr: io.ErrClosedPipe, + }, + { + name: "not found", + eventstore: expectEventstore( + expectFilter(), + ), + wantErr: zerrors.ThrowNotFound(nil, "QUERY-eeR0e", "Errors.DeviceAuth.NotExisting"), + }, + { + name: "ok, initiated", + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("device1", "instance1"), + "client1", "device1", "user-code", timestamp, []string{"foo", "bar"}, + )), + ), + ), + want: &DeviceAuth{ + ClientID: "client1", + DeviceCode: "device1", + UserCode: "user-code", + Expires: timestamp, + Scopes: []string{"foo", "bar"}, + State: domain.DeviceAuthStateInitiated, + }, + }, + { + name: "ok, approved", + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("device1", "instance1"), + "client1", "device1", "user-code", timestamp, []string{"foo", "bar"}, + )), + eventFromEventPusher(deviceauth.NewApprovedEvent( + ctx, + deviceauth.NewAggregate("device1", "instance1"), + "user1", []domain.UserAuthMethodType{domain.UserAuthMethodTypePasswordless}, + timestamp, + )), + ), + ), + want: &DeviceAuth{ + ClientID: "client1", + DeviceCode: "device1", + UserCode: "user-code", + Expires: timestamp, + Scopes: []string{"foo", "bar"}, + State: domain.DeviceAuthStateApproved, + Subject: "user1", + UserAuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePasswordless}, + AuthTime: timestamp, + }, + }, + { + name: "ok, denied", + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("device1", "instance1"), + "client1", "device1", "user-code", timestamp, []string{"foo", "bar"}, + )), + eventFromEventPusher(deviceauth.NewCanceledEvent( + ctx, + deviceauth.NewAggregate("device1", "instance1"), + domain.DeviceAuthCanceledDenied, + )), + ), + ), + want: &DeviceAuth{ + ClientID: "client1", + DeviceCode: "device1", + UserCode: "user-code", + Expires: timestamp, + Scopes: []string{"foo", "bar"}, + State: domain.DeviceAuthStateDenied, + }, + }, + { + name: "ok, expired", + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("device1", "instance1"), + "client1", "device1", "user-code", timestamp, []string{"foo", "bar"}, + )), + eventFromEventPusher(deviceauth.NewCanceledEvent( + ctx, + deviceauth.NewAggregate("device1", "instance1"), + domain.DeviceAuthCanceledExpired, + )), + ), + ), + want: &DeviceAuth{ + ClientID: "client1", + DeviceCode: "device1", + UserCode: "user-code", + Expires: timestamp, + Scopes: []string{"foo", "bar"}, + State: domain.DeviceAuthStateExpired, + }, + }, } - defer client.Close() - - mock.ExpectBegin() - mock.ExpectQuery(expectedDeviceAuthWhereDeviceCodeQuery).WillReturnRows( - sqlmock.NewRows(deviceAuthSelectColumns).AddRow(expectedDeviceAuthValues...), - ) - mock.ExpectCommit() - q := Queries{ - client: &database.DB{DB: client}, + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := &Queries{ + eventstore: tt.eventstore(t), + } + got, err := q.DeviceAuthByDeviceCode(ctx, "device1") + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) } - got, err := q.DeviceAuthByDeviceCode(context.TODO(), "123", "456") - require.NoError(t, err) - assert.Equal(t, expectedDeviceAuth, got) - require.NoError(t, mock.ExpectationsWereMet()) } -func TestQueries_DeviceAuthByUserCode(t *testing.T) { +const ( + expectedDeviceAuthQueryC = `SELECT` + + ` projections.device_auth_requests.client_id,` + + ` projections.device_auth_requests.device_code,` + + ` projections.device_auth_requests.user_code,` + + ` projections.device_auth_requests.scopes` + + ` FROM projections.device_auth_requests` + expectedDeviceAuthWhereUserCodeQueryC = expectedDeviceAuthQueryC + + ` WHERE projections.device_auth_requests.instance_id = $1` + + ` AND projections.device_auth_requests.user_code = $2` +) + +var ( + expectedDeviceAuthQuery = regexp.QuoteMeta(expectedDeviceAuthQueryC) + expectedDeviceAuthWhereUserCodeQuery = regexp.QuoteMeta(expectedDeviceAuthWhereUserCodeQueryC) + expectedDeviceAuthValues = []driver.Value{ + "client-id", + "device1", + "user-code", + database.TextArray[string]{"a", "b", "c"}, + } + expectedDeviceAuth = &domain.AuthRequestDevice{ + ClientID: "client-id", + DeviceCode: "device1", + UserCode: "user-code", + Scopes: []string{"a", "b", "c"}, + } +) + +func TestQueries_DeviceAuthRequestByUserCode(t *testing.T) { client, mock, err := sqlmock.New() if err != nil { t.Fatalf("failed to build mock client: %v", err) @@ -96,7 +202,7 @@ func TestQueries_DeviceAuthByUserCode(t *testing.T) { q := Queries{ client: &database.DB{DB: client}, } - got, err := q.DeviceAuthByUserCode(context.TODO(), "789") + got, err := q.DeviceAuthRequestByUserCode(context.TODO(), "789") require.NoError(t, err) assert.Equal(t, expectedDeviceAuth, got) require.NoError(t, mock.ExpectationsWereMet()) @@ -110,7 +216,7 @@ func Test_prepareDeviceAuthQuery(t *testing.T) { tests := []struct { name string want want - object any + object *domain.AuthRequestDevice }{ { name: "success", @@ -137,7 +243,7 @@ func Test_prepareDeviceAuthQuery(t *testing.T) { return nil, true }, }, - object: (*domain.DeviceAuth)(nil), + object: nil, }, { name: "other error", @@ -153,7 +259,7 @@ func Test_prepareDeviceAuthQuery(t *testing.T) { return nil, true }, }, - object: (*domain.DeviceAuth)(nil), + object: nil, }, } for _, tt := range tests { diff --git a/internal/query/projection/device_auth.go b/internal/query/projection/device_auth.go index 935d72005..d8231004f 100644 --- a/internal/query/projection/device_auth.go +++ b/internal/query/projection/device_auth.go @@ -3,7 +3,6 @@ package projection import ( "context" - "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -12,57 +11,51 @@ import ( ) const ( - DeviceAuthProjectionTable = "projections.device_authorizations" + DeviceAuthRequestProjectionTable = "projections.device_auth_requests" - DeviceAuthColumnID = "id" - DeviceAuthColumnClientID = "client_id" - DeviceAuthColumnDeviceCode = "device_code" - DeviceAuthColumnUserCode = "user_code" - DeviceAuthColumnExpires = "expires" - DeviceAuthColumnScopes = "scopes" - DeviceAuthColumnState = "state" - DeviceAuthColumnSubject = "subject" - - DeviceAuthColumnCreationDate = "creation_date" - DeviceAuthColumnChangeDate = "change_date" - DeviceAuthColumnSequence = "sequence" - DeviceAuthColumnInstanceID = "instance_id" + DeviceAuthRequestColumnClientID = "client_id" + DeviceAuthRequestColumnDeviceCode = "device_code" + DeviceAuthRequestColumnUserCode = "user_code" + DeviceAuthRequestColumnScopes = "scopes" + DeviceAuthRequestColumnCreationDate = "creation_date" + DeviceAuthRequestColumnChangeDate = "change_date" + DeviceAuthRequestColumnSequence = "sequence" + DeviceAuthRequestColumnInstanceID = "instance_id" ) -type deviceAuthProjection struct{} +// deviceAuthRequestProjection holds device authorization requests +// and makes them search-able by User Code. +// In principle the projected data is only needed during user login. +// Device Token logic uses the eventstore directly. +type deviceAuthRequestProjection struct{} func newDeviceAuthProjection(ctx context.Context, config handler.Config) *handler.Handler { - return handler.NewHandler(ctx, &config, new(deviceAuthProjection)) + return handler.NewHandler(ctx, &config, new(deviceAuthRequestProjection)) } -func (*deviceAuthProjection) Name() string { - return DeviceAuthProjectionTable +func (*deviceAuthRequestProjection) Name() string { + return DeviceAuthRequestProjectionTable } -func (*deviceAuthProjection) Init() *old_handler.Check { +func (*deviceAuthRequestProjection) Init() *old_handler.Check { return handler.NewTableCheck( handler.NewTable([]*handler.InitColumn{ - handler.NewColumn(DeviceAuthColumnID, handler.ColumnTypeText), - handler.NewColumn(DeviceAuthColumnClientID, handler.ColumnTypeText), - handler.NewColumn(DeviceAuthColumnDeviceCode, handler.ColumnTypeText), - handler.NewColumn(DeviceAuthColumnUserCode, handler.ColumnTypeText), - handler.NewColumn(DeviceAuthColumnExpires, handler.ColumnTypeTimestamp), - handler.NewColumn(DeviceAuthColumnScopes, handler.ColumnTypeTextArray), - handler.NewColumn(DeviceAuthColumnState, handler.ColumnTypeEnum, handler.Default(domain.DeviceAuthStateInitiated)), - handler.NewColumn(DeviceAuthColumnSubject, handler.ColumnTypeText, handler.Default("")), - handler.NewColumn(DeviceAuthColumnCreationDate, handler.ColumnTypeTimestamp), - handler.NewColumn(DeviceAuthColumnChangeDate, handler.ColumnTypeTimestamp), - handler.NewColumn(DeviceAuthColumnSequence, handler.ColumnTypeInt64), - handler.NewColumn(DeviceAuthColumnInstanceID, handler.ColumnTypeText), + handler.NewColumn(DeviceAuthRequestColumnClientID, handler.ColumnTypeText), + handler.NewColumn(DeviceAuthRequestColumnDeviceCode, handler.ColumnTypeText), + handler.NewColumn(DeviceAuthRequestColumnUserCode, handler.ColumnTypeText), + handler.NewColumn(DeviceAuthRequestColumnScopes, handler.ColumnTypeTextArray), + handler.NewColumn(DeviceAuthRequestColumnCreationDate, handler.ColumnTypeTimestamp), + handler.NewColumn(DeviceAuthRequestColumnChangeDate, handler.ColumnTypeTimestamp), + handler.NewColumn(DeviceAuthRequestColumnSequence, handler.ColumnTypeInt64), + handler.NewColumn(DeviceAuthRequestColumnInstanceID, handler.ColumnTypeText), }, - handler.NewPrimaryKey(DeviceAuthColumnInstanceID, DeviceAuthColumnID), - handler.WithIndex(handler.NewIndex("user_code", []string{DeviceAuthColumnInstanceID, DeviceAuthColumnUserCode})), - handler.WithIndex(handler.NewIndex("device_code", []string{DeviceAuthColumnInstanceID, DeviceAuthColumnClientID, DeviceAuthColumnDeviceCode})), + handler.NewPrimaryKey(DeviceAuthRequestColumnInstanceID, DeviceAuthRequestColumnDeviceCode), + handler.WithIndex(handler.NewIndex("user_code", []string{DeviceAuthRequestColumnInstanceID, DeviceAuthRequestColumnUserCode})), ), ) } -func (p *deviceAuthProjection) Reducers() []handler.AggregateReducer { +func (p *deviceAuthRequestProjection) Reducers() []handler.AggregateReducer { return []handler.AggregateReducer{ { Aggregate: deviceauth.AggregateType, @@ -73,22 +66,18 @@ func (p *deviceAuthProjection) Reducers() []handler.AggregateReducer { }, { Event: deviceauth.ApprovedEventType, - Reduce: p.reduceAppoved, + Reduce: p.reduceDoneEvents, }, { Event: deviceauth.CanceledEventType, - Reduce: p.reduceCanceled, - }, - { - Event: deviceauth.RemovedEventType, - Reduce: p.reduceRemoved, + Reduce: p.reduceDoneEvents, }, }, }, } } -func (p *deviceAuthProjection) reduceAdded(event eventstore.Event) (*handler.Statement, error) { +func (p *deviceAuthRequestProjection) reduceAdded(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*deviceauth.AddedEvent) if !ok { return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-chu6O", "reduce.wrong.event.type %T != %s", event, deviceauth.AddedEventType) @@ -96,66 +85,30 @@ func (p *deviceAuthProjection) reduceAdded(event eventstore.Event) (*handler.Sta return handler.NewCreateStatement( e, []handler.Column{ - handler.NewCol(DeviceAuthColumnID, e.Aggregate().ID), - handler.NewCol(DeviceAuthColumnClientID, e.ClientID), - handler.NewCol(DeviceAuthColumnDeviceCode, e.DeviceCode), - handler.NewCol(DeviceAuthColumnUserCode, e.UserCode), - handler.NewCol(DeviceAuthColumnExpires, e.Expires), - handler.NewCol(DeviceAuthColumnScopes, e.Scopes), - handler.NewCol(DeviceAuthColumnCreationDate, e.CreationDate()), - handler.NewCol(DeviceAuthColumnChangeDate, e.CreationDate()), - handler.NewCol(DeviceAuthColumnSequence, e.Sequence()), - handler.NewCol(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(DeviceAuthRequestColumnClientID, e.ClientID), + handler.NewCol(DeviceAuthRequestColumnDeviceCode, e.DeviceCode), + handler.NewCol(DeviceAuthRequestColumnUserCode, e.UserCode), + handler.NewCol(DeviceAuthRequestColumnScopes, e.Scopes), + handler.NewCol(DeviceAuthRequestColumnCreationDate, e.CreationDate()), + handler.NewCol(DeviceAuthRequestColumnChangeDate, e.CreationDate()), + handler.NewCol(DeviceAuthRequestColumnSequence, e.Sequence()), + handler.NewCol(DeviceAuthRequestColumnInstanceID, e.Aggregate().InstanceID), }, ), nil } -func (p *deviceAuthProjection) reduceAppoved(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*deviceauth.ApprovedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-kei0A", "reduce.wrong.event.type %T != %s", event, deviceauth.ApprovedEventType) - } - return handler.NewUpdateStatement(e, - []handler.Column{ - handler.NewCol(DeviceAuthColumnState, domain.DeviceAuthStateApproved), - handler.NewCol(DeviceAuthColumnSubject, e.Subject), - handler.NewCol(DeviceAuthColumnChangeDate, e.CreationDate()), - handler.NewCol(DeviceAuthColumnSequence, e.Sequence()), - }, - []handler.Condition{ - handler.NewCond(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID), - handler.NewCond(DeviceAuthColumnID, e.Aggregate().ID), - }, - ), nil -} +// reduceDoneEvents removes the device auth request from the projection. +func (p *deviceAuthRequestProjection) reduceDoneEvents(event eventstore.Event) (*handler.Statement, error) { + switch event.(type) { + case *deviceauth.ApprovedEvent, *deviceauth.CanceledEvent: + return handler.NewDeleteStatement(event, + []handler.Condition{ + handler.NewCond(DeviceAuthRequestColumnInstanceID, event.Aggregate().InstanceID), + handler.NewCond(DeviceAuthRequestColumnDeviceCode, event.Aggregate().ID), + }, + ), nil -func (p *deviceAuthProjection) reduceCanceled(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*deviceauth.CanceledEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-eeS8d", "reduce.wrong.event.type %T != %s", event, deviceauth.CanceledEventType) + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-eeS8d", "reduce.wrong.event.type %T", event) } - return handler.NewUpdateStatement(e, - []handler.Column{ - handler.NewCol(DeviceAuthColumnState, e.Reason.State()), - handler.NewCol(DeviceAuthColumnChangeDate, e.CreationDate()), - handler.NewCol(DeviceAuthColumnSequence, e.Sequence()), - }, - []handler.Condition{ - handler.NewCond(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID), - handler.NewCond(DeviceAuthColumnID, e.Aggregate().ID), - }, - ), nil -} - -func (p *deviceAuthProjection) reduceRemoved(event eventstore.Event) (*handler.Statement, error) { - e, ok := event.(*deviceauth.RemovedEvent) - if !ok { - return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-AJi1u", "reduce.wrong.event.type %T != %s", event, deviceauth.RemovedEventType) - } - return handler.NewDeleteStatement(e, - []handler.Condition{ - handler.NewCond(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID), - handler.NewCond(DeviceAuthColumnID, e.Aggregate().ID), - }, - ), nil } diff --git a/internal/query/query.go b/internal/query/query.go index c2538414b..b746909ef 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -20,6 +20,7 @@ import ( "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/action" "github.com/zitadel/zitadel/internal/repository/authrequest" + "github.com/zitadel/zitadel/internal/repository/deviceauth" "github.com/zitadel/zitadel/internal/repository/idpintent" iam_repo "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/keypair" @@ -99,6 +100,7 @@ func StartQueries( quota.RegisterEventMappers(repo.eventstore) limits.RegisterEventMappers(repo.eventstore) restrictions.RegisterEventMappers(repo.eventstore) + deviceauth.RegisterEventMappers(repo.eventstore) repo.checkPermission = permissionCheck(repo) diff --git a/internal/query/query_test.go b/internal/query/query_test.go index f1dee6981..a7f43bbab 100644 --- a/internal/query/query_test.go +++ b/internal/query/query_test.go @@ -12,6 +12,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore/repository/mock" action_repo "github.com/zitadel/zitadel/internal/repository/action" "github.com/zitadel/zitadel/internal/repository/authrequest" + "github.com/zitadel/zitadel/internal/repository/deviceauth" "github.com/zitadel/zitadel/internal/repository/feature" "github.com/zitadel/zitadel/internal/repository/idpintent" iam_repo "github.com/zitadel/zitadel/internal/repository/instance" @@ -54,6 +55,7 @@ func expectEventstore(expects ...expect) func(*testing.T) *eventstore.Eventstore quota_repo.RegisterEventMappers(es) limits.RegisterEventMappers(es) feature.RegisterEventMappers(es) + deviceauth.RegisterEventMappers(es) return es } } diff --git a/internal/repository/deviceauth/constraints.go b/internal/repository/deviceauth/constraints.go index d8362760e..e88c83e38 100644 --- a/internal/repository/deviceauth/constraints.go +++ b/internal/repository/deviceauth/constraints.go @@ -1,8 +1,6 @@ package deviceauth import ( - "strings" - "github.com/zitadel/zitadel/internal/eventstore" ) @@ -13,15 +11,11 @@ const ( DuplicateDeviceCode = "Errors.DeviceCode.AlreadyExists" ) -func deviceCodeUniqueField(clientID, deviceCode string) string { - return strings.Join([]string{clientID, deviceCode}, ":") -} - -func NewAddUniqueConstraints(clientID, deviceCode, userCode string) []*eventstore.UniqueConstraint { +func NewAddUniqueConstraints(deviceCode, userCode string) []*eventstore.UniqueConstraint { return []*eventstore.UniqueConstraint{ eventstore.NewAddEventUniqueConstraint( UniqueDeviceCode, - deviceCodeUniqueField(clientID, deviceCode), + deviceCode, DuplicateDeviceCode, ), eventstore.NewAddEventUniqueConstraint( @@ -32,11 +26,11 @@ func NewAddUniqueConstraints(clientID, deviceCode, userCode string) []*eventstor } } -func NewRemoveUniqueConstraints(clientID, deviceCode, userCode string) []*eventstore.UniqueConstraint { +func NewRemoveUniqueConstraints(deviceCode, userCode string) []*eventstore.UniqueConstraint { return []*eventstore.UniqueConstraint{ eventstore.NewRemoveUniqueConstraint( UniqueDeviceCode, - deviceCodeUniqueField(clientID, deviceCode), + deviceCode, ), eventstore.NewRemoveUniqueConstraint( UniqueUserCode, diff --git a/internal/repository/deviceauth/device_auth.go b/internal/repository/deviceauth/device_auth.go index 26b50f4ee..54f8eb623 100644 --- a/internal/repository/deviceauth/device_auth.go +++ b/internal/repository/deviceauth/device_auth.go @@ -13,7 +13,6 @@ const ( AddedEventType = eventTypePrefix + "added" ApprovedEventType = eventTypePrefix + "approved" CanceledEventType = eventTypePrefix + "canceled" - RemovedEventType = eventTypePrefix + "removed" ) type AddedEvent struct { @@ -36,7 +35,7 @@ func (e *AddedEvent) Payload() any { } func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return NewAddUniqueConstraints(e.ClientID, e.DeviceCode, e.UserCode) + return NewAddUniqueConstraints(e.DeviceCode, e.UserCode) } func NewAddedEvent( @@ -58,7 +57,9 @@ func NewAddedEvent( type ApprovedEvent struct { *eventstore.BaseEvent `json:"-"` - Subject string + Subject string + UserAuthMethods []domain.UserAuthMethodType + AuthTime time.Time } func (e *ApprovedEvent) SetBaseEvent(b *eventstore.BaseEvent) { @@ -77,12 +78,16 @@ func NewApprovedEvent( ctx context.Context, aggregate *eventstore.Aggregate, subject string, + userAuthMethods []domain.UserAuthMethodType, + authTime time.Time, ) *ApprovedEvent { return &ApprovedEvent{ eventstore.NewBaseEventForPush( ctx, aggregate, ApprovedEventType, ), subject, + userAuthMethods, + authTime, } } @@ -107,36 +112,3 @@ func (e *CanceledEvent) UniqueConstraints() []*eventstore.UniqueConstraint { func NewCanceledEvent(ctx context.Context, aggregate *eventstore.Aggregate, reason domain.DeviceAuthCanceled) *CanceledEvent { return &CanceledEvent{eventstore.NewBaseEventForPush(ctx, aggregate, CanceledEventType), reason} } - -type RemovedEvent struct { - *eventstore.BaseEvent `json:"-"` - - ClientID string - DeviceCode string - UserCode string -} - -func (e *RemovedEvent) SetBaseEvent(b *eventstore.BaseEvent) { - e.BaseEvent = b -} - -func (e *RemovedEvent) Payload() any { - return e -} - -func (e *RemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { - return NewRemoveUniqueConstraints(e.ClientID, e.DeviceCode, e.UserCode) -} - -func NewRemovedEvent( - ctx context.Context, - aggregate *eventstore.Aggregate, - clientID, deviceCode, userCode string, -) *RemovedEvent { - return &RemovedEvent{ - eventstore.NewBaseEventForPush( - ctx, aggregate, RemovedEventType, - ), - clientID, deviceCode, userCode, - } -} diff --git a/internal/repository/deviceauth/eventstore.go b/internal/repository/deviceauth/eventstore.go new file mode 100644 index 000000000..6ebdf71b6 --- /dev/null +++ b/internal/repository/deviceauth/eventstore.go @@ -0,0 +1,9 @@ +package deviceauth + +import "github.com/zitadel/zitadel/internal/eventstore" + +func RegisterEventMappers(es *eventstore.Eventstore) { + es.RegisterFilterEventMapper(AggregateType, AddedEventType, eventstore.GenericEventMapper[AddedEvent]). + RegisterFilterEventMapper(AggregateType, ApprovedEventType, eventstore.GenericEventMapper[ApprovedEvent]). + RegisterFilterEventMapper(AggregateType, CanceledEventType, eventstore.GenericEventMapper[CanceledEvent]) +} diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index 79d409e1c..ca24c58f9 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -2,7 +2,6 @@ package org import ( "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/repository/deviceauth" ) func RegisterEventMappers(es *eventstore.Eventstore) { @@ -114,9 +113,5 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, MetadataRemovedAllType, MetadataRemovedAllEventMapper). RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper). RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper). - RegisterFilterEventMapper(AggregateType, NotificationPolicyRemovedEventType, NotificationPolicyRemovedEventMapper). - RegisterFilterEventMapper(AggregateType, deviceauth.AddedEventType, eventstore.GenericEventMapper[deviceauth.AddedEvent]). - RegisterFilterEventMapper(AggregateType, deviceauth.ApprovedEventType, eventstore.GenericEventMapper[deviceauth.ApprovedEvent]). - RegisterFilterEventMapper(AggregateType, deviceauth.CanceledEventType, eventstore.GenericEventMapper[deviceauth.CanceledEvent]). - RegisterFilterEventMapper(AggregateType, deviceauth.RemovedEventType, eventstore.GenericEventMapper[deviceauth.RemovedEvent]) + RegisterFilterEventMapper(AggregateType, NotificationPolicyRemovedEventType, NotificationPolicyRemovedEventMapper) }