mirror of
https://github.com/zitadel/zitadel.git
synced 2025-04-15 20:41:30 +00:00
feat(v2): implement user register OTP (#6030)
* feat(v2): implement user register OTP * feat(v2): implement user verify OTP * session: retry on permission denied
This commit is contained in:
parent
4eaf3fb21e
commit
09aafb35eb
38
internal/api/grpc/user/v2/otp.go
Normal file
38
internal/api/grpc/user/v2/otp.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) RegisterOTP(ctx context.Context, req *user.RegisterOTPRequest) (*user.RegisterOTPResponse, error) {
|
||||||
|
return otpDetailsToPb(
|
||||||
|
s.command.AddUserOTP(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func otpDetailsToPb(otp *domain.OTPv2, err error) (*user.RegisterOTPResponse, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user.RegisterOTPResponse{
|
||||||
|
Details: object.DomainToDetailsPb(otp.ObjectDetails),
|
||||||
|
Uri: otp.URI,
|
||||||
|
Secret: otp.Secret,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) VerifyOTPRegistration(ctx context.Context, req *user.VerifyOTPRegistrationRequest) (*user.VerifyOTPRegistrationResponse, error) {
|
||||||
|
objectDetails, err := s.command.CheckUserOTP(ctx, req.GetUserId(), req.GetCode(), authz.GetCtxData(ctx).ResourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user.VerifyOTPRegistrationResponse{
|
||||||
|
Details: object.DomainToDetailsPb(objectDetails),
|
||||||
|
}, nil
|
||||||
|
}
|
155
internal/api/grpc/user/v2/otp_integration_test.go
Normal file
155
internal/api/grpc/user/v2/otp_integration_test.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServer_RegisterOTP(t *testing.T) {
|
||||||
|
// userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
req *user.RegisterOTPRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *user.RegisterOTPResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing user id",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.RegisterOTPRequest{},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user mismatch",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.RegisterOTPRequest{
|
||||||
|
UserId: "wrong",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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.RegisterOTPRequest{
|
||||||
|
UserId: userID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.RegisterOTPResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := Client.RegisterOTP(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)
|
||||||
|
assert.NotEmpty(t, got.GetUri())
|
||||||
|
assert.NotEmpty(t, got.GetSecret())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_VerifyOTPRegistration(t *testing.T) {
|
||||||
|
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||||
|
|
||||||
|
/* TODO: after we are able to obtain a Bearer token for a human user
|
||||||
|
reg, err := Client.RegisterOTP(CTX, &user.RegisterOTPRequest{
|
||||||
|
UserId: userID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
code, err := totp.GenerateCode(reg.Secret, time.Now())
|
||||||
|
require.NoError(t, err)
|
||||||
|
*/
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
req *user.VerifyOTPRegistrationRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *user.VerifyOTPRegistrationResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "user mismatch",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.VerifyOTPRegistrationRequest{
|
||||||
|
UserId: "wrong",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong code",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.VerifyOTPRegistrationRequest{
|
||||||
|
UserId: userID,
|
||||||
|
Code: "123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
/* TODO: after we are able to obtain a Bearer token for a human user
|
||||||
|
https://github.com/zitadel/zitadel/issues/6022
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.VerifyOTPRegistrationRequest{
|
||||||
|
UserId: userID,
|
||||||
|
Code: code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.VerifyOTPRegistrationResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
ResourceOwner: Tester.Organisation.ResourceOwner,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := Client.VerifyOTPRegistration(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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
71
internal/api/grpc/user/v2/otp_test.go
Normal file
71
internal/api/grpc/user/v2/otp_test.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_otpDetailsToPb(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
otp *domain.OTPv2
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *user.RegisterOTPResponse
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "error",
|
||||||
|
args: args{
|
||||||
|
err: io.ErrClosedPipe,
|
||||||
|
},
|
||||||
|
wantErr: io.ErrClosedPipe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
args: args{
|
||||||
|
otp: &domain.OTPv2{
|
||||||
|
ObjectDetails: &domain.ObjectDetails{
|
||||||
|
Sequence: 123,
|
||||||
|
EventDate: time.Unix(456, 789),
|
||||||
|
ResourceOwner: "me",
|
||||||
|
},
|
||||||
|
Secret: "secret",
|
||||||
|
URI: "URI",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.RegisterOTPResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: 123,
|
||||||
|
ChangeDate: ×tamppb.Timestamp{
|
||||||
|
Seconds: 456,
|
||||||
|
Nanos: 789,
|
||||||
|
},
|
||||||
|
ResourceOwner: "me",
|
||||||
|
},
|
||||||
|
Secret: "secret",
|
||||||
|
Uri: "URI",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := otpDetailsToPb(tt.args.otp, tt.args.err)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
if !proto.Equal(tt.want, got) {
|
||||||
|
t.Errorf("RegisterOTPResponse =\n%v\nwant\n%v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -125,6 +125,18 @@ func expectPushFailed(err error, events []*repository.Event, uniqueConstraints .
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func expectRandomPush(events []*repository.Event, uniqueConstraints ...*repository.UniqueConstraint) expect {
|
||||||
|
return func(m *mock.MockRepository) {
|
||||||
|
m.ExpectRandomPush(events, uniqueConstraints...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectRandomPushFailed(err error, events []*repository.Event, uniqueConstraints ...*repository.UniqueConstraint) expect {
|
||||||
|
return func(m *mock.MockRepository) {
|
||||||
|
m.ExpectRandomPushFailed(err, events, uniqueConstraints...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func expectFilter(events ...*repository.Event) expect {
|
func expectFilter(events ...*repository.Event) expect {
|
||||||
return func(m *mock.MockRepository) {
|
return func(m *mock.MockRepository) {
|
||||||
m.ExpectFilterEvents(events...)
|
m.ExpectFilterEvents(events...)
|
||||||
|
@ -3,12 +3,14 @@ package command
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/pquerna/otp"
|
||||||
"github.com/zitadel/logging"
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
"github.com/zitadel/zitadel/internal/repository/user"
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
@ -43,7 +45,32 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
|
|||||||
if userID == "" {
|
if userID == "" {
|
||||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
|
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
|
||||||
}
|
}
|
||||||
human, err := c.getHuman(ctx, userID, resourceowner)
|
prep, err := c.createHumanOTP(ctx, userID, resourceowner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = c.eventstore.Push(ctx, prep.cmds...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &domain.OTP{
|
||||||
|
ObjectRoot: models.ObjectRoot{
|
||||||
|
AggregateID: prep.userAgg.ID,
|
||||||
|
},
|
||||||
|
SecretString: prep.key.Secret(),
|
||||||
|
Url: prep.key.URL(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type preparedOTP struct {
|
||||||
|
wm *HumanOTPWriteModel
|
||||||
|
userAgg *eventstore.Aggregate
|
||||||
|
key *otp.Key
|
||||||
|
cmds []eventstore.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) createHumanOTP(ctx context.Context, userID, resourceOwner string) (*preparedOTP, error) {
|
||||||
|
human, err := c.getHuman(ctx, userID, resourceOwner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Log("COMMAND-DAqe1").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get human for loginname")
|
logging.Log("COMMAND-DAqe1").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get human for loginname")
|
||||||
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-MM9fs", "Errors.User.NotFound")
|
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-MM9fs", "Errors.User.NotFound")
|
||||||
@ -59,7 +86,7 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
|
|||||||
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-8ugTs", "Errors.Org.DomainPolicy.NotFound")
|
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-8ugTs", "Errors.Org.DomainPolicy.NotFound")
|
||||||
}
|
}
|
||||||
|
|
||||||
otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceowner)
|
otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceOwner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -80,16 +107,13 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_, err = c.eventstore.Push(ctx, user.NewHumanOTPAddedEvent(ctx, userAgg, secret))
|
return &preparedOTP{
|
||||||
if err != nil {
|
wm: otpWriteModel,
|
||||||
return nil, err
|
userAgg: userAgg,
|
||||||
}
|
key: key,
|
||||||
return &domain.OTP{
|
cmds: []eventstore.Command{
|
||||||
ObjectRoot: models.ObjectRoot{
|
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||||
AggregateID: human.AggregateID,
|
|
||||||
},
|
},
|
||||||
SecretString: key.Secret(),
|
|
||||||
Url: key.URL(),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,17 @@ package command
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
@ -231,6 +237,476 @@ func TestCommandSide_AddHumanOTP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommands_createHumanOTP(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
userID string
|
||||||
|
resourceOwner string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "user not existing, not found error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-MM9fs", "Errors.User.NotFound"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "org not existing, not found error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.German,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-55M9f", "Errors.Org.NotFound"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "org iam policy not existing, not found error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.German,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewOrgAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("org1", "org1").Aggregate,
|
||||||
|
"org",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(),
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8ugTs", "Errors.Org.DomainPolicy.NotFound"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "otp already exists, already exists error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.German,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewOrgAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("org1", "org1").Aggregate,
|
||||||
|
"org",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("org1").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte("a"),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPVerifiedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"agent1")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowAlreadyExists(nil, "COMMAND-do9se", "Errors.User.MFA.OTP.AlreadyReady"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "issuer not in context",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.German,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewOrgAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("org1", "org1").Aggregate,
|
||||||
|
"org",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("org1").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowInternal(nil, "TOTP-ieY3o", "Errors.Internal"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.German,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewOrgAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("org1", "org1").Aggregate,
|
||||||
|
"org",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("org1").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithRequestedDomain(context.Background(), "zitadel.com"),
|
||||||
|
resourceOwner: "org1",
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore,
|
||||||
|
multifactors: domain.MultifactorConfigs{
|
||||||
|
OTP: domain.OTPConfig{
|
||||||
|
CryptoMFA: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
got, err := c.createHumanOTP(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
if tt.want {
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.NotNil(t, got.wm)
|
||||||
|
assert.NotNil(t, got.userAgg)
|
||||||
|
require.NotNil(t, got.key)
|
||||||
|
assert.NotEmpty(t, got.key.URL())
|
||||||
|
assert.NotEmpty(t, got.key.Secret())
|
||||||
|
assert.Len(t, got.cmds, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_HumanCheckMFAOTPSetup(t *testing.T) {
|
||||||
|
ctx := authz.NewMockContext("inst1", "org1", "user1")
|
||||||
|
|
||||||
|
cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||||
|
key, secret, err := domain.NewOTPKey("example.com", "user1", cryptoAlg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||||
|
|
||||||
|
code, err := totp.GenerateCode(key.Secret(), time.Now())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
userID string
|
||||||
|
code string
|
||||||
|
resourceOwner string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing user id",
|
||||||
|
args: args{},
|
||||||
|
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filter error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(t,
|
||||||
|
expectFilterError(io.ErrClosedPipe),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
userID: "user1",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
wantErr: io.ErrClosedPipe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "otp not existing error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPRemovedEvent(ctx, userAgg),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
resourceOwner: "org1",
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotExisting"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "otp already ready error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPVerifiedEvent(context.Background(),
|
||||||
|
userAgg,
|
||||||
|
"agent1",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
resourceOwner: "org1",
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-qx4ls", "Errors.Users.MFA.OTP.AlreadyReady"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong code",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
resourceOwner: "org1",
|
||||||
|
code: "wrong",
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowInvalidArgument(nil, "EVENT-8isk2", "Errors.User.MFA.OTP.InvalidCode"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "push error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPushFailed(io.ErrClosedPipe,
|
||||||
|
[]*repository.Event{eventFromEventPusher(
|
||||||
|
user.NewHumanOTPVerifiedEvent(ctx,
|
||||||
|
userAgg,
|
||||||
|
"agent1",
|
||||||
|
),
|
||||||
|
)},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
resourceOwner: "org1",
|
||||||
|
code: code,
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
wantErr: io.ErrClosedPipe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush([]*repository.Event{eventFromEventPusher(
|
||||||
|
user.NewHumanOTPVerifiedEvent(ctx,
|
||||||
|
userAgg,
|
||||||
|
"agent1",
|
||||||
|
),
|
||||||
|
)}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
resourceOwner: "org1",
|
||||||
|
code: code,
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore,
|
||||||
|
multifactors: domain.MultifactorConfigs{
|
||||||
|
OTP: domain.OTPConfig{
|
||||||
|
CryptoMFA: cryptoAlg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
got, err := c.HumanCheckMFAOTPSetup(ctx, tt.args.userID, tt.args.code, "agent1", tt.args.resourceOwner)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
if tt.want {
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, "org1", got.ResourceOwner)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCommandSide_RemoveHumanOTP(t *testing.T) {
|
func TestCommandSide_RemoveHumanOTP(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
|
33
internal/command/user_v2_otp.go
Normal file
33
internal/command/user_v2_otp.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Commands) AddUserOTP(ctx context.Context, userID, resourceowner string) (*domain.OTPv2, error) {
|
||||||
|
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
prep, err := c.createHumanOTP(ctx, userID, resourceowner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = c.pushAppendAndReduce(ctx, prep.wm, prep.cmds...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &domain.OTPv2{
|
||||||
|
ObjectDetails: writeModelToObjectDetails(&prep.wm.WriteModel),
|
||||||
|
Secret: prep.key.Secret(),
|
||||||
|
URI: prep.key.URL(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) CheckUserOTP(ctx context.Context, userID, code, resourceOwner string) (*domain.ObjectDetails, error) {
|
||||||
|
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.HumanCheckMFAOTPSetup(ctx, userID, code, "", resourceOwner)
|
||||||
|
}
|
263
internal/command/user_v2_otp_test.go
Normal file
263
internal/command/user_v2_otp_test.go
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
"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/crypto"
|
||||||
|
"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/repository/org"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommands_AddUserOTP(t *testing.T) {
|
||||||
|
ctx := authz.NewMockContext("inst1", "org1", "user1")
|
||||||
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
userID string
|
||||||
|
resourceowner string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "wrong user",
|
||||||
|
args: args{
|
||||||
|
userID: "foo",
|
||||||
|
resourceowner: "org1",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create otp error",
|
||||||
|
args: args{
|
||||||
|
userID: "user1",
|
||||||
|
resourceowner: "org1",
|
||||||
|
},
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-MM9fs", "Errors.User.NotFound"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "push error",
|
||||||
|
args: args{
|
||||||
|
userID: "user1",
|
||||||
|
resourceowner: "org1",
|
||||||
|
},
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(ctx,
|
||||||
|
userAgg,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.German,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewOrgAddedEvent(ctx,
|
||||||
|
userAgg,
|
||||||
|
"org",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(ctx,
|
||||||
|
userAgg,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(),
|
||||||
|
expectRandomPushFailed(io.ErrClosedPipe, []*repository.Event{eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(ctx, userAgg, nil),
|
||||||
|
)}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
wantErr: io.ErrClosedPipe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
args: args{
|
||||||
|
userID: "user1",
|
||||||
|
resourceowner: "org1",
|
||||||
|
},
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(ctx,
|
||||||
|
userAgg,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.German,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewOrgAddedEvent(ctx,
|
||||||
|
userAgg,
|
||||||
|
"org",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(ctx,
|
||||||
|
userAgg,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(),
|
||||||
|
expectRandomPush([]*repository.Event{eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(ctx, userAgg, nil),
|
||||||
|
)}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore,
|
||||||
|
multifactors: domain.MultifactorConfigs{
|
||||||
|
OTP: domain.OTPConfig{
|
||||||
|
Issuer: "zitadel.com",
|
||||||
|
CryptoMFA: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
got, err := c.AddUserOTP(ctx, tt.args.userID, tt.args.resourceowner)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
if tt.want {
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, "org1", got.ResourceOwner)
|
||||||
|
assert.NotEmpty(t, got.Secret)
|
||||||
|
assert.NotEmpty(t, got.URI)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_CheckUserOTP(t *testing.T) {
|
||||||
|
ctx := authz.NewMockContext("inst1", "org1", "user1")
|
||||||
|
|
||||||
|
cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||||
|
key, secret, err := domain.NewOTPKey("example.com", "user1", cryptoAlg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||||
|
|
||||||
|
code, err := totp.GenerateCode(key.Secret(), time.Now())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
userID string
|
||||||
|
code string
|
||||||
|
resourceOwner string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "wrong user id",
|
||||||
|
args: args{
|
||||||
|
userID: "foo",
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush([]*repository.Event{eventFromEventPusher(
|
||||||
|
user.NewHumanOTPVerifiedEvent(ctx, userAgg, ""),
|
||||||
|
)}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
resourceOwner: "org1",
|
||||||
|
code: code,
|
||||||
|
userID: "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore,
|
||||||
|
multifactors: domain.MultifactorConfigs{
|
||||||
|
OTP: domain.OTPConfig{
|
||||||
|
CryptoMFA: cryptoAlg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
got, err := c.CheckUserOTP(ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
if tt.want {
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, "org1", got.ResourceOwner)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -17,10 +17,17 @@ type OTP struct {
|
|||||||
State MFAState
|
State MFAState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OTPv2 struct {
|
||||||
|
*ObjectDetails
|
||||||
|
|
||||||
|
Secret string
|
||||||
|
URI string
|
||||||
|
}
|
||||||
|
|
||||||
func NewOTPKey(issuer, accountName string, cryptoAlg crypto.EncryptionAlgorithm) (*otp.Key, *crypto.CryptoValue, error) {
|
func NewOTPKey(issuer, accountName string, cryptoAlg crypto.EncryptionAlgorithm) (*otp.Key, *crypto.CryptoValue, error) {
|
||||||
key, err := totp.Generate(totp.GenerateOpts{Issuer: issuer, AccountName: accountName})
|
key, err := totp.Generate(totp.GenerateOpts{Issuer: issuer, AccountName: accountName})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, caos_errs.ThrowInternal(err, "TOTP-ieY3o", "Errors.Internal")
|
||||||
}
|
}
|
||||||
encryptedSecret, err := crypto.Encrypt([]byte(key.Secret()), cryptoAlg)
|
encryptedSecret, err := crypto.Encrypt([]byte(key.Secret()), cryptoAlg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -70,3 +70,25 @@ func (m *MockRepository) ExpectPushFailed(err error, expectedEvents []*repositor
|
|||||||
)
|
)
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockRepository) ExpectRandomPush(expectedEvents []*repository.Event, expectedUniqueConstraints ...*repository.UniqueConstraint) *MockRepository {
|
||||||
|
m.EXPECT().Push(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
|
||||||
|
func(ctx context.Context, events []*repository.Event, uniqueConstraints ...*repository.UniqueConstraint) error {
|
||||||
|
assert.Len(m.ctrl.T, events, len(expectedEvents))
|
||||||
|
assert.Len(m.ctrl.T, expectedUniqueConstraints, len(uniqueConstraints))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockRepository) ExpectRandomPushFailed(err error, expectedEvents []*repository.Event, expectedUniqueConstraints ...*repository.UniqueConstraint) *MockRepository {
|
||||||
|
m.EXPECT().Push(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
|
||||||
|
func(ctx context.Context, events []*repository.Event, uniqueConstraints ...*repository.UniqueConstraint) error {
|
||||||
|
assert.Len(m.ctrl.T, events, len(expectedEvents))
|
||||||
|
assert.Len(m.ctrl.T, expectedUniqueConstraints, len(uniqueConstraints))
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user