Skip to content

Commit

Permalink
fix: import totp in add human user with secret (#7936)
Browse files Browse the repository at this point in the history
* fix: import totp in add human user with secret

* fix: import totp in add human user with secret

* fix: import totp in add human user with secret

* fix: review comment changes
  • Loading branch information
stebenz committed May 14, 2024
1 parent 15d5338 commit 0e9ebed
Show file tree
Hide file tree
Showing 13 changed files with 395 additions and 51 deletions.
217 changes: 178 additions & 39 deletions internal/api/grpc/session/v2/session_integration_test.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions internal/api/grpc/user/v2/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
Register: false,
Metadata: metadata,
Links: links,
TOTPSecret: req.GetTotpSecret(),
}, nil
}

Expand Down
46 changes: 46 additions & 0 deletions internal/api/grpc/user/v2/user_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,52 @@ func TestServer_AddHumanUser(t *testing.T) {
},
},
},
{
name: "with totp",
args: args{
CTX,
&user.AddHumanUserRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: Tester.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
GivenName: "Donald",
FamilyName: "Duck",
NickName: gu.Ptr("Dukkie"),
DisplayName: gu.Ptr("Donald Duck"),
PreferredLanguage: gu.Ptr("en"),
Gender: user.Gender_GENDER_DIVERSE.Enum(),
},
Email: &user.SetHumanEmail{
Email: "livio@zitadel.com",
Verification: &user.SetHumanEmail_IsVerified{
IsVerified: true,
},
},
Metadata: []*user.SetMetadataEntry{
{
Key: "somekey",
Value: []byte("somevalue"),
},
},
PasswordType: &user.AddHumanUserRequest_Password{
Password: &user.Password{
Password: "DifficultPW666!",
ChangeRequired: false,
},
},
TotpSecret: gu.Ptr("secret"),
},
},
want: &user.AddHumanUserResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "password not complexity conform",
args: args{
Expand Down
4 changes: 3 additions & 1 deletion internal/command/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,9 @@ func TestCheckTOTP(t *testing.T) {
ctx := authz.NewMockContext("instance1", "org1", "user1")

cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
key, secret, err := domain.NewTOTPKey("example.com", "user1", cryptoAlg)
key, err := domain.NewTOTPKey("example.com", "user1")
require.NoError(t, err)
secret, err := crypto.Encrypt([]byte(key.Secret()), cryptoAlg)
require.NoError(t, err)

sessAgg := &session.NewAggregate("session1", "instance1").Aggregate
Expand Down
3 changes: 3 additions & 0 deletions internal/command/user_human.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ type AddHuman struct {
// Links are optional
Links []*AddLink

// TOTPSecret is optional
TOTPSecret string

// Details are set after a successful execution of the command
Details *domain.ObjectDetails

Expand Down
8 changes: 6 additions & 2 deletions internal/command/user_human_otp.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st
if issuer == "" {
issuer = authz.GetInstance(ctx).RequestedDomain()
}
key, secret, err := domain.NewTOTPKey(issuer, accountName, c.multifactors.OTP.CryptoMFA)
key, err := domain.NewTOTPKey(issuer, accountName)
if err != nil {
return nil, err
}
encryptedSecret, err := crypto.Encrypt([]byte(key.Secret()), c.multifactors.OTP.CryptoMFA)
if err != nil {
return nil, err
}
Expand All @@ -115,7 +119,7 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st
userAgg: userAgg,
key: key,
cmds: []eventstore.Command{
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
user.NewHumanOTPAddedEvent(ctx, userAgg, encryptedSecret),
},
}, nil
}
Expand Down
4 changes: 3 additions & 1 deletion internal/command/user_human_otp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,9 @@ func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) {
ctx := authz.NewMockContext("", "org1", "user1")

cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
key, secret, err := domain.NewTOTPKey("example.com", "user1", cryptoAlg)
key, err := domain.NewTOTPKey("example.com", "user1")
require.NoError(t, err)
secret, err := crypto.Encrypt([]byte(key.Secret()), cryptoAlg)
require.NoError(t, err)
userAgg := &user.NewAggregate("user1", "org1").Aggregate
userAgg2 := &user.NewAggregate("user2", "org1").Aggregate
Expand Down
11 changes: 11 additions & 0 deletions internal/command/user_v2_human.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human
cmds = append(cmds, cmd)
}

if human.TOTPSecret != "" {
encryptedSecret, err := crypto.Encrypt([]byte(human.TOTPSecret), c.multifactors.OTP.CryptoMFA)
if err != nil {
return err
}
cmds = append(cmds,
user.NewHumanOTPAddedEvent(ctx, &existingHuman.Aggregate().Aggregate, encryptedSecret),
user.NewHumanOTPVerifiedEvent(ctx, &existingHuman.Aggregate().Aggregate, ""),
)
}

if len(cmds) == 0 {
human.Details = writeModelToObjectDetails(&existingHuman.WriteModel)
return nil
Expand Down
95 changes: 95 additions & 0 deletions internal/command/user_v2_human_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"golang.org/x/text/language"

Expand Down Expand Up @@ -47,6 +48,11 @@ func TestCommandSide_AddUserHuman(t *testing.T) {

userAgg := user.NewAggregate("user1", "org1")

cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
totpSecret := "TOTPSecret"
totpSecretEnc, err := crypto.Encrypt([]byte(totpSecret), cryptoAlg)
require.NoError(t, err)

tests := []struct {
name string
fields fields
Expand Down Expand Up @@ -1394,6 +1400,89 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
wantID: "user1",
},
},
{
name: "register human with TOTPSecret, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&userAgg.Aggregate,
true,
true,
true,
),
),
),
expectPush(
user.NewHumanRegisteredEvent(context.Background(),
&userAgg.Aggregate,
"username",
"firstname",
"lastname",
"",
"firstname lastname",
language.English,
domain.GenderUnspecified,
"email@test.ch",
true,
"userAgentID",
),
user.NewHumanInitialCodeAddedEvent(context.Background(),
&userAgg.Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("userinit"),
},
time.Hour*1,
"authRequestID",
),
user.NewHumanOTPAddedEvent(context.Background(),
&userAgg.Aggregate,
totpSecretEnc,
),
user.NewHumanOTPVerifiedEvent(context.Background(),
&userAgg.Aggregate,
"",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
newCode: mockEncryptedCode("userinit", time.Hour),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@test.ch",
},
PreferredLanguage: language.English,
Register: true,
UserAgentID: "userAgentID",
AuthRequestID: "authRequestID",
TOTPSecret: totpSecret,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res: res{
want: &domain.ObjectDetails{
Sequence: 0,
EventDate: time.Time{},
ResourceOwner: "org1",
},
wantID: "user1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -1403,6 +1492,12 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
idGenerator: tt.fields.idGenerator,
newEncryptedCode: tt.fields.newCode,
checkPermission: tt.fields.checkPermission,
multifactors: domain.MultifactorConfigs{
OTP: domain.OTPConfig{
Issuer: "zitadel.com",
CryptoMFA: cryptoAlg,
},
},
}
err := r.AddUserHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail, tt.args.codeAlg)
if tt.res.err == nil {
Expand Down
5 changes: 4 additions & 1 deletion internal/command/user_v2_totp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,11 @@ func TestCommands_CheckUserTOTP(t *testing.T) {
ctx := authz.NewMockContext("", "org1", "user1")

cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
key, secret, err := domain.NewTOTPKey("example.com", "user1", cryptoAlg)
key, err := domain.NewTOTPKey("example.com", "user1")
require.NoError(t, err)
secret, err := crypto.Encrypt([]byte(key.Secret()), cryptoAlg)
require.NoError(t, err)

userAgg := &user.NewAggregate("user1", "org1").Aggregate

code, err := totp.GenerateCode(key.Secret(), time.Now())
Expand Down
10 changes: 3 additions & 7 deletions internal/domain/human_otp.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,12 @@ type TOTP struct {
URI string
}

func NewTOTPKey(issuer, accountName string, cryptoAlg crypto.EncryptionAlgorithm) (*otp.Key, *crypto.CryptoValue, error) {
func NewTOTPKey(issuer, accountName string) (*otp.Key, error) {
key, err := totp.Generate(totp.GenerateOpts{Issuer: issuer, AccountName: accountName})
if err != nil {
return nil, nil, zerrors.ThrowInternal(err, "TOTP-ieY3o", "Errors.Internal")
return nil, zerrors.ThrowInternal(err, "TOTP-ieY3o", "Errors.Internal")
}
encryptedSecret, err := crypto.Encrypt([]byte(key.Secret()), cryptoAlg)
if err != nil {
return nil, nil, err
}
return key, encryptedSecret, nil
return key, nil
}

func VerifyTOTP(code string, secret *crypto.CryptoValue, cryptoAlg crypto.EncryptionAlgorithm) error {
Expand Down
31 changes: 31 additions & 0 deletions internal/integration/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,37 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse
return resp
}

func (s *Tester) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user.AddHumanUserResponse {
resp, err := s.Client.UserV2.AddHumanUser(ctx, &user.AddHumanUserRequest{
Organization: &object.Organization{
Org: &object.Organization_OrgId{
OrgId: s.Organisation.ID,
},
},
Profile: &user.SetHumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
PreferredLanguage: gu.Ptr("nl"),
Gender: gu.Ptr(user.Gender_GENDER_MALE),
},
Email: &user.SetHumanEmail{
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
Verification: &user.SetHumanEmail_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
},
},
Phone: &user.SetHumanPhone{
Phone: "+41791234567",
Verification: &user.SetHumanPhone_ReturnCode{
ReturnCode: &user.ReturnPhoneVerificationCode{},
},
},
TotpSecret: gu.Ptr(secret),
})
logging.OnError(err).Fatal("create human user")
return resp
}

func (s *Tester) CreateOrganization(ctx context.Context, name, adminEmail string) *org.AddOrganizationResponse {
resp, err := s.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{
Name: name,
Expand Down
11 changes: 11 additions & 0 deletions proto/zitadel/user/v2beta/user_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,17 @@ message AddHumanUserRequest{
HashedPassword hashed_password = 8;
}
repeated IDPLink idp_links = 9;

// An Implementation of RFC 6238 is used, with HMAC-SHA-1 and time-step of 30 seconds.
// Currently no other options are supported, and if anything different is used the validation will fail.
optional string totp_secret = 12 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1;
max_length: 200;
example: "\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\"";
}
];
}

message AddHumanUserResponse {
Expand Down

0 comments on commit 0e9ebed

Please sign in to comment.