diff --git a/internal/api/grpc/session/v2/session_integration_test.go b/internal/api/grpc/session/v2/session_integration_test.go index b4602cd8fe..1b3e6d19ee 100644 --- a/internal/api/grpc/session/v2/session_integration_test.go +++ b/internal/api/grpc/session/v2/session_integration_test.go @@ -27,7 +27,7 @@ var ( func TestMain(m *testing.M) { os.Exit(func() int { - ctx, errCtx, cancel := integration.Contexts(time.Hour) + ctx, errCtx, cancel := integration.Contexts(5 * time.Minute) defer cancel() Tester = integration.NewTester(ctx) @@ -55,7 +55,7 @@ retry: s = resp.GetSession() break retry } - if status.Convert(err).Code() == codes.NotFound { + if code := status.Convert(err).Code(); code == codes.NotFound || code == codes.PermissionDenied { select { case <-CTX.Done(): t.Fatal(CTX.Err(), err) diff --git a/internal/api/grpc/user/v2/passkey.go b/internal/api/grpc/user/v2/passkey.go index e68feb6815..6f62be3dec 100644 --- a/internal/api/grpc/user/v2/passkey.go +++ b/internal/api/grpc/user/v2/passkey.go @@ -3,13 +3,13 @@ package user import ( "context" - "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" ) @@ -41,24 +41,32 @@ func passkeyAuthenticatorToDomain(pa user.PasskeyAuthenticator) domain.Authentic } } -func passkeyRegistrationDetailsToPb(details *domain.PasskeyRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) { +func webAuthNRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*object_pb.Details, *structpb.Struct, error) { + if err != nil { + return nil, nil, err + } + options := new(structpb.Struct) + if err := options.UnmarshalJSON(details.PublicKeyCredentialCreationOptions); err != nil { + return nil, nil, caos_errs.ThrowInternal(err, "USERv2-Dohr6", "Errors.Internal") + } + return object.DomainToDetailsPb(details.ObjectDetails), options, nil +} + +func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) { + objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err) if err != nil { return nil, err } - options := new(structpb.Struct) - if err := protojson.Unmarshal(details.PublicKeyCredentialCreationOptions, options); err != nil { - return nil, caos_errs.ThrowInternal(err, "USERv2-Dohr6", "Errors.Internal") - } return &user.RegisterPasskeyResponse{ - Details: object.DomainToDetailsPb(details.ObjectDetails), - PasskeyId: details.PasskeyID, + Details: objectDetails, + PasskeyId: details.ID, PublicKeyCredentialCreationOptions: options, }, nil } func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) { resourceOwner := authz.GetCtxData(ctx).ResourceOwner - pkc, err := protojson.Marshal(req.GetPublicKeyCredential()) + pkc, err := req.GetPublicKeyCredential().MarshalJSON() if err != nil { return nil, caos_errs.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal") } diff --git a/internal/api/grpc/user/v2/passkey_integration_test.go b/internal/api/grpc/user/v2/passkey_integration_test.go index 5bad7ecfd3..dbc6b11785 100644 --- a/internal/api/grpc/user/v2/passkey_integration_test.go +++ b/internal/api/grpc/user/v2/passkey_integration_test.go @@ -94,7 +94,8 @@ func TestServer_RegisterPasskey(t *testing.T) { }, wantErr: true, }, - /* TODO after we are able to obtain a Bearer token for a human user + /* TODO: after we are able to obtain a Bearer token for a human user + https://github.com/zitadel/zitadel/issues/6022 { name: "human user", args: args{ diff --git a/internal/api/grpc/user/v2/passkey_test.go b/internal/api/grpc/user/v2/passkey_test.go index b8988a73e7..fe8d397b54 100644 --- a/internal/api/grpc/user/v2/passkey_test.go +++ b/internal/api/grpc/user/v2/passkey_test.go @@ -50,7 +50,7 @@ func Test_passkeyAuthenticatorToDomain(t *testing.T) { func Test_passkeyRegistrationDetailsToPb(t *testing.T) { type args struct { - details *domain.PasskeyRegistrationDetails + details *domain.WebAuthNRegistrationDetails err error } tests := []struct { @@ -70,13 +70,13 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { { name: "unmarshall error", args: args{ - details: &domain.PasskeyRegistrationDetails{ + details: &domain.WebAuthNRegistrationDetails{ ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), ResourceOwner: "me", }, - PasskeyID: "123", + ID: "123", PublicKeyCredentialCreationOptions: []byte(`\\`), }, err: nil, @@ -86,13 +86,13 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { { name: "ok", args: args{ - details: &domain.PasskeyRegistrationDetails{ + details: &domain.WebAuthNRegistrationDetails{ ObjectDetails: &domain.ObjectDetails{ Sequence: 22, EventDate: time.Unix(3000, 22), ResourceOwner: "me", }, - PasskeyID: "123", + ID: "123", PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`), }, err: nil, diff --git a/internal/api/grpc/user/v2/u2f.go b/internal/api/grpc/user/v2/u2f.go new file mode 100644 index 0000000000..ae1daf8443 --- /dev/null +++ b/internal/api/grpc/user/v2/u2f.go @@ -0,0 +1,44 @@ +package user + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" +) + +func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) { + return u2fRegistrationDetailsToPb( + s.command.RegisterUserU2F(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner), + ) +} + +func u2fRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterU2FResponse, error) { + objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err) + if err != nil { + return nil, err + } + return &user.RegisterU2FResponse{ + Details: objectDetails, + U2FId: details.ID, + PublicKeyCredentialCreationOptions: options, + }, nil +} + +func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FRegistrationRequest) (*user.VerifyU2FRegistrationResponse, error) { + resourceOwner := authz.GetCtxData(ctx).ResourceOwner + pkc, err := req.GetPublicKeyCredential().MarshalJSON() + if err != nil { + return nil, caos_errs.ThrowInternal(err, "USERv2-IeTh4", "Errors.Internal") + } + objectDetails, err := s.command.HumanVerifyU2FSetup(ctx, req.GetUserId(), resourceOwner, req.GetTokenName(), "", pkc) + if err != nil { + return nil, err + } + return &user.VerifyU2FRegistrationResponse{ + Details: object.DomainToDetailsPb(objectDetails), + }, nil +} diff --git a/internal/api/grpc/user/v2/u2f_integration_test.go b/internal/api/grpc/user/v2/u2f_integration_test.go new file mode 100644 index 0000000000..93c0b4c0a6 --- /dev/null +++ b/internal/api/grpc/user/v2/u2f_integration_test.go @@ -0,0 +1,167 @@ +//go:build integration + +package user_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/zitadel/zitadel/internal/integration" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" +) + +func TestServer_RegisterU2F(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + + type args struct { + ctx context.Context + req *user.RegisterU2FRequest + } + tests := []struct { + name string + args args + want *user.RegisterU2FResponse + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: CTX, + req: &user.RegisterU2FRequest{}, + }, + wantErr: true, + }, + { + name: "user mismatch", + args: args{ + ctx: CTX, + req: &user.RegisterU2FRequest{ + UserId: userID, + }, + }, + wantErr: true, + }, + /* TODO: after we are able to obtain a Bearer token for a human user + https://github.com/zitadel/zitadel/issues/6022 + { + name: "human user", + args: args{ + ctx: CTX, + req: &user.RegisterU2FRequest{ + UserId: userID, + }, + }, + want: &user.RegisterU2FResponse{ + Details: &object.Details{ + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + */ + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.RegisterU2F(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + if tt.want != nil { + assert.NotEmpty(t, got.GetU2FId()) + assert.NotEmpty(t, got.GetPublicKeyCredentialCreationOptions()) + _, err = Tester.WebAuthN.CreateAttestationResponse(got.GetPublicKeyCredentialCreationOptions()) + require.NoError(t, err) + } + }) + } +} + +func TestServer_VerifyU2FRegistration(t *testing.T) { + userID := Tester.CreateHumanUser(CTX).GetUserId() + /* TODO after we are able to obtain a Bearer token for a human user + pkr, err := Client.RegisterU2F(CTX, &user.RegisterU2FRequest{ + UserId: userID, + }) + require.NoError(t, err) + require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions()) + + attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) + require.NoError(t, err) + */ + + type args struct { + ctx context.Context + req *user.VerifyU2FRegistrationRequest + } + tests := []struct { + name string + args args + want *user.VerifyU2FRegistrationResponse + wantErr bool + }{ + { + name: "missing user id", + args: args{ + ctx: CTX, + req: &user.VerifyU2FRegistrationRequest{ + U2FId: "123", + TokenName: "nice name", + }, + }, + wantErr: true, + }, + /* TODO after we are able to obtain a Bearer token for a human user + { + name: "success", + args: args{ + ctx: CTX, + req: &user.VerifyU2FRegistrationRequest{ + UserId: userID, + U2FId: pkr.GetU2FId(), + PublicKeyCredential: attestationResponse, + TokenName: "nice name", + }, + }, + want: &user.VerifyU2FRegistrationResponse{ + Details: &object.Details{ + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + */ + { + name: "wrong credential", + args: args{ + ctx: CTX, + req: &user.VerifyU2FRegistrationRequest{ + UserId: userID, + U2FId: "123", + PublicKeyCredential: &structpb.Struct{ + Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}}, + }, + TokenName: "nice name", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.VerifyU2FRegistration(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/user/v2/u2f_test.go b/internal/api/grpc/user/v2/u2f_test.go new file mode 100644 index 0000000000..1fe70a91d1 --- /dev/null +++ b/internal/api/grpc/user/v2/u2f_test.go @@ -0,0 +1,97 @@ +package user + +import ( + "io" + "testing" + "time" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" +) + +func Test_u2fRegistrationDetailsToPb(t *testing.T) { + type args struct { + details *domain.WebAuthNRegistrationDetails + err error + } + tests := []struct { + name string + args args + want *user.RegisterU2FResponse + wantErr error + }{ + { + name: "an error", + args: args{ + details: nil, + err: io.ErrClosedPipe, + }, + wantErr: io.ErrClosedPipe, + }, + { + name: "unmarshall error", + args: args{ + details: &domain.WebAuthNRegistrationDetails{ + ObjectDetails: &domain.ObjectDetails{ + Sequence: 22, + EventDate: time.Unix(3000, 22), + ResourceOwner: "me", + }, + ID: "123", + PublicKeyCredentialCreationOptions: []byte(`\\`), + }, + err: nil, + }, + wantErr: caos_errs.ThrowInternal(nil, "USERv2-Dohr6", "Errors.Internal"), + }, + { + name: "ok", + args: args{ + details: &domain.WebAuthNRegistrationDetails{ + ObjectDetails: &domain.ObjectDetails{ + Sequence: 22, + EventDate: time.Unix(3000, 22), + ResourceOwner: "me", + }, + ID: "123", + PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`), + }, + err: nil, + }, + want: &user.RegisterU2FResponse{ + Details: &object.Details{ + Sequence: 22, + ChangeDate: ×tamppb.Timestamp{ + Seconds: 3000, + Nanos: 22, + }, + ResourceOwner: "me", + }, + U2FId: "123", + PublicKeyCredentialCreationOptions: &structpb.Struct{ + Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := u2fRegistrationDetailsToPb(tt.args.details, tt.args.err) + require.ErrorIs(t, err, tt.wantErr) + if !proto.Equal(tt.want, got) { + t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got) + } + if tt.want != nil { + grpc.AllFieldsSet(t, got.ProtoReflect()) + } + }) + } +} diff --git a/internal/command/user_v2_passkey.go b/internal/command/user_v2_passkey.go index 2795e6df8a..fe2a420921 100644 --- a/internal/command/user_v2_passkey.go +++ b/internal/command/user_v2_passkey.go @@ -16,7 +16,7 @@ import ( // RegisterUserPasskey creates a passkey registration for the current authenticated user. // UserID, ussualy taken from the request is compaired against the user ID in the context. -func (c *Commands) RegisterUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment) (*domain.PasskeyRegistrationDetails, error) { +func (c *Commands) RegisterUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment) (*domain.WebAuthNRegistrationDetails, error) { if err := authz.UserIDInCTX(ctx, userID); err != nil { return nil, err } @@ -25,7 +25,7 @@ func (c *Commands) RegisterUserPasskey(ctx context.Context, userID, resourceOwne // RegisterUserPasskeyWithCode registers a new passkey for a unauthenticated user id. // The resource is protected by the code, identified by the codeID. -func (c *Commands) RegisterUserPasskeyWithCode(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, codeID, code string, alg crypto.EncryptionAlgorithm) (*domain.PasskeyRegistrationDetails, error) { +func (c *Commands) RegisterUserPasskeyWithCode(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, codeID, code string, alg crypto.EncryptionAlgorithm) (*domain.WebAuthNRegistrationDetails, error) { event, err := c.verifyUserPasskeyCode(ctx, userID, resourceOwner, codeID, code, alg) if err != nil { return nil, err @@ -63,7 +63,7 @@ func (c *Commands) verifyUserPasskeyCodeFailed(ctx context.Context, wm *HumanPas logging.WithFields("userID", userAgg.ID).OnError(err).Error("RegisterUserPasskeyWithCode push failed") } -func (c *Commands) registerUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, events ...eventCallback) (*domain.PasskeyRegistrationDetails, error) { +func (c *Commands) registerUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, events ...eventCallback) (*domain.WebAuthNRegistrationDetails, error) { wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, userID, resourceOwner, authenticator) if err != nil { return nil, err @@ -79,7 +79,7 @@ func (c *Commands) createUserPasskey(ctx context.Context, userID, resourceOwner return c.addHumanWebAuthN(ctx, userID, resourceOwner, false, passwordlessTokens, authenticator, domain.UserVerificationRequirementRequired) } -func (c *Commands) pushUserPasskey(ctx context.Context, wm *HumanWebAuthNWriteModel, userAgg *eventstore.Aggregate, webAuthN *domain.WebAuthNToken, events ...eventCallback) (*domain.PasskeyRegistrationDetails, error) { +func (c *Commands) pushUserPasskey(ctx context.Context, wm *HumanWebAuthNWriteModel, userAgg *eventstore.Aggregate, webAuthN *domain.WebAuthNToken, events ...eventCallback) (*domain.WebAuthNRegistrationDetails, error) { cmds := make([]eventstore.Command, len(events)+1) cmds[0] = user.NewHumanPasswordlessAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge) for i, event := range events { @@ -90,9 +90,9 @@ func (c *Commands) pushUserPasskey(ctx context.Context, wm *HumanWebAuthNWriteMo if err != nil { return nil, err } - return &domain.PasskeyRegistrationDetails{ + return &domain.WebAuthNRegistrationDetails{ ObjectDetails: writeModelToObjectDetails(&wm.WriteModel), - PasskeyID: wm.WebauthNTokenID, + ID: wm.WebauthNTokenID, PublicKeyCredentialCreationOptions: webAuthN.CredentialCreationData, }, nil } diff --git a/internal/command/user_v2_passkey_test.go b/internal/command/user_v2_passkey_test.go index aefa9685aa..d8b0d0a188 100644 --- a/internal/command/user_v2_passkey_test.go +++ b/internal/command/user_v2_passkey_test.go @@ -46,7 +46,7 @@ func TestCommands_RegisterUserPasskey(t *testing.T) { name string fields fields args args - want *domain.PasskeyRegistrationDetails + want *domain.WebAuthNRegistrationDetails wantErr error }{ { @@ -449,7 +449,7 @@ func TestCommands_pushUserPasskey(t *testing.T) { require.ErrorIs(t, err, tt.wantErr) if tt.wantErr == nil { assert.NotEmpty(t, got.PublicKeyCredentialCreationOptions) - assert.Equal(t, "123", got.PasskeyID) + assert.Equal(t, "123", got.ID) assert.Equal(t, "org1", got.ObjectDetails.ResourceOwner) } }) diff --git a/internal/command/user_v2_u2f.go b/internal/command/user_v2_u2f.go new file mode 100644 index 0000000000..fcda1de6b3 --- /dev/null +++ b/internal/command/user_v2_u2f.go @@ -0,0 +1,46 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" +) + +func (c *Commands) RegisterUserU2F(ctx context.Context, userID, resourceOwner string) (*domain.WebAuthNRegistrationDetails, error) { + if err := authz.UserIDInCTX(ctx, userID); err != nil { + return nil, err + } + return c.registerUserU2F(ctx, userID, resourceOwner) +} + +func (c *Commands) registerUserU2F(ctx context.Context, userID, resourceOwner string) (*domain.WebAuthNRegistrationDetails, error) { + wm, userAgg, webAuthN, err := c.createUserU2F(ctx, userID, resourceOwner) + if err != nil { + return nil, err + } + return c.pushUserU2F(ctx, wm, userAgg, webAuthN) +} + +func (c *Commands) createUserU2F(ctx context.Context, userID, resourceOwner string) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) { + tokens, err := c.getHumanU2FTokens(ctx, userID, resourceOwner) + if err != nil { + return nil, nil, nil, err + } + return c.addHumanWebAuthN(ctx, userID, resourceOwner, false, tokens, domain.AuthenticatorAttachmentUnspecified, domain.UserVerificationRequirementRequired) +} + +func (c *Commands) pushUserU2F(ctx context.Context, wm *HumanWebAuthNWriteModel, userAgg *eventstore.Aggregate, webAuthN *domain.WebAuthNToken) (*domain.WebAuthNRegistrationDetails, error) { + cmd := user.NewHumanU2FAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge) + err := c.pushAppendAndReduce(ctx, wm, cmd) + if err != nil { + return nil, err + } + return &domain.WebAuthNRegistrationDetails{ + ObjectDetails: writeModelToObjectDetails(&wm.WriteModel), + ID: wm.WebauthNTokenID, + PublicKeyCredentialCreationOptions: webAuthN.CredentialCreationData, + }, nil +} diff --git a/internal/command/user_v2_u2f_test.go b/internal/command/user_v2_u2f_test.go new file mode 100644 index 0000000000..aecb0ad79a --- /dev/null +++ b/internal/command/user_v2_u2f_test.go @@ -0,0 +1,215 @@ +package command + +import ( + "context" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/id" + id_mock "github.com/zitadel/zitadel/internal/id/mock" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/user" + webauthn_helper "github.com/zitadel/zitadel/internal/webauthn" +) + +func TestCommands_RegisterUserU2F(t *testing.T) { + ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil) + ctx = authz.WithRequestedDomain(ctx, "example.com") + + webauthnConfig := &webauthn_helper.Config{ + DisplayName: "test", + ExternalSecure: true, + } + userAgg := &user.NewAggregate("user1", "org1").Aggregate + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + } + type args struct { + userID string + resourceOwner string + } + tests := []struct { + name string + fields fields + args args + want *domain.WebAuthNRegistrationDetails + wantErr error + }{ + { + name: "wrong user", + args: args{ + userID: "foo", + resourceOwner: "org1", + }, + wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"), + }, + { + name: "get human passwordless error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilterError(io.ErrClosedPipe), + ), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + }, + wantErr: io.ErrClosedPipe, + }, + { + name: "id generator error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(), // getHumanPasswordlessTokens + expectFilter(eventFromEventPusher( + user.NewHumanAddedEvent(ctx, + userAgg, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + )), + expectFilter(eventFromEventPusher( + org.NewOrgAddedEvent(ctx, + &org.NewAggregate("org1").Aggregate, + "org1", + ), + )), + expectFilter(eventFromEventPusher( + org.NewDomainPolicyAddedEvent(ctx, + &org.NewAggregate("org1").Aggregate, + false, false, false, + ), + )), + ), + idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + }, + wantErr: io.ErrClosedPipe, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + webauthnConfig: webauthnConfig, + } + _, err := c.RegisterUserU2F(ctx, tt.args.userID, tt.args.resourceOwner) + require.ErrorIs(t, err, tt.wantErr) + // successful case can't be tested due to random challenge. + }) + } +} + +func TestCommands_pushUserU2F(t *testing.T) { + ctx := authz.WithRequestedDomain(context.Background(), "example.com") + webauthnConfig := &webauthn_helper.Config{ + DisplayName: "test", + ExternalSecure: true, + } + userAgg := &user.NewAggregate("user1", "org1").Aggregate + + prep := []expect{ + expectFilter(), // getHumanU2FTokens + expectFilter(eventFromEventPusher( + user.NewHumanAddedEvent(ctx, + userAgg, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + )), + expectFilter(eventFromEventPusher( + org.NewOrgAddedEvent(ctx, + &org.NewAggregate("org1").Aggregate, + "org1", + ), + )), + expectFilter(eventFromEventPusher( + org.NewDomainPolicyAddedEvent(ctx, + &org.NewAggregate("org1").Aggregate, + false, false, false, + ), + )), + expectFilter(eventFromEventPusher( + user.NewHumanWebAuthNAddedEvent(eventstore.NewBaseEventForPush( + ctx, &org.NewAggregate("org1").Aggregate, user.HumanPasswordlessTokenAddedType, + ), "111", "challenge"), + )), + } + + tests := []struct { + name string + expectPush func(challenge string) expect + wantErr error + }{ + { + name: "push error", + expectPush: func(challenge string) expect { + return expectPushFailed(io.ErrClosedPipe, []*repository.Event{eventFromEventPusher( + user.NewHumanU2FAddedEvent(ctx, + userAgg, "123", challenge, + ), + )}) + }, + wantErr: io.ErrClosedPipe, + }, + { + name: "success", + expectPush: func(challenge string) expect { + return expectPush([]*repository.Event{eventFromEventPusher( + user.NewHumanU2FAddedEvent(ctx, + userAgg, "123", challenge, + ), + )}) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: eventstoreExpect(t, prep...), + webauthnConfig: webauthnConfig, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"), + } + wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, "user1", "org1", domain.AuthenticatorAttachmentCrossPlattform) + require.NoError(t, err) + + c.eventstore = eventstoreExpect(t, tt.expectPush(webAuthN.Challenge)) + + got, err := c.pushUserU2F(ctx, wm, userAgg, webAuthN) + require.ErrorIs(t, err, tt.wantErr) + if tt.wantErr == nil { + assert.NotEmpty(t, got.PublicKeyCredentialCreationOptions) + assert.Equal(t, "123", got.ID) + assert.Equal(t, "org1", got.ObjectDetails.ResourceOwner) + } + }) + } +} diff --git a/internal/domain/user_v2_passkey.go b/internal/domain/user_v2_passkey.go index 37f34097e9..69d7b4a359 100644 --- a/internal/domain/user_v2_passkey.go +++ b/internal/domain/user_v2_passkey.go @@ -21,9 +21,9 @@ type PasskeyCodeDetails struct { Code string } -type PasskeyRegistrationDetails struct { +type WebAuthNRegistrationDetails struct { *ObjectDetails - PasskeyID string + ID string PublicKeyCredentialCreationOptions []byte }