mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 13:52:51 +00:00
When an expired invite code was used for webauthn (passkeys), the change date got updated by the failed event. This change date was used to test for expiry, meaning failed events would reset the expiry timeout. This fix adds a Code Creation Date to the writemodel which gets set by the first event. This can be the added or requested event. Other changes: - Expiry renamed to CodeExpiry so it's consistent with similar write models using secret codes. - humanVerifyPasswordlessInitCode takes an algorithm instead of the complete generator, so the method can be unit tested easier - Added tests that reproduced the original issue
1126 lines
31 KiB
Go
1126 lines
31 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/mock/gomock"
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
http_util "github.com/zitadel/zitadel/internal/api/http"
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"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/org"
|
|
"github.com/zitadel/zitadel/internal/repository/user"
|
|
webauthn_helper "github.com/zitadel/zitadel/internal/webauthn"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
func TestCommands_RegisterUserPasskey(t *testing.T) {
|
|
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
|
|
ctx = http_util.WithRequestedHost(ctx, "example.com")
|
|
|
|
webauthnConfig := &webauthn_helper.Config{
|
|
DisplayName: "test",
|
|
ExternalSecure: true,
|
|
}
|
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
|
type fields struct {
|
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
|
idGenerator id.Generator
|
|
checkPermission domain.PermissionCheck
|
|
}
|
|
type args struct {
|
|
userID string
|
|
resourceOwner string
|
|
rpID string
|
|
authenticator domain.AuthenticatorAttachment
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want *domain.WebAuthNRegistrationDetails
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "no permission",
|
|
fields: fields{
|
|
eventstore: expectEventstore(),
|
|
checkPermission: newMockPermissionCheckNotAllowed(),
|
|
},
|
|
args: args{
|
|
userID: "foo",
|
|
resourceOwner: "org1",
|
|
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
|
|
},
|
|
wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
|
},
|
|
{
|
|
name: "get human passwordless error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilterError(io.ErrClosedPipe),
|
|
),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "id generator error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(), // getHumanPasswordlessTokens
|
|
expectFilter(eventFromEventPusher(
|
|
user.NewHumanAddedEvent(ctx,
|
|
userAgg,
|
|
"username",
|
|
"firstname",
|
|
"lastname",
|
|
"nickname",
|
|
"displayname",
|
|
language.German,
|
|
domain.GenderUnspecified,
|
|
"[email protected]",
|
|
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",
|
|
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
eventstore: tt.fields.eventstore(t),
|
|
idGenerator: tt.fields.idGenerator,
|
|
webauthnConfig: webauthnConfig,
|
|
checkPermission: tt.fields.checkPermission,
|
|
}
|
|
_, err := c.RegisterUserPasskey(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.rpID, tt.args.authenticator)
|
|
require.ErrorIs(t, err, tt.wantErr)
|
|
// successful case can't be tested due to random challenge.
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
|
|
ctx := http_util.WithRequestedHost(context.Background(), "example.com")
|
|
webauthnConfig := &webauthn_helper.Config{
|
|
DisplayName: "test",
|
|
ExternalSecure: true,
|
|
}
|
|
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
|
es := expectEventstore(
|
|
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
|
)(t)
|
|
code, err := newEncryptedCode(ctx, es.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg) //nolint:staticcheck
|
|
require.NoError(t, err)
|
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
|
type fields struct {
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
idGenerator id.Generator
|
|
}
|
|
type args struct {
|
|
userID string
|
|
resourceOwner string
|
|
rpID string
|
|
authenticator domain.AuthenticatorAttachment
|
|
codeID string
|
|
code string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "code verification error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusherWithCreationDateNow(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg, "123", code.Crypted, time.Minute, "", false,
|
|
),
|
|
),
|
|
eventFromEventPusherWithCreationDateNow(
|
|
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
|
|
codeID: "123",
|
|
code: "wrong",
|
|
},
|
|
wantErr: zerrors.ThrowInvalidArgument(err, "COMMAND-Eeb2a", "Errors.User.Code.Invalid"),
|
|
},
|
|
{
|
|
name: "code verification ok, get human passwordless error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusherWithCreationDateNow(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg, "123", code.Crypted, time.Minute, "", false,
|
|
),
|
|
),
|
|
eventFromEventPusherWithCreationDateNow(
|
|
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
|
),
|
|
),
|
|
expectFilterError(io.ErrClosedPipe),
|
|
),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
|
|
codeID: "123",
|
|
code: code.Plain,
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
eventstore: tt.fields.eventstore(t),
|
|
idGenerator: tt.fields.idGenerator,
|
|
webauthnConfig: webauthnConfig,
|
|
}
|
|
_, err := c.RegisterUserPasskeyWithCode(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authenticator, tt.args.codeID, tt.args.code, tt.args.rpID, alg)
|
|
require.ErrorIs(t, err, tt.wantErr)
|
|
// successful case can't be tested due to random challenge.
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_verifyUserPasskeyCode(t *testing.T) {
|
|
ctx := http_util.WithRequestedHost(context.Background(), "example.com")
|
|
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
|
es := expectEventstore(
|
|
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
|
)(t)
|
|
code, err := newEncryptedCode(ctx, es.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg) //nolint:staticcheck
|
|
require.NoError(t, err)
|
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
|
|
|
type fields struct {
|
|
eventstore func(*testing.T) *eventstore.Eventstore
|
|
}
|
|
type args struct {
|
|
userID string
|
|
resourceOwner string
|
|
codeID string
|
|
code string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want *user.HumanPasswordlessInitCodeCheckSucceededEvent
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "filter error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilterError(io.ErrClosedPipe),
|
|
),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
codeID: "123",
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "code verification error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusherWithCreationDateNow(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg, "123", code.Crypted, time.Minute, "", false,
|
|
),
|
|
),
|
|
eventFromEventPusherWithCreationDateNow(
|
|
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
codeID: "123",
|
|
code: "wrong",
|
|
},
|
|
wantErr: zerrors.ThrowInvalidArgument(err, "COMMAND-Eeb2a", "Errors.User.Code.Invalid"),
|
|
},
|
|
{
|
|
name: "success",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusherWithCreationDateNow(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg, "123", code.Crypted, time.Minute, "", false,
|
|
),
|
|
),
|
|
eventFromEventPusherWithCreationDateNow(
|
|
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
codeID: "123",
|
|
code: code.Plain,
|
|
},
|
|
want: user.NewHumanPasswordlessInitCodeCheckSucceededEvent(ctx, userAgg, "123"),
|
|
},
|
|
{
|
|
name: "expired error",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusherWithCreationDate(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg, "123", code.Crypted, time.Minute, "", false,
|
|
),
|
|
time.Now().Add(-2*time.Minute),
|
|
),
|
|
eventFromEventPusherWithCreationDate(
|
|
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
|
time.Now().Add(-2*time.Minute),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
codeID: "123",
|
|
code: code.Plain,
|
|
},
|
|
wantErr: zerrors.ThrowInvalidArgument(err, "COMMAND-Eeb2a", "Errors.User.Code.Invalid"),
|
|
},
|
|
{
|
|
// https://github.com/zitadel/zitadel/security/advisories/GHSA-2x66-r53r-9r86
|
|
name: "expired, fail, check again",
|
|
fields: fields{
|
|
eventstore: expectEventstore(
|
|
expectFilter(
|
|
eventFromEventPusherWithCreationDate(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg, "123", code.Crypted, time.Minute, "", false,
|
|
),
|
|
time.Now().Add(-2*time.Minute),
|
|
),
|
|
eventFromEventPusherWithCreationDate(
|
|
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
|
time.Now().Add(-2*time.Minute),
|
|
),
|
|
eventFromEventPusherWithCreationDateNow(
|
|
user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"),
|
|
),
|
|
),
|
|
expectPush(
|
|
user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"),
|
|
),
|
|
),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
codeID: "123",
|
|
code: code.Plain,
|
|
},
|
|
wantErr: zerrors.ThrowInvalidArgument(err, "COMMAND-Eeb2a", "Errors.User.Code.Invalid"),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
eventstore: tt.fields.eventstore(t),
|
|
}
|
|
got, err := c.verifyUserPasskeyCode(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.codeID, tt.args.code, alg)
|
|
require.ErrorIs(t, err, tt.wantErr)
|
|
if tt.wantErr == nil {
|
|
assert.Equal(t, tt.want, got(ctx, userAgg))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_pushUserPasskey(t *testing.T) {
|
|
ctx := http_util.WithRequestedHost(authz.NewMockContext("instance1", "org1", "user1"), "example.com")
|
|
webauthnConfig := &webauthn_helper.Config{
|
|
DisplayName: "test",
|
|
ExternalSecure: true,
|
|
}
|
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
|
|
|
prep := []expect{
|
|
expectFilter(), // getHumanPasswordlessTokens
|
|
expectFilter(eventFromEventPusher(
|
|
user.NewHumanAddedEvent(ctx,
|
|
userAgg,
|
|
"username",
|
|
"firstname",
|
|
"lastname",
|
|
"nickname",
|
|
"displayname",
|
|
language.German,
|
|
domain.GenderUnspecified,
|
|
"[email protected]",
|
|
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", "rpID"),
|
|
)),
|
|
}
|
|
|
|
type args struct {
|
|
events []eventCallback
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
expectPush func(challenge string) expect
|
|
args args
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "push error",
|
|
expectPush: func(challenge string) expect {
|
|
return expectPushFailed(io.ErrClosedPipe,
|
|
user.NewHumanPasswordlessAddedEvent(ctx,
|
|
userAgg, "123", challenge, "rpID",
|
|
),
|
|
)
|
|
},
|
|
args: args{},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "success",
|
|
expectPush: func(challenge string) expect {
|
|
return expectPush(
|
|
user.NewHumanPasswordlessAddedEvent(ctx,
|
|
userAgg, "123", challenge, "rpID",
|
|
),
|
|
)
|
|
},
|
|
args: args{},
|
|
},
|
|
{
|
|
name: "initcode succeeded event",
|
|
expectPush: func(challenge string) expect {
|
|
return expectPush(
|
|
user.NewHumanPasswordlessAddedEvent(ctx,
|
|
userAgg, "123", challenge, "rpID",
|
|
),
|
|
user.NewHumanPasswordlessInitCodeCheckSucceededEvent(ctx, userAgg, "123"),
|
|
)
|
|
},
|
|
args: args{
|
|
events: []eventCallback{func(ctx context.Context, userAgg *eventstore.Aggregate) eventstore.Command {
|
|
return user.NewHumanPasswordlessInitCodeCheckSucceededEvent(ctx, userAgg, "123")
|
|
}},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
eventstore: expectEventstore(prep...)(t),
|
|
webauthnConfig: webauthnConfig,
|
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
|
}
|
|
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, "user1", "org1", "rpID", domain.AuthenticatorAttachmentCrossPlattform)
|
|
require.NoError(t, err)
|
|
|
|
c.eventstore = expectEventstore(tt.expectPush(webAuthN.Challenge))(t)
|
|
|
|
got, err := c.pushUserPasskey(ctx, wm, userAgg, webAuthN, tt.args.events...)
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_AddUserPasskeyCode(t *testing.T) {
|
|
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
|
type fields struct {
|
|
newCode encrypedCodeFunc
|
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
|
idGenerator id.Generator
|
|
loginPaths func(*testing.T) LoginPaths
|
|
}
|
|
type args struct {
|
|
userID string
|
|
resourceOwner string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want *domain.ObjectDetails
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "id generator error",
|
|
fields: fields{
|
|
newCode: mockEncryptedCode("passkey1", time.Hour),
|
|
eventstore: expectEventstore(),
|
|
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
|
|
loginPaths: expectLoginPathsNoCall,
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "success",
|
|
fields: fields{
|
|
newCode: mockEncryptedCode("passkey1", time.Minute),
|
|
eventstore: expectEventstore(
|
|
expectFilter(eventFromEventPusher(
|
|
user.NewHumanAddedEvent(context.Background(),
|
|
userAgg,
|
|
"username",
|
|
"firstname",
|
|
"lastname",
|
|
"nickname",
|
|
"displayname",
|
|
language.German,
|
|
domain.GenderUnspecified,
|
|
"[email protected]",
|
|
true,
|
|
),
|
|
)),
|
|
expectPush(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg,
|
|
"123", &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("passkey1"),
|
|
}, time.Minute, "", false,
|
|
),
|
|
),
|
|
),
|
|
loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate(""),
|
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
want: &domain.ObjectDetails{
|
|
ResourceOwner: "org1",
|
|
},
|
|
},
|
|
{
|
|
name: "success, url template",
|
|
fields: fields{
|
|
newCode: mockEncryptedCode("passkey1", time.Minute),
|
|
eventstore: expectEventstore(
|
|
expectFilter(eventFromEventPusher(
|
|
user.NewHumanAddedEvent(context.Background(),
|
|
userAgg,
|
|
"username",
|
|
"firstname",
|
|
"lastname",
|
|
"nickname",
|
|
"displayname",
|
|
language.German,
|
|
domain.GenderUnspecified,
|
|
"[email protected]",
|
|
true,
|
|
),
|
|
)),
|
|
expectPush(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg,
|
|
"123", &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("passkey1"),
|
|
}, time.Minute, "http://example.com/{{.user}}/email/{{.code}}", false,
|
|
),
|
|
),
|
|
),
|
|
loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate("http://example.com/{{.user}}/email/{{.code}}"),
|
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
want: &domain.ObjectDetails{
|
|
ResourceOwner: "org1",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
newEncryptedCode: tt.fields.newCode,
|
|
eventstore: tt.fields.eventstore(t),
|
|
idGenerator: tt.fields.idGenerator,
|
|
loginPaths: tt.fields.loginPaths(t),
|
|
}
|
|
got, err := c.AddUserPasskeyCode(context.Background(), tt.args.userID, tt.args.resourceOwner, alg)
|
|
require.ErrorIs(t, err, tt.wantErr)
|
|
assertObjectDetails(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_AddUserPasskeyCodeURLTemplate(t *testing.T) {
|
|
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
|
|
|
type fields struct {
|
|
newCode encrypedCodeFunc
|
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
|
idGenerator id.Generator
|
|
}
|
|
type args struct {
|
|
userID string
|
|
resourceOwner string
|
|
urlTmpl string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want *domain.ObjectDetails
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "template error",
|
|
fields: fields{
|
|
newCode: newEncryptedCode,
|
|
eventstore: expectEventstore(),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
urlTmpl: "{{",
|
|
},
|
|
wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
|
},
|
|
{
|
|
name: "id generator error",
|
|
fields: fields{
|
|
newCode: newEncryptedCode,
|
|
eventstore: expectEventstore(),
|
|
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
urlTmpl: "https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}",
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "success",
|
|
fields: fields{
|
|
newCode: mockEncryptedCode("passkey1", time.Minute),
|
|
eventstore: expectEventstore(
|
|
expectFilter(eventFromEventPusher(
|
|
user.NewHumanAddedEvent(context.Background(),
|
|
userAgg,
|
|
"username",
|
|
"firstname",
|
|
"lastname",
|
|
"nickname",
|
|
"displayname",
|
|
language.German,
|
|
domain.GenderUnspecified,
|
|
"[email protected]",
|
|
true,
|
|
),
|
|
)),
|
|
expectPush(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg,
|
|
"123", &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("passkey1"),
|
|
},
|
|
time.Minute,
|
|
"https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}",
|
|
false,
|
|
),
|
|
),
|
|
),
|
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
urlTmpl: "https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}",
|
|
},
|
|
want: &domain.ObjectDetails{
|
|
ResourceOwner: "org1",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
newEncryptedCode: tt.fields.newCode,
|
|
eventstore: tt.fields.eventstore(t),
|
|
idGenerator: tt.fields.idGenerator,
|
|
}
|
|
got, err := c.AddUserPasskeyCodeURLTemplate(context.Background(), tt.args.userID, tt.args.resourceOwner, alg, tt.args.urlTmpl)
|
|
require.ErrorIs(t, err, tt.wantErr)
|
|
assertObjectDetails(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_AddUserPasskeyCodeReturn(t *testing.T) {
|
|
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
|
type fields struct {
|
|
newCode encrypedCodeFunc
|
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
|
idGenerator id.Generator
|
|
loginPaths func(*testing.T) LoginPaths
|
|
}
|
|
type args struct {
|
|
userID string
|
|
resourceOwner string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want *domain.PasskeyCodeDetails
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "id generator error",
|
|
fields: fields{
|
|
newCode: newEncryptedCode,
|
|
eventstore: expectEventstore(),
|
|
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
|
|
loginPaths: expectLoginPathsNoCall,
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "success",
|
|
fields: fields{
|
|
newCode: mockEncryptedCode("passkey1", time.Minute),
|
|
eventstore: expectEventstore(
|
|
expectFilter(eventFromEventPusher(
|
|
user.NewHumanAddedEvent(context.Background(),
|
|
userAgg,
|
|
"username",
|
|
"firstname",
|
|
"lastname",
|
|
"nickname",
|
|
"displayname",
|
|
language.German,
|
|
domain.GenderUnspecified,
|
|
"[email protected]",
|
|
true,
|
|
),
|
|
)),
|
|
expectPush(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg,
|
|
"123", &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("passkey1"),
|
|
}, time.Minute, "", true,
|
|
),
|
|
),
|
|
),
|
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
|
loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate(""),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
want: &domain.PasskeyCodeDetails{
|
|
ObjectDetails: &domain.ObjectDetails{
|
|
ResourceOwner: "org1",
|
|
},
|
|
CodeID: "123",
|
|
Code: "passkey1",
|
|
},
|
|
},
|
|
{
|
|
name: "success, url template",
|
|
fields: fields{
|
|
newCode: mockEncryptedCode("passkey1", time.Minute),
|
|
eventstore: expectEventstore(
|
|
expectFilter(eventFromEventPusher(
|
|
user.NewHumanAddedEvent(context.Background(),
|
|
userAgg,
|
|
"username",
|
|
"firstname",
|
|
"lastname",
|
|
"nickname",
|
|
"displayname",
|
|
language.German,
|
|
domain.GenderUnspecified,
|
|
"[email protected]",
|
|
true,
|
|
),
|
|
)),
|
|
expectPush(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg,
|
|
"123", &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("passkey1"),
|
|
}, time.Minute, "http://example.com/{{.user}}/email/{{.code}}", true,
|
|
),
|
|
),
|
|
),
|
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
|
loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate("http://example.com/{{.user}}/email/{{.code}}"),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
want: &domain.PasskeyCodeDetails{
|
|
ObjectDetails: &domain.ObjectDetails{
|
|
ResourceOwner: "org1",
|
|
},
|
|
CodeID: "123",
|
|
Code: "passkey1",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
newEncryptedCode: tt.fields.newCode,
|
|
eventstore: tt.fields.eventstore(t),
|
|
idGenerator: tt.fields.idGenerator,
|
|
loginPaths: tt.fields.loginPaths(t),
|
|
}
|
|
got, err := c.AddUserPasskeyCodeReturn(context.Background(), tt.args.userID, tt.args.resourceOwner, alg)
|
|
if tt.wantErr != nil {
|
|
require.ErrorIs(t, err, tt.wantErr)
|
|
return
|
|
}
|
|
assert.Equal(t, tt.want.CodeID, got.CodeID)
|
|
assert.Equal(t, tt.want.Code, got.Code)
|
|
assertObjectDetails(t, tt.want.ObjectDetails, got.ObjectDetails)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_addUserPasskeyCode(t *testing.T) {
|
|
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
|
type fields struct {
|
|
newCode encrypedCodeFunc
|
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
|
idGenerator id.Generator
|
|
loginPaths func(*testing.T) LoginPaths
|
|
}
|
|
type args struct {
|
|
userID string
|
|
resourceOwner string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want *domain.PasskeyCodeDetails
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "id generator error",
|
|
fields: fields{
|
|
newCode: newEncryptedCode,
|
|
eventstore: expectEventstore(),
|
|
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
|
|
loginPaths: expectLoginPathsNoCall,
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "crypto error",
|
|
fields: fields{
|
|
newCode: newEncryptedCode,
|
|
eventstore: expectEventstore(expectFilterError(io.ErrClosedPipe)),
|
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
|
loginPaths: expectLoginPathsNoCall,
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "filter query error",
|
|
fields: fields{
|
|
newCode: newEncryptedCode,
|
|
eventstore: expectEventstore(
|
|
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
|
expectFilterError(io.ErrClosedPipe),
|
|
),
|
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
|
loginPaths: expectLoginPathsNoCall,
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "push error",
|
|
fields: fields{
|
|
newCode: mockEncryptedCode("passkey1", time.Minute),
|
|
eventstore: expectEventstore(
|
|
expectFilter(eventFromEventPusher(
|
|
user.NewHumanAddedEvent(context.Background(),
|
|
userAgg,
|
|
"username",
|
|
"firstname",
|
|
"lastname",
|
|
"nickname",
|
|
"displayname",
|
|
language.German,
|
|
domain.GenderUnspecified,
|
|
"[email protected]",
|
|
true,
|
|
),
|
|
)),
|
|
expectPushFailed(io.ErrClosedPipe,
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
&user.NewAggregate("user1", "org1").Aggregate,
|
|
"123", &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("passkey1"),
|
|
}, time.Minute, "", false,
|
|
),
|
|
),
|
|
),
|
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
|
loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate(""),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
wantErr: io.ErrClosedPipe,
|
|
},
|
|
{
|
|
name: "success",
|
|
fields: fields{
|
|
newCode: mockEncryptedCode("passkey1", time.Minute),
|
|
eventstore: expectEventstore(
|
|
expectFilter(eventFromEventPusher(
|
|
user.NewHumanAddedEvent(context.Background(),
|
|
userAgg,
|
|
"username",
|
|
"firstname",
|
|
"lastname",
|
|
"nickname",
|
|
"displayname",
|
|
language.German,
|
|
domain.GenderUnspecified,
|
|
"[email protected]",
|
|
true,
|
|
),
|
|
)),
|
|
expectPush(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg,
|
|
"123", &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("passkey1"),
|
|
}, time.Minute, "", false,
|
|
),
|
|
),
|
|
),
|
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
|
loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate(""),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
want: &domain.PasskeyCodeDetails{
|
|
ObjectDetails: &domain.ObjectDetails{
|
|
ResourceOwner: "org1",
|
|
},
|
|
CodeID: "123",
|
|
Code: "passkey1",
|
|
},
|
|
}, {
|
|
name: "success, url template",
|
|
fields: fields{
|
|
newCode: mockEncryptedCode("passkey1", time.Minute),
|
|
eventstore: expectEventstore(
|
|
expectFilter(eventFromEventPusher(
|
|
user.NewHumanAddedEvent(context.Background(),
|
|
userAgg,
|
|
"username",
|
|
"firstname",
|
|
"lastname",
|
|
"nickname",
|
|
"displayname",
|
|
language.German,
|
|
domain.GenderUnspecified,
|
|
"[email protected]",
|
|
true,
|
|
),
|
|
)),
|
|
expectPush(
|
|
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
|
userAgg,
|
|
"123", &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("passkey1"),
|
|
}, time.Minute, "http://example.com/{{.user}}/email/{{.code}}", false,
|
|
),
|
|
),
|
|
),
|
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
|
loginPaths: expectLoginPathsDefaultPasskeySetURLTemplate("http://example.com/{{.user}}/email/{{.code}}"),
|
|
},
|
|
args: args{
|
|
userID: "user1",
|
|
resourceOwner: "org1",
|
|
},
|
|
want: &domain.PasskeyCodeDetails{
|
|
ObjectDetails: &domain.ObjectDetails{
|
|
ResourceOwner: "org1",
|
|
},
|
|
CodeID: "123",
|
|
Code: "passkey1",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
newEncryptedCode: tt.fields.newCode,
|
|
eventstore: tt.fields.eventstore(t),
|
|
idGenerator: tt.fields.idGenerator,
|
|
loginPaths: tt.fields.loginPaths(t),
|
|
}
|
|
got, err := c.addUserPasskeyCode(context.Background(), tt.args.userID, tt.args.resourceOwner, alg, "", false)
|
|
if tt.wantErr != nil {
|
|
require.ErrorIs(t, err, tt.wantErr)
|
|
return
|
|
}
|
|
assert.Equal(t, tt.want.CodeID, got.CodeID)
|
|
assert.Equal(t, tt.want.Code, got.Code)
|
|
assertObjectDetails(t, tt.want.ObjectDetails, got.ObjectDetails)
|
|
})
|
|
}
|
|
}
|