mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 20:57:24 +00:00
feat(api): add and remove OTP (SMS and email) (#6295)
* refactor: rename otp to totp * feat: add otp sms and email * implement tests
This commit is contained in:
parent
ca13e70c92
commit
a1942ecdaa
@ -10,7 +10,7 @@ import (
|
||||
// equals the authenticated user in the context.
|
||||
func UserIDInCTX(ctx context.Context, userID string) error {
|
||||
if GetCtxData(ctx).UserID != userID {
|
||||
return errors.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong")
|
||||
return errors.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -552,7 +552,7 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm
|
||||
|
||||
if user.User.OtpCode != "" {
|
||||
logging.Debugf("import user otp: %s", user.GetUserId())
|
||||
if err := s.command.ImportHumanOTP(ctx, user.UserId, "", org.GetOrgId(), user.User.OtpCode); err != nil {
|
||||
if err := s.command.ImportHumanTOTP(ctx, user.UserId, "", org.GetOrgId(), user.User.OtpCode); err != nil {
|
||||
errors = append(errors, &admin_pb.ImportDataError{Type: "human_user_otp", Id: user.GetUserId(), Message: err.Error()})
|
||||
if isCtxTimeout(ctx) {
|
||||
return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err
|
||||
|
@ -18,7 +18,7 @@ func (s *Server) ListMyAuthFactors(ctx context.Context, _ *auth_pb.ListMyAuthFac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = query.AppendAuthMethodsQuery(domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeOTP)
|
||||
err = query.AppendAuthMethodsQuery(domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeTOTP, domain.UserAuthMethodTypeOTPSMS, domain.UserAuthMethodTypeOTPEmail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -37,16 +37,16 @@ func (s *Server) ListMyAuthFactors(ctx context.Context, _ *auth_pb.ListMyAuthFac
|
||||
|
||||
func (s *Server) AddMyAuthFactorOTP(ctx context.Context, _ *auth_pb.AddMyAuthFactorOTPRequest) (*auth_pb.AddMyAuthFactorOTPResponse, error) {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
otp, err := s.command.AddHumanOTP(ctx, ctxData.UserID, ctxData.ResourceOwner)
|
||||
otp, err := s.command.AddHumanTOTP(ctx, ctxData.UserID, ctxData.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &auth_pb.AddMyAuthFactorOTPResponse{
|
||||
Url: otp.Url,
|
||||
Secret: otp.SecretString,
|
||||
Url: otp.URI,
|
||||
Secret: otp.Secret,
|
||||
Details: object.AddToDetailsPb(
|
||||
otp.Sequence,
|
||||
otp.ChangeDate,
|
||||
otp.EventDate,
|
||||
otp.ResourceOwner,
|
||||
),
|
||||
}, nil
|
||||
@ -54,7 +54,7 @@ func (s *Server) AddMyAuthFactorOTP(ctx context.Context, _ *auth_pb.AddMyAuthFac
|
||||
|
||||
func (s *Server) VerifyMyAuthFactorOTP(ctx context.Context, req *auth_pb.VerifyMyAuthFactorOTPRequest) (*auth_pb.VerifyMyAuthFactorOTPResponse, error) {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
objectDetails, err := s.command.HumanCheckMFAOTPSetup(ctx, ctxData.UserID, req.Code, "", ctxData.ResourceOwner)
|
||||
objectDetails, err := s.command.HumanCheckMFATOTPSetup(ctx, ctxData.UserID, req.Code, "", ctxData.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -65,7 +65,7 @@ func (s *Server) VerifyMyAuthFactorOTP(ctx context.Context, req *auth_pb.VerifyM
|
||||
|
||||
func (s *Server) RemoveMyAuthFactorOTP(ctx context.Context, _ *auth_pb.RemoveMyAuthFactorOTPRequest) (*auth_pb.RemoveMyAuthFactorOTPResponse, error) {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
objectDetails, err := s.command.HumanRemoveOTP(ctx, ctxData.UserID, ctxData.ResourceOwner)
|
||||
objectDetails, err := s.command.HumanRemoveTOTP(ctx, ctxData.UserID, ctxData.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -74,6 +74,50 @@ func (s *Server) RemoveMyAuthFactorOTP(ctx context.Context, _ *auth_pb.RemoveMyA
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) AddMyAuthFactorOTPSMS(ctx context.Context, _ *auth_pb.AddMyAuthFactorOTPSMSRequest) (*auth_pb.AddMyAuthFactorOTPSMSResponse, error) {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
details, err := s.command.AddHumanOTPSMS(ctx, ctxData.UserID, ctxData.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &auth_pb.AddMyAuthFactorOTPSMSResponse{
|
||||
Details: object.DomainToAddDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) RemoveMyAuthFactorOTPSMS(ctx context.Context, _ *auth_pb.RemoveMyAuthFactorOTPSMSRequest) (*auth_pb.RemoveMyAuthFactorOTPSMSResponse, error) {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
details, err := s.command.RemoveHumanOTPSMS(ctx, ctxData.UserID, ctxData.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &auth_pb.RemoveMyAuthFactorOTPSMSResponse{
|
||||
Details: object.DomainToChangeDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) AddMyAuthFactorOTPEmail(ctx context.Context, _ *auth_pb.AddMyAuthFactorOTPEmailRequest) (*auth_pb.AddMyAuthFactorOTPEmailResponse, error) {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
details, err := s.command.AddHumanOTPEmail(ctx, ctxData.UserID, ctxData.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &auth_pb.AddMyAuthFactorOTPEmailResponse{
|
||||
Details: object.DomainToAddDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) RemoveMyAuthFactorOTPEmail(ctx context.Context, _ *auth_pb.RemoveMyAuthFactorOTPEmailRequest) (*auth_pb.RemoveMyAuthFactorOTPEmailResponse, error) {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
details, err := s.command.RemoveHumanOTPEmail(ctx, ctxData.UserID, ctxData.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &auth_pb.RemoveMyAuthFactorOTPEmailResponse{
|
||||
Details: object.DomainToChangeDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) AddMyAuthFactorU2F(ctx context.Context, _ *auth_pb.AddMyAuthFactorU2FRequest) (*auth_pb.AddMyAuthFactorU2FResponse, error) {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
u2f, err := s.command.HumanAddU2FSetup(ctx, ctxData.UserID, ctxData.ResourceOwner, false)
|
||||
|
@ -613,7 +613,7 @@ func (s *Server) ListHumanAuthFactors(ctx context.Context, req *mgmt_pb.ListHuma
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = query.AppendAuthMethodsQuery(domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeOTP)
|
||||
err = query.AppendAuthMethodsQuery(domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeTOTP, domain.UserAuthMethodTypeOTPSMS, domain.UserAuthMethodTypeOTPEmail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -631,7 +631,7 @@ func (s *Server) ListHumanAuthFactors(ctx context.Context, req *mgmt_pb.ListHuma
|
||||
}
|
||||
|
||||
func (s *Server) RemoveHumanAuthFactorOTP(ctx context.Context, req *mgmt_pb.RemoveHumanAuthFactorOTPRequest) (*mgmt_pb.RemoveHumanAuthFactorOTPResponse, error) {
|
||||
objectDetails, err := s.command.HumanRemoveOTP(ctx, req.UserId, authz.GetCtxData(ctx).OrgID)
|
||||
objectDetails, err := s.command.HumanRemoveTOTP(ctx, req.UserId, authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -396,7 +396,7 @@ func Test_ZITADEL_API_missing_authentication(t *testing.T) {
|
||||
func Test_ZITADEL_API_missing_mfa(t *testing.T) {
|
||||
id, token, _, _ := Tester.CreatePasswordSession(t, CTX, User.GetUserId(), integration.UserPassword)
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
||||
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sessionResp)
|
||||
@ -405,7 +405,7 @@ func Test_ZITADEL_API_missing_mfa(t *testing.T) {
|
||||
func Test_ZITADEL_API_success(t *testing.T) {
|
||||
id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
||||
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, id, sessionResp.GetSession().GetFactors().GetPasskey().GetVerifiedAt().AsTime())
|
||||
@ -415,7 +415,7 @@ func Test_ZITADEL_API_session_not_found(t *testing.T) {
|
||||
id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
|
||||
|
||||
// test session token works
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
||||
_, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -425,7 +425,7 @@ func Test_ZITADEL_API_session_not_found(t *testing.T) {
|
||||
SessionToken: gu.Ptr(token),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
ctx = metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
ctx = Tester.WithAuthorizationToken(context.Background(), token)
|
||||
_, err = Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
@ -197,7 +197,7 @@ func AuthMethodToPb(mfa *query.AuthMethod) *user_pb.AuthFactor {
|
||||
State: MFAStateToPb(mfa.State),
|
||||
}
|
||||
switch mfa.Type {
|
||||
case domain.UserAuthMethodTypeOTP:
|
||||
case domain.UserAuthMethodTypeTOTP:
|
||||
factor.Type = &user_pb.AuthFactor_Otp{
|
||||
Otp: &user_pb.AuthFactorOTP{},
|
||||
}
|
||||
@ -208,6 +208,14 @@ func AuthMethodToPb(mfa *query.AuthMethod) *user_pb.AuthFactor {
|
||||
Name: mfa.Name,
|
||||
},
|
||||
}
|
||||
case domain.UserAuthMethodTypeOTPSMS:
|
||||
factor.Type = &user_pb.AuthFactor_OtpSms{
|
||||
OtpSms: &user_pb.AuthFactorOTPSMS{},
|
||||
}
|
||||
case domain.UserAuthMethodTypeOTPEmail:
|
||||
factor.Type = &user_pb.AuthFactor_OtpEmail{
|
||||
OtpEmail: &user_pb.AuthFactorOTPEmail{},
|
||||
}
|
||||
}
|
||||
return factor
|
||||
}
|
||||
|
43
internal/api/grpc/user/v2/otp.go
Normal file
43
internal/api/grpc/user/v2/otp.go
Normal file
@ -0,0 +1,43 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) {
|
||||
details, err := s.command.AddHumanOTPSMS(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.AddOTPSMSResponse{Details: object.DomainToDetailsPb(details)}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) RemoveOTPSMS(ctx context.Context, req *user.RemoveOTPSMSRequest) (*user.RemoveOTPSMSResponse, error) {
|
||||
objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.RemoveOTPSMSResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) AddOTPEmail(ctx context.Context, req *user.AddOTPEmailRequest) (*user.AddOTPEmailResponse, error) {
|
||||
details, err := s.command.AddHumanOTPEmail(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.AddOTPEmailResponse{Details: object.DomainToDetailsPb(details)}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) RemoveOTPEmail(ctx context.Context, req *user.RemoveOTPEmailRequest) (*user.RemoveOTPEmailResponse, error) {
|
||||
objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.RemoveOTPEmailResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
|
||||
}
|
302
internal/api/grpc/user/v2/otp_integration_test.go
Normal file
302
internal/api/grpc/user/v2/otp_integration_test.go
Normal file
@ -0,0 +1,302 @@
|
||||
//go:build integration
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
func TestServer_AddOTPSMS(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreatePasskeySession(t, CTX, userID)
|
||||
|
||||
// TODO: add when phone can be added to user
|
||||
/*
|
||||
userIDPhone := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userIDPhone)
|
||||
_, sessionTokenPhone, _, _ := Tester.CreatePasskeySession(t, CTX, userIDPhone)
|
||||
*/
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.AddOTPSMSRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.AddOTPSMSResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.AddOTPSMSRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "user mismatch",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.AddOTPSMSRequest{
|
||||
UserId: "wrong",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "phone not verified",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
|
||||
req: &user.AddOTPSMSRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
// TODO: add when phone can be added to user
|
||||
/*
|
||||
{
|
||||
name: "add success",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenPhone),
|
||||
req: &user.AddOTPSMSRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
want: &user.AddOTPSMSResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
*/
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.AddOTPSMS(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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_RemoveOTPSMS(t *testing.T) {
|
||||
// TODO: add when phone can be added to user
|
||||
/*
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreatePasskeySession(t, CTX, userID)
|
||||
*/
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RemoveOTPSMSRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RemoveOTPSMSResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "not added",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RemoveOTPSMSRequest{
|
||||
UserId: "wrong",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
// TODO: add when phone can be added to user
|
||||
/*
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
|
||||
req: &user.RemoveOTPSMSRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
want: &user.RemoveOTPSMSResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ResourceOwner,
|
||||
},
|
||||
},
|
||||
},
|
||||
*/
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.RemoveOTPSMS(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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_AddOTPEmail(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreatePasskeySession(t, CTX, userID)
|
||||
|
||||
userVerified := Tester.CreateHumanUser(CTX)
|
||||
_, err := Tester.Client.UserV2.VerifyEmail(CTX, &user.VerifyEmailRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
VerificationCode: userVerified.GetEmailCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
|
||||
_, sessionTokenVerified, _, _ := Tester.CreatePasskeySession(t, CTX, userVerified.GetUserId())
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.AddOTPEmailRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.AddOTPEmailResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.AddOTPEmailRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "user mismatch",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.AddOTPEmailRequest{
|
||||
UserId: "wrong",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "email not verified",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
|
||||
req: &user.AddOTPEmailRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "add success",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified),
|
||||
req: &user.AddOTPEmailRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.AddOTPEmailResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.AddOTPEmail(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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_RemoveOTPEmail(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreatePasskeySession(t, CTX, userID)
|
||||
|
||||
userVerified := Tester.CreateHumanUser(CTX)
|
||||
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
|
||||
_, sessionTokenVerified, _, _ := Tester.CreatePasskeySession(t, CTX, userVerified.GetUserId())
|
||||
userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified)
|
||||
_, err := Tester.Client.UserV2.VerifyEmail(userVerifiedCtx, &user.VerifyEmailRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
VerificationCode: userVerified.GetEmailCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = Tester.Client.UserV2.AddOTPEmail(userVerifiedCtx, &user.AddOTPEmailRequest{UserId: userVerified.GetUserId()})
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RemoveOTPEmailRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RemoveOTPEmailResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "not added",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
|
||||
req: &user.RemoveOTPEmailRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: userVerifiedCtx,
|
||||
req: &user.RemoveOTPEmailRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.RemoveOTPEmailResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ResourceOwner,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.RemoveOTPEmail(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)
|
||||
})
|
||||
}
|
||||
}
|
@ -213,7 +213,7 @@ func authMethodTypesToPb(methodTypes []domain.UserAuthMethodType) []user.Authent
|
||||
|
||||
func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.AuthenticationMethodType {
|
||||
switch methodType {
|
||||
case domain.UserAuthMethodTypeOTP:
|
||||
case domain.UserAuthMethodTypeTOTP:
|
||||
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_TOTP
|
||||
case domain.UserAuthMethodTypeU2F:
|
||||
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_U2F
|
||||
@ -223,6 +223,10 @@ func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.Authenticatio
|
||||
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSWORD
|
||||
case domain.UserAuthMethodTypeIDP:
|
||||
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP
|
||||
case domain.UserAuthMethodTypeOTPSMS:
|
||||
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_SMS
|
||||
case domain.UserAuthMethodTypeOTPEmail:
|
||||
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_EMAIL
|
||||
case domain.UserAuthMethodTypeUnspecified:
|
||||
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED
|
||||
default:
|
||||
|
@ -194,8 +194,8 @@ func Test_authMethodTypeToPb(t *testing.T) {
|
||||
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
"(t)otp",
|
||||
domain.UserAuthMethodTypeOTP,
|
||||
"totp",
|
||||
domain.UserAuthMethodTypeTOTP,
|
||||
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_TOTP,
|
||||
},
|
||||
{
|
||||
@ -218,6 +218,16 @@ func Test_authMethodTypeToPb(t *testing.T) {
|
||||
domain.UserAuthMethodTypeIDP,
|
||||
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP,
|
||||
},
|
||||
{
|
||||
"otp sms",
|
||||
domain.UserAuthMethodTypeOTPSMS,
|
||||
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_SMS,
|
||||
},
|
||||
{
|
||||
"otp email",
|
||||
domain.UserAuthMethodTypeOTPEmail,
|
||||
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_OTP_EMAIL,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -22,25 +22,35 @@ const (
|
||||
// [RFC 8176, section 2]: https://datatracker.ietf.org/doc/html/rfc8176#section-2
|
||||
func AuthMethodTypesToAMR(methodTypes []domain.UserAuthMethodType) []string {
|
||||
amr := make([]string, 0, 4)
|
||||
var mfa bool
|
||||
var factors, otp int
|
||||
for _, methodType := range methodTypes {
|
||||
switch methodType {
|
||||
case domain.UserAuthMethodTypePassword:
|
||||
amr = append(amr, PWD)
|
||||
factors++
|
||||
case domain.UserAuthMethodTypePasswordless:
|
||||
mfa = true
|
||||
amr = append(amr, UserPresence)
|
||||
factors += 2
|
||||
case domain.UserAuthMethodTypeU2F:
|
||||
amr = append(amr, UserPresence)
|
||||
case domain.UserAuthMethodTypeOTP:
|
||||
amr = append(amr, OTP)
|
||||
factors++
|
||||
case domain.UserAuthMethodTypeTOTP,
|
||||
domain.UserAuthMethodTypeOTPSMS,
|
||||
domain.UserAuthMethodTypeOTPEmail:
|
||||
// a user could use multiple (t)otp, which is a factor, but still will be returned as a single `otp` entry
|
||||
otp++
|
||||
factors++
|
||||
case domain.UserAuthMethodTypeIDP:
|
||||
// no AMR value according to specification
|
||||
factors++
|
||||
case domain.UserAuthMethodTypeUnspecified:
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if mfa || len(amr) >= 2 {
|
||||
if otp > 0 {
|
||||
amr = append(amr, OTP)
|
||||
}
|
||||
if factors >= 2 {
|
||||
amr = append(amr, MFA)
|
||||
}
|
||||
return amr
|
||||
|
@ -46,12 +46,33 @@ func TestAMR(t *testing.T) {
|
||||
[]string{UserPresence},
|
||||
},
|
||||
{
|
||||
"otp checked",
|
||||
"totp checked",
|
||||
args{
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeOTP},
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeTOTP},
|
||||
},
|
||||
[]string{OTP},
|
||||
},
|
||||
{
|
||||
"otp sms checked",
|
||||
args{
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeOTPSMS},
|
||||
},
|
||||
[]string{OTP},
|
||||
},
|
||||
{
|
||||
"otp email checked",
|
||||
args{
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeOTPEmail},
|
||||
},
|
||||
[]string{OTP},
|
||||
},
|
||||
{
|
||||
"multiple (t)otp checked",
|
||||
args{
|
||||
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeTOTP, domain.UserAuthMethodTypeOTPEmail},
|
||||
},
|
||||
[]string{OTP, MFA},
|
||||
},
|
||||
{
|
||||
"multiple checked",
|
||||
args{
|
||||
|
@ -261,7 +261,7 @@ func CodeChallengeToOIDC(challenge *domain.OIDCCodeChallenge) *oidc.CodeChalleng
|
||||
|
||||
func AMRFromMFAType(mfaType domain.MFAType) string {
|
||||
switch mfaType {
|
||||
case domain.MFATypeOTP:
|
||||
case domain.MFATypeTOTP:
|
||||
return OTP
|
||||
case domain.MFATypeU2F,
|
||||
domain.MFATypeU2FUserVerification:
|
||||
|
@ -33,7 +33,7 @@ func (l *Login) handleMFAInitVerify(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var verifyData *mfaVerifyData
|
||||
switch data.MFAType {
|
||||
case domain.MFATypeOTP:
|
||||
case domain.MFATypeTOTP:
|
||||
verifyData = l.handleOTPVerify(w, r, authReq, data)
|
||||
}
|
||||
|
||||
@ -50,13 +50,13 @@ func (l *Login) handleMFAInitVerify(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (l *Login) handleOTPVerify(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaInitVerifyData) *mfaVerifyData {
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
_, err := l.command.HumanCheckMFAOTPSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Code, userAgentID, authReq.UserOrgID)
|
||||
_, err := l.command.HumanCheckMFATOTPSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Code, userAgentID, authReq.UserOrgID)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
mfadata := &mfaVerifyData{
|
||||
MFAType: data.MFAType,
|
||||
otpData: otpData{
|
||||
totpData: totpData{
|
||||
Secret: data.Secret,
|
||||
Url: data.URL,
|
||||
},
|
||||
@ -73,10 +73,10 @@ func (l *Login) renderMFAInitVerify(w http.ResponseWriter, r *http.Request, auth
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
|
||||
data.profileData = l.getProfileData(authReq)
|
||||
if data.MFAType == domain.MFATypeOTP {
|
||||
code, err := generateQrCode(data.otpData.Url)
|
||||
if data.MFAType == domain.MFATypeTOTP {
|
||||
code, err := generateQrCode(data.totpData.Url)
|
||||
if err == nil {
|
||||
data.otpData.QrCode = code
|
||||
data.totpData.QrCode = code
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,8 +80,8 @@ func (l *Login) renderMFAPrompt(w http.ResponseWriter, r *http.Request, authReq
|
||||
|
||||
func (l *Login) handleMFACreation(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaVerifyData) {
|
||||
switch data.MFAType {
|
||||
case domain.MFATypeOTP:
|
||||
l.handleOTPCreation(w, r, authReq, data)
|
||||
case domain.MFATypeTOTP:
|
||||
l.handleTOTPCreation(w, r, authReq, data)
|
||||
return
|
||||
case domain.MFATypeU2F:
|
||||
l.renderRegisterU2F(w, r, authReq, nil)
|
||||
@ -90,16 +90,16 @@ func (l *Login) handleMFACreation(w http.ResponseWriter, r *http.Request, authRe
|
||||
l.renderError(w, r, authReq, caos_errs.ThrowPreconditionFailed(nil, "APP-Or3HO", "Errors.User.MFA.NoProviders"))
|
||||
}
|
||||
|
||||
func (l *Login) handleOTPCreation(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaVerifyData) {
|
||||
otp, err := l.command.AddHumanOTP(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID)
|
||||
func (l *Login) handleTOTPCreation(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaVerifyData) {
|
||||
otp, err := l.command.AddHumanTOTP(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
data.otpData = otpData{
|
||||
Secret: otp.SecretString,
|
||||
Url: otp.Url,
|
||||
data.totpData = totpData{
|
||||
Secret: otp.Secret,
|
||||
Url: otp.URI,
|
||||
}
|
||||
l.renderMFAInitVerify(w, r, authReq, data, nil)
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ func (l *Login) handleMFAVerify(w http.ResponseWriter, r *http.Request) {
|
||||
l.renderMFAVerifySelected(w, r, authReq, step, data.SelectedProvider, nil)
|
||||
return
|
||||
}
|
||||
if data.MFAType == domain.MFATypeOTP {
|
||||
if data.MFAType == domain.MFATypeTOTP {
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
err = l.authRepo.VerifyMFAOTP(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, authReq.UserOrgID, data.Code, userAgentID, domain.BrowserInfoFromRequest(r))
|
||||
|
||||
@ -45,7 +45,7 @@ func (l *Login) handleMFAVerify(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
l.renderMFAVerifySelected(w, r, authReq, step, domain.MFATypeOTP, err)
|
||||
l.renderMFAVerifySelected(w, r, authReq, step, domain.MFATypeTOTP, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -79,9 +79,9 @@ func (l *Login) renderMFAVerifySelected(w http.ResponseWriter, r *http.Request,
|
||||
data.Description = translator.LocalizeWithoutArgs("VerifyMFAU2F.Description")
|
||||
l.renderU2FVerification(w, r, authReq, removeSelectedProviderFromList(verificationStep.MFAProviders, domain.MFATypeU2F), nil)
|
||||
return
|
||||
case domain.MFATypeOTP:
|
||||
data.MFAProviders = removeSelectedProviderFromList(verificationStep.MFAProviders, domain.MFATypeOTP)
|
||||
data.SelectedMFAProvider = domain.MFATypeOTP
|
||||
case domain.MFATypeTOTP:
|
||||
data.MFAProviders = removeSelectedProviderFromList(verificationStep.MFAProviders, domain.MFATypeTOTP)
|
||||
data.SelectedMFAProvider = domain.MFATypeTOTP
|
||||
data.Title = translator.LocalizeWithoutArgs("VerifyMFAOTP.Title")
|
||||
data.Description = translator.LocalizeWithoutArgs("VerifyMFAOTP.Description")
|
||||
default:
|
||||
|
@ -673,7 +673,7 @@ type mfaVerifyData struct {
|
||||
baseData
|
||||
profileData
|
||||
MFAType domain.MFAType
|
||||
otpData
|
||||
totpData
|
||||
}
|
||||
|
||||
type mfaDoneData struct {
|
||||
@ -682,7 +682,7 @@ type mfaDoneData struct {
|
||||
MFAType domain.MFAType
|
||||
}
|
||||
|
||||
type otpData struct {
|
||||
type totpData struct {
|
||||
Url string
|
||||
Secret string
|
||||
QrCode string
|
||||
|
@ -368,7 +368,7 @@ func (repo *AuthRequestRepo) VerifyMFAOTP(ctx context.Context, authRequestID, us
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return repo.Command.HumanCheckMFAOTP(ctx, userID, code, resourceOwner, request.WithCurrentInfo(info))
|
||||
return repo.Command.HumanCheckMFATOTP(ctx, userID, code, resourceOwner, request.WithCurrentInfo(info))
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) BeginMFAU2FLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (login *domain.WebAuthNLogin, err error) {
|
||||
|
@ -1016,7 +1016,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
},
|
||||
}, false},
|
||||
[]domain.NextStep{&domain.MFAVerificationStep{
|
||||
MFAProviders: []domain.MFAType{domain.MFATypeOTP},
|
||||
MFAProviders: []domain.MFAType{domain.MFATypeTOTP},
|
||||
}},
|
||||
nil,
|
||||
},
|
||||
@ -1050,7 +1050,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
},
|
||||
}, false},
|
||||
[]domain.NextStep{&domain.MFAVerificationStep{
|
||||
MFAProviders: []domain.MFAType{domain.MFATypeOTP},
|
||||
MFAProviders: []domain.MFAType{domain.MFATypeTOTP},
|
||||
}},
|
||||
nil,
|
||||
},
|
||||
@ -1087,7 +1087,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
},
|
||||
}, false},
|
||||
[]domain.NextStep{&domain.MFAVerificationStep{
|
||||
MFAProviders: []domain.MFAType{domain.MFATypeOTP},
|
||||
MFAProviders: []domain.MFAType{domain.MFATypeTOTP},
|
||||
}},
|
||||
nil,
|
||||
},
|
||||
@ -1637,7 +1637,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
},
|
||||
&domain.MFAPromptStep{
|
||||
MFAProviders: []domain.MFAType{
|
||||
domain.MFATypeOTP,
|
||||
domain.MFATypeTOTP,
|
||||
},
|
||||
},
|
||||
false,
|
||||
@ -1663,7 +1663,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
&domain.MFAPromptStep{
|
||||
Required: true,
|
||||
MFAProviders: []domain.MFAType{
|
||||
domain.MFATypeOTP,
|
||||
domain.MFATypeTOTP,
|
||||
},
|
||||
},
|
||||
false,
|
||||
@ -1731,7 +1731,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
},
|
||||
|
||||
&domain.MFAVerificationStep{
|
||||
MFAProviders: []domain.MFAType{domain.MFATypeOTP},
|
||||
MFAProviders: []domain.MFAType{domain.MFATypeTOTP},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
@ -1755,7 +1755,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
isInternal: false,
|
||||
},
|
||||
&domain.MFAVerificationStep{
|
||||
MFAProviders: []domain.MFAType{domain.MFATypeOTP},
|
||||
MFAProviders: []domain.MFAType{domain.MFATypeTOTP},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
@ -1803,7 +1803,7 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
|
||||
&domain.MFAPromptStep{
|
||||
Required: true,
|
||||
MFAProviders: []domain.MFAType{
|
||||
domain.MFATypeOTP,
|
||||
domain.MFATypeTOTP,
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
@ -199,12 +199,21 @@ func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType
|
||||
// TODO: add checks with https://github.com/zitadel/zitadel/issues/5477
|
||||
/*
|
||||
if !session.TOTPFactor.TOTPCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeOTP)
|
||||
types = append(types, domain.UserAuthMethodTypeTOTP)
|
||||
}
|
||||
if !session.U2FFactor.U2FCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeU2F)
|
||||
}
|
||||
*/
|
||||
// TODO: add checks with https://github.com/zitadel/zitadel/issues/6224
|
||||
/*
|
||||
if !session.TOTPFactor.OTPSMSCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeOTPSMS)
|
||||
}
|
||||
if !session.TOTPFactor.OTPEmailCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeOTPEmail)
|
||||
}
|
||||
*/
|
||||
return types
|
||||
}
|
||||
|
||||
|
@ -155,6 +155,7 @@ func (wm *SessionWriteModel) AuthenticationTime() time.Time {
|
||||
wm.PasskeyCheckedAt,
|
||||
wm.IntentCheckedAt,
|
||||
// TODO: add U2F and OTP check https://github.com/zitadel/zitadel/issues/5477
|
||||
// TODO: add OTP (sms and email) check https://github.com/zitadel/zitadel/issues/6224
|
||||
} {
|
||||
if check.After(authTime) {
|
||||
authTime = check
|
||||
@ -178,11 +179,20 @@ func (wm *SessionWriteModel) AuthMethodTypes() []domain.UserAuthMethodType {
|
||||
// TODO: add checks with https://github.com/zitadel/zitadel/issues/5477
|
||||
/*
|
||||
if !wm.TOTPCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeOTP)
|
||||
types = append(types, domain.UserAuthMethodTypeTOTP)
|
||||
}
|
||||
if !wm.U2FCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeU2F)
|
||||
}
|
||||
*/
|
||||
// TODO: add checks with https://github.com/zitadel/zitadel/issues/6224
|
||||
/*
|
||||
if !wm.TOTPFactor.OTPSMSCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeOTPSMS)
|
||||
}
|
||||
if !wm.TOTPFactor.OTPEmailCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeOTPEmail)
|
||||
}
|
||||
*/
|
||||
return types
|
||||
}
|
||||
|
@ -11,12 +11,11 @@ import (
|
||||
"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/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func (c *Commands) ImportHumanOTP(ctx context.Context, userID, userAgentID, resourceowner string, key string) error {
|
||||
func (c *Commands) ImportHumanTOTP(ctx context.Context, userID, userAgentID, resourceowner string, key string) error {
|
||||
encryptedSecret, err := crypto.Encrypt([]byte(key), c.multifactors.OTP.CryptoMFA)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -25,7 +24,7 @@ func (c *Commands) ImportHumanOTP(ctx context.Context, userID, userAgentID, reso
|
||||
return err
|
||||
}
|
||||
|
||||
otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceowner)
|
||||
otpWriteModel, err := c.totpWriteModelByID(ctx, userID, resourceowner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -41,7 +40,7 @@ func (c *Commands) ImportHumanOTP(ctx context.Context, userID, userAgentID, reso
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string) (*domain.OTP, error) {
|
||||
func (c *Commands) AddHumanTOTP(ctx context.Context, userID, resourceowner string) (*domain.TOTP, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
|
||||
}
|
||||
@ -49,21 +48,19 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = c.eventstore.Push(ctx, prep.cmds...)
|
||||
err = c.pushAppendAndReduce(ctx, prep.wm, 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(),
|
||||
return &domain.TOTP{
|
||||
ObjectDetails: writeModelToObjectDetails(&prep.wm.WriteModel),
|
||||
Secret: prep.key.Secret(),
|
||||
URI: prep.key.URL(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type preparedTOTP struct {
|
||||
wm *HumanOTPWriteModel
|
||||
wm *HumanTOTPWriteModel
|
||||
userAgg *eventstore.Aggregate
|
||||
key *otp.Key
|
||||
cmds []eventstore.Command
|
||||
@ -72,21 +69,21 @@ type preparedTOTP struct {
|
||||
func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner string) (*preparedTOTP, error) {
|
||||
human, err := c.getHuman(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
logging.Log("COMMAND-DAqe1").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get human for loginname")
|
||||
logging.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")
|
||||
}
|
||||
org, err := c.getOrg(ctx, human.ResourceOwner)
|
||||
if err != nil {
|
||||
logging.Log("COMMAND-Cm0ds").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org for loginname")
|
||||
logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org for loginname")
|
||||
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-55M9f", "Errors.Org.NotFound")
|
||||
}
|
||||
orgPolicy, err := c.getOrgDomainPolicy(ctx, org.AggregateID)
|
||||
if err != nil {
|
||||
logging.Log("COMMAND-y5zv9").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org policy for loginname")
|
||||
logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org policy for loginname")
|
||||
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-8ugTs", "Errors.Org.DomainPolicy.NotFound")
|
||||
}
|
||||
|
||||
otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceOwner)
|
||||
otpWriteModel, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -103,7 +100,7 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st
|
||||
if issuer == "" {
|
||||
issuer = authz.GetInstance(ctx).RequestedDomain()
|
||||
}
|
||||
key, secret, err := domain.NewOTPKey(issuer, accountName, c.multifactors.OTP.CryptoMFA)
|
||||
key, secret, err := domain.NewTOTPKey(issuer, accountName, c.multifactors.OTP.CryptoMFA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -117,12 +114,12 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Commands) HumanCheckMFAOTPSetup(ctx context.Context, userID, code, userAgentID, resourceowner string) (*domain.ObjectDetails, error) {
|
||||
func (c *Commands) HumanCheckMFATOTPSetup(ctx context.Context, userID, code, userAgentID, resourceowner string) (*domain.ObjectDetails, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
|
||||
}
|
||||
|
||||
existingOTP, err := c.otpWriteModelByID(ctx, userID, resourceowner)
|
||||
existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceowner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -132,7 +129,7 @@ func (c *Commands) HumanCheckMFAOTPSetup(ctx context.Context, userID, code, user
|
||||
if existingOTP.State == domain.MFAStateReady {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-qx4ls", "Errors.Users.MFA.OTP.AlreadyReady")
|
||||
}
|
||||
if err := domain.VerifyMFAOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA); err != nil {
|
||||
if err := domain.VerifyTOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
|
||||
@ -148,11 +145,11 @@ func (c *Commands) HumanCheckMFAOTPSetup(ctx context.Context, userID, code, user
|
||||
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) HumanCheckMFAOTP(ctx context.Context, userID, code, resourceowner string, authRequest *domain.AuthRequest) error {
|
||||
func (c *Commands) HumanCheckMFATOTP(ctx context.Context, userID, code, resourceowner string, authRequest *domain.AuthRequest) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
|
||||
}
|
||||
existingOTP, err := c.otpWriteModelByID(ctx, userID, resourceowner)
|
||||
existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceowner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -160,22 +157,22 @@ func (c *Commands) HumanCheckMFAOTP(ctx context.Context, userID, code, resourceo
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotReady")
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
|
||||
err = domain.VerifyMFAOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA)
|
||||
err = domain.VerifyTOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA)
|
||||
if err == nil {
|
||||
_, err = c.eventstore.Push(ctx, user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
|
||||
return err
|
||||
}
|
||||
_, pushErr := c.eventstore.Push(ctx, user.NewHumanOTPCheckFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
|
||||
logging.Log("COMMAND-9fj7s").OnError(pushErr).Error("error create password check failed event")
|
||||
logging.OnError(pushErr).Error("error create password check failed event")
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) HumanRemoveOTP(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
|
||||
func (c *Commands) HumanRemoveTOTP(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
|
||||
}
|
||||
|
||||
existingOTP, err := c.otpWriteModelByID(ctx, userID, resourceOwner)
|
||||
existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -194,11 +191,128 @@ func (c *Commands) HumanRemoveOTP(ctx context.Context, userID, resourceOwner str
|
||||
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) otpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPWriteModel, err error) {
|
||||
func (c *Commands) AddHumanOTPSMS(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-QSF2s", "Errors.User.UserIDMissing")
|
||||
}
|
||||
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
otpWriteModel, err := c.otpSMSWriteModelByID(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if otpWriteModel.otpAdded {
|
||||
return nil, caos_errs.ThrowAlreadyExists(nil, "COMMAND-Ad3g2", "Errors.User.MFA.OTP.AlreadyReady")
|
||||
}
|
||||
if !otpWriteModel.phoneVerified {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Q54j2", "Errors.User.MFA.OTP.NotReady")
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModel(&otpWriteModel.WriteModel)
|
||||
if err = c.pushAppendAndReduce(ctx, otpWriteModel, user.NewHumanOTPSMSAddedEvent(ctx, userAgg)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&otpWriteModel.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) RemoveHumanOTPSMS(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3br2", "Errors.User.UserIDMissing")
|
||||
}
|
||||
|
||||
existingOTP, err := c.otpSMSWriteModelByID(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userID != authz.GetCtxData(ctx).UserID {
|
||||
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.ResourceOwner, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !existingOTP.otpAdded {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Sr3h3", "Errors.User.MFA.OTP.NotExisting")
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
|
||||
if err = c.pushAppendAndReduce(ctx, existingOTP, user.NewHumanOTPSMSRemovedEvent(ctx, userAgg)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) AddHumanOTPEmail(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Sg1hz", "Errors.User.UserIDMissing")
|
||||
}
|
||||
otpWriteModel, err := c.otpEmailWriteModelByID(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if otpWriteModel.otpAdded {
|
||||
return nil, caos_errs.ThrowAlreadyExists(nil, "COMMAND-MKL2s", "Errors.User.MFA.OTP.AlreadyReady")
|
||||
}
|
||||
if !otpWriteModel.emailVerified {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-KLJ2d", "Errors.User.MFA.OTP.NotReady")
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModel(&otpWriteModel.WriteModel)
|
||||
if err = c.pushAppendAndReduce(ctx, otpWriteModel, user.NewHumanOTPEmailAddedEvent(ctx, userAgg)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&otpWriteModel.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) RemoveHumanOTPEmail(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-S2h11", "Errors.User.UserIDMissing")
|
||||
}
|
||||
|
||||
existingOTP, err := c.otpEmailWriteModelByID(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userID != authz.GetCtxData(ctx).UserID {
|
||||
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.ResourceOwner, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !existingOTP.otpAdded {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-b312D", "Errors.User.MFA.OTP.NotExisting")
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
|
||||
if err = c.pushAppendAndReduce(ctx, existingOTP, user.NewHumanOTPEmailRemovedEvent(ctx, userAgg)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) totpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanTOTPWriteModel, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
writeModel = NewHumanOTPWriteModel(userID, resourceOwner)
|
||||
writeModel = NewHumanTOTPWriteModel(userID, resourceOwner)
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModel, nil
|
||||
}
|
||||
|
||||
func (c *Commands) otpSMSWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPSMSWriteModel, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
writeModel = NewHumanOTPSMSWriteModel(userID, resourceOwner)
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModel, nil
|
||||
}
|
||||
|
||||
func (c *Commands) otpEmailWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPEmailWriteModel, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
writeModel = NewHumanOTPEmailWriteModel(userID, resourceOwner)
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -7,15 +7,15 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
type HumanOTPWriteModel struct {
|
||||
type HumanTOTPWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
State domain.MFAState
|
||||
Secret *crypto.CryptoValue
|
||||
}
|
||||
|
||||
func NewHumanOTPWriteModel(userID, resourceOwner string) *HumanOTPWriteModel {
|
||||
return &HumanOTPWriteModel{
|
||||
func NewHumanTOTPWriteModel(userID, resourceOwner string) *HumanTOTPWriteModel {
|
||||
return &HumanTOTPWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: userID,
|
||||
ResourceOwner: resourceOwner,
|
||||
@ -23,7 +23,7 @@ func NewHumanOTPWriteModel(userID, resourceOwner string) *HumanOTPWriteModel {
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *HumanOTPWriteModel) Reduce() error {
|
||||
func (wm *HumanTOTPWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *user.HumanOTPAddedEvent:
|
||||
@ -40,7 +40,7 @@ func (wm *HumanOTPWriteModel) Reduce() error {
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *HumanOTPWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
func (wm *HumanTOTPWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(user.AggregateType).
|
||||
@ -59,3 +59,107 @@ func (wm *HumanOTPWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
type HumanOTPSMSWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
phoneVerified bool
|
||||
otpAdded bool
|
||||
}
|
||||
|
||||
func NewHumanOTPSMSWriteModel(userID, resourceOwner string) *HumanOTPSMSWriteModel {
|
||||
return &HumanOTPSMSWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: userID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *HumanOTPSMSWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch event.(type) {
|
||||
case *user.HumanPhoneVerifiedEvent:
|
||||
wm.phoneVerified = true
|
||||
case *user.HumanOTPSMSAddedEvent:
|
||||
wm.otpAdded = true
|
||||
case *user.HumanOTPSMSRemovedEvent:
|
||||
wm.otpAdded = false
|
||||
case *user.HumanPhoneRemovedEvent,
|
||||
*user.UserRemovedEvent:
|
||||
wm.phoneVerified = false
|
||||
wm.otpAdded = false
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *HumanOTPSMSWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(user.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(user.HumanPhoneVerifiedType,
|
||||
user.HumanOTPSMSAddedType,
|
||||
user.HumanOTPSMSRemovedType,
|
||||
user.HumanPhoneRemovedType,
|
||||
user.UserRemovedType,
|
||||
).
|
||||
Builder()
|
||||
|
||||
if wm.ResourceOwner != "" {
|
||||
query.ResourceOwner(wm.ResourceOwner)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
type HumanOTPEmailWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
emailVerified bool
|
||||
otpAdded bool
|
||||
}
|
||||
|
||||
func NewHumanOTPEmailWriteModel(userID, resourceOwner string) *HumanOTPEmailWriteModel {
|
||||
return &HumanOTPEmailWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: userID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *HumanOTPEmailWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch event.(type) {
|
||||
case *user.HumanEmailVerifiedEvent:
|
||||
wm.emailVerified = true
|
||||
case *user.HumanOTPEmailAddedEvent:
|
||||
wm.otpAdded = true
|
||||
case *user.HumanOTPEmailRemovedEvent:
|
||||
wm.otpAdded = false
|
||||
case *user.UserRemovedEvent:
|
||||
wm.emailVerified = false
|
||||
wm.otpAdded = false
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *HumanOTPEmailWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(user.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(user.HumanEmailVerifiedType,
|
||||
user.HumanOTPEmailAddedType,
|
||||
user.HumanOTPEmailRemovedType,
|
||||
user.UserRemovedType,
|
||||
).
|
||||
Builder()
|
||||
|
||||
if wm.ResourceOwner != "" {
|
||||
query.ResourceOwner(wm.ResourceOwner)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
func TestCommandSide_AddHumanOTP(t *testing.T) {
|
||||
func TestCommandSide_AddHumanTOTP(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
@ -223,7 +223,7 @@ func TestCommandSide_AddHumanOTP(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
}
|
||||
got, err := r.AddHumanOTP(tt.args.ctx, tt.args.userID, tt.args.orgID)
|
||||
got, err := r.AddHumanTOTP(tt.args.ctx, tt.args.userID, tt.args.orgID)
|
||||
if tt.res.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@ -237,7 +237,7 @@ func TestCommandSide_AddHumanOTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_createHumanOTP(t *testing.T) {
|
||||
func TestCommands_createHumanTOTP(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
@ -527,11 +527,11 @@ func TestCommands_createHumanOTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_HumanCheckMFAOTPSetup(t *testing.T) {
|
||||
func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) {
|
||||
ctx := authz.NewMockContext("", "org1", "user1")
|
||||
|
||||
cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
key, secret, err := domain.NewOTPKey("example.com", "user1", cryptoAlg)
|
||||
key, secret, err := domain.NewTOTPKey("example.com", "user1", cryptoAlg)
|
||||
require.NoError(t, err)
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
|
||||
@ -697,7 +697,7 @@ func TestCommands_HumanCheckMFAOTPSetup(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
got, err := c.HumanCheckMFAOTPSetup(ctx, tt.args.userID, tt.args.code, "agent1", tt.args.resourceOwner)
|
||||
got, err := c.HumanCheckMFATOTPSetup(ctx, tt.args.userID, tt.args.code, "agent1", tt.args.resourceOwner)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.want {
|
||||
require.NotNil(t, got)
|
||||
@ -707,7 +707,7 @@ func TestCommands_HumanCheckMFAOTPSetup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandSide_RemoveHumanOTP(t *testing.T) {
|
||||
func TestCommandSide_RemoveHumanTOTP(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
@ -802,7 +802,7 @@ func TestCommandSide_RemoveHumanOTP(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
}
|
||||
got, err := r.HumanRemoveOTP(tt.args.ctx, tt.args.userID, tt.args.orgID)
|
||||
got, err := r.HumanRemoveTOTP(tt.args.ctx, tt.args.userID, tt.args.orgID)
|
||||
if tt.res.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@ -815,3 +815,540 @@ func TestCommandSide_RemoveHumanOTP(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandSide_AddHumanOTPSMS(t *testing.T) {
|
||||
ctx := authz.NewMockContext("inst1", "org1", "user1")
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
}
|
||||
type (
|
||||
args struct {
|
||||
ctx context.Context
|
||||
userID string
|
||||
resourceOwner string
|
||||
}
|
||||
)
|
||||
type res struct {
|
||||
want *domain.ObjectDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "userid missing, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-QSF2s", "Errors.User.UserIDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong user, permission denied error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "other",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "otp sms already exists, already exists error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPSMSAddedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowAlreadyExists(nil, "COMMAND-Ad3g2", "Errors.User.MFA.OTP.AlreadyReady"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "phone not verified, precondition failed error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Q54j2", "Errors.User.MFA.OTP.NotReady"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "phone removed, precondition failed error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPhoneChangedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"+4179654321",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPhoneVerifiedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPhoneRemovedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Q54j2", "Errors.User.MFA.OTP.NotReady"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful add",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPhoneChangedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"+4179654321",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPhoneVerifiedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusherWithInstanceID("inst1",
|
||||
user.NewHumanOTPSMSAddedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
got, err := r.AddHumanOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
|
||||
assert.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandSide_RemoveHumanOTPSMS(t *testing.T) {
|
||||
ctx := authz.NewMockContext("inst1", "org1", "user1")
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type (
|
||||
args struct {
|
||||
ctx context.Context
|
||||
userID string
|
||||
resourceOwner string
|
||||
}
|
||||
)
|
||||
type res struct {
|
||||
want *domain.ObjectDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "userid missing, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3br2", "Errors.User.UserIDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "other user not permission, permission denied error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "other",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "otp sms not added, not found error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowNotFound(nil, "COMMAND-Sr3h3", "Errors.User.MFA.OTP.NotExisting"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful remove",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPSMSAddedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusherWithInstanceID("inst1",
|
||||
user.NewHumanOTPSMSRemovedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
}
|
||||
got, err := r.RemoveHumanOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
|
||||
assert.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandSide_AddHumanOTPEmail(t *testing.T) {
|
||||
ctx := authz.NewMockContext("inst1", "org1", "user1")
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
}
|
||||
type (
|
||||
args struct {
|
||||
ctx context.Context
|
||||
userID string
|
||||
resourceOwner string
|
||||
}
|
||||
)
|
||||
type res struct {
|
||||
want *domain.ObjectDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "userid missing, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-Sg1hz", "Errors.User.UserIDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "otp email already exists, already exists error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPEmailAddedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowAlreadyExists(nil, "COMMAND-MKL2s", "Errors.User.MFA.OTP.AlreadyReady"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "email not verified, precondition failed error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-KLJ2d", "Errors.User.MFA.OTP.NotReady"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful add",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailChangedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"email@test.ch",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailVerifiedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusherWithInstanceID("inst1",
|
||||
user.NewHumanOTPEmailAddedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
got, err := r.AddHumanOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
|
||||
assert.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandSide_RemoveHumanOTPEmail(t *testing.T) {
|
||||
ctx := authz.NewMockContext("inst1", "org1", "user1")
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type (
|
||||
args struct {
|
||||
ctx context.Context
|
||||
userID string
|
||||
resourceOwner string
|
||||
}
|
||||
)
|
||||
type res struct {
|
||||
want *domain.ObjectDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "userid missing, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-S2h11", "Errors.User.UserIDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "other user not permission, permission denied error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "other",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "otp email not added, not found error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowNotFound(nil, "COMMAND-b312D", "Errors.User.MFA.OTP.NotExisting"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful remove",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPEmailAddedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusherWithInstanceID("inst1",
|
||||
user.NewHumanOTPEmailRemovedEvent(ctx,
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
}
|
||||
got, err := r.RemoveHumanOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
|
||||
assert.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -885,70 +885,70 @@ func TestCommandSide_RemoveHumanPhone(t *testing.T) {
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
//{
|
||||
// name: "userid missing, invalid argument error",
|
||||
// fields: fields{
|
||||
// eventstore: eventstoreExpect(
|
||||
// t,
|
||||
// ),
|
||||
// },
|
||||
// args: args{
|
||||
// ctx: context.Background(),
|
||||
// resourceOwner: "org1",
|
||||
// },
|
||||
// res: res{
|
||||
// err: caos_errs.IsErrorInvalidArgument,
|
||||
// },
|
||||
//},
|
||||
//{
|
||||
// name: "user not existing, precondition error",
|
||||
// fields: fields{
|
||||
// eventstore: eventstoreExpect(
|
||||
// t,
|
||||
// expectFilter(),
|
||||
// ),
|
||||
// },
|
||||
// args: args{
|
||||
// ctx: context.Background(),
|
||||
// userID: "user1",
|
||||
// resourceOwner: "org1",
|
||||
// },
|
||||
// res: res{
|
||||
// err: caos_errs.IsPreconditionFailed,
|
||||
// },
|
||||
//},
|
||||
//{
|
||||
// name: "phone not existing, precondition 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,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// },
|
||||
// args: args{
|
||||
// ctx: context.Background(),
|
||||
// userID: "user1",
|
||||
// resourceOwner: "org1",
|
||||
// },
|
||||
// res: res{
|
||||
// err: caos_errs.IsNotFound,
|
||||
// },
|
||||
//},
|
||||
{
|
||||
name: "userid missing, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user not existing, precondition error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "phone not existing, precondition 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove phone, ok",
|
||||
fields: fields{
|
||||
|
@ -57,7 +57,7 @@ func TestCommands_RegisterUserPasskey(t *testing.T) {
|
||||
resourceOwner: "org1",
|
||||
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
|
||||
},
|
||||
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
},
|
||||
{
|
||||
name: "get human passwordless error",
|
||||
|
@ -29,5 +29,5 @@ func (c *Commands) CheckUserTOTP(ctx context.Context, userID, code, resourceOwne
|
||||
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.HumanCheckMFAOTPSetup(ctx, userID, code, "", resourceOwner)
|
||||
return c.HumanCheckMFATOTPSetup(ctx, userID, code, "", resourceOwner)
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ func TestCommands_AddUserTOTP(t *testing.T) {
|
||||
userID: "foo",
|
||||
resourceowner: "org1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
},
|
||||
{
|
||||
name: "create otp error",
|
||||
@ -191,7 +191,7 @@ func TestCommands_CheckUserTOTP(t *testing.T) {
|
||||
ctx := authz.NewMockContext("", "org1", "user1")
|
||||
|
||||
cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
key, secret, err := domain.NewOTPKey("example.com", "user1", cryptoAlg)
|
||||
key, secret, err := domain.NewTOTPKey("example.com", "user1", cryptoAlg)
|
||||
require.NoError(t, err)
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
|
||||
@ -218,7 +218,7 @@ func TestCommands_CheckUserTOTP(t *testing.T) {
|
||||
args: args{
|
||||
userID: "foo",
|
||||
},
|
||||
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
|
@ -52,7 +52,7 @@ func TestCommands_RegisterUserU2F(t *testing.T) {
|
||||
userID: "foo",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
},
|
||||
{
|
||||
name: "get human passwordless error",
|
||||
|
@ -102,7 +102,7 @@ const (
|
||||
type MFAType int
|
||||
|
||||
const (
|
||||
MFATypeOTP MFAType = iota
|
||||
MFATypeTOTP MFAType = iota
|
||||
MFATypeU2F
|
||||
MFATypeU2FUserVerification
|
||||
)
|
||||
|
@ -3,20 +3,11 @@ package domain
|
||||
import (
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type OTP struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Secret *crypto.CryptoValue
|
||||
SecretString string
|
||||
Url string
|
||||
State MFAState
|
||||
}
|
||||
|
||||
type TOTP struct {
|
||||
*ObjectDetails
|
||||
|
||||
@ -24,7 +15,7 @@ type TOTP struct {
|
||||
URI string
|
||||
}
|
||||
|
||||
func NewOTPKey(issuer, accountName string, cryptoAlg crypto.EncryptionAlgorithm) (*otp.Key, *crypto.CryptoValue, error) {
|
||||
func NewTOTPKey(issuer, accountName string, cryptoAlg crypto.EncryptionAlgorithm) (*otp.Key, *crypto.CryptoValue, error) {
|
||||
key, err := totp.Generate(totp.GenerateOpts{Issuer: issuer, AccountName: accountName})
|
||||
if err != nil {
|
||||
return nil, nil, caos_errs.ThrowInternal(err, "TOTP-ieY3o", "Errors.Internal")
|
||||
@ -36,7 +27,7 @@ func NewOTPKey(issuer, accountName string, cryptoAlg crypto.EncryptionAlgorithm)
|
||||
return key, encryptedSecret, nil
|
||||
}
|
||||
|
||||
func VerifyMFAOTP(code string, secret *crypto.CryptoValue, cryptoAlg crypto.EncryptionAlgorithm) error {
|
||||
func VerifyTOTP(code string, secret *crypto.CryptoValue, cryptoAlg crypto.EncryptionAlgorithm) error {
|
||||
decrypt, err := crypto.DecryptString(secret, cryptoAlg)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -48,11 +48,13 @@ type UserAuthMethodType int32
|
||||
|
||||
const (
|
||||
UserAuthMethodTypeUnspecified UserAuthMethodType = iota
|
||||
UserAuthMethodTypeOTP
|
||||
UserAuthMethodTypeTOTP
|
||||
UserAuthMethodTypeU2F
|
||||
UserAuthMethodTypePasswordless
|
||||
UserAuthMethodTypePassword
|
||||
UserAuthMethodTypeIDP
|
||||
UserAuthMethodTypeOTPSMS
|
||||
UserAuthMethodTypeOTPEmail
|
||||
userAuthMethodTypeCount
|
||||
)
|
||||
|
||||
@ -67,15 +69,14 @@ func HasMFA(methods []UserAuthMethodType) bool {
|
||||
var factors int
|
||||
for _, method := range methods {
|
||||
switch method {
|
||||
case UserAuthMethodTypePassword:
|
||||
factors++
|
||||
case UserAuthMethodTypePasswordless:
|
||||
return true
|
||||
case UserAuthMethodTypeU2F:
|
||||
factors++
|
||||
case UserAuthMethodTypeOTP:
|
||||
factors++
|
||||
case UserAuthMethodTypeIDP:
|
||||
case UserAuthMethodTypePassword,
|
||||
UserAuthMethodTypeU2F,
|
||||
UserAuthMethodTypeTOTP,
|
||||
UserAuthMethodTypeOTPSMS,
|
||||
UserAuthMethodTypeOTPEmail,
|
||||
UserAuthMethodTypeIDP:
|
||||
factors++
|
||||
case UserAuthMethodTypeUnspecified,
|
||||
userAuthMethodTypeCount:
|
||||
|
@ -315,6 +315,9 @@ func GenericEventMapper[T any, PT BaseEventSetter[T]](event *repository.Event) (
|
||||
e := PT(new(T))
|
||||
e.SetBaseEvent(BaseEventFromRepo(event))
|
||||
|
||||
if len(event.Data) == 0 {
|
||||
return e, nil
|
||||
}
|
||||
err := json.Unmarshal(event.Data, e)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "V2-Thai6", "unable to unmarshal event")
|
||||
|
@ -248,7 +248,11 @@ func (s *Tester) WithInstanceAuthorization(ctx context.Context, u UserType, inst
|
||||
if u == SystemUser {
|
||||
s.ensureSystemUser()
|
||||
}
|
||||
return metadata.AppendToOutgoingContext(ctx, "Authorization", fmt.Sprintf("Bearer %s", s.Users.Get(instanceID, u).Token))
|
||||
return s.WithAuthorizationToken(ctx, s.Users.Get(instanceID, u).Token)
|
||||
}
|
||||
|
||||
func (s *Tester) WithAuthorizationToken(ctx context.Context, token string) context.Context {
|
||||
return metadata.AppendToOutgoingContext(ctx, "Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
|
||||
func (s *Tester) ensureSystemUser() {
|
||||
|
@ -89,6 +89,14 @@ func (p *userAuthMethodProjection) reducers() []handler.AggregateReducer {
|
||||
Event: user.HumanMFAOTPVerifiedType,
|
||||
Reduce: p.reduceActivateEvent,
|
||||
},
|
||||
{
|
||||
Event: user.HumanOTPSMSAddedType,
|
||||
Reduce: p.reduceAddAuthMethod,
|
||||
},
|
||||
{
|
||||
Event: user.HumanOTPEmailAddedType,
|
||||
Reduce: p.reduceAddAuthMethod,
|
||||
},
|
||||
{
|
||||
Event: user.HumanPasswordlessTokenRemovedType,
|
||||
Reduce: p.reduceRemoveAuthMethod,
|
||||
@ -101,6 +109,14 @@ func (p *userAuthMethodProjection) reducers() []handler.AggregateReducer {
|
||||
Event: user.HumanMFAOTPRemovedType,
|
||||
Reduce: p.reduceRemoveAuthMethod,
|
||||
},
|
||||
{
|
||||
Event: user.HumanOTPSMSRemovedType,
|
||||
Reduce: p.reduceRemoveAuthMethod,
|
||||
},
|
||||
{
|
||||
Event: user.HumanOTPEmailRemovedType,
|
||||
Reduce: p.reduceRemoveAuthMethod,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -135,7 +151,7 @@ func (p *userAuthMethodProjection) reduceInitAuthMethod(event eventstore.Event)
|
||||
methodType = domain.UserAuthMethodTypeU2F
|
||||
tokenID = e.WebAuthNTokenID
|
||||
case *user.HumanOTPAddedEvent:
|
||||
methodType = domain.UserAuthMethodTypeOTP
|
||||
methodType = domain.UserAuthMethodTypeTOTP
|
||||
default:
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-f92f", "reduce.wrong.event.type %v", []eventstore.EventType{user.HumanPasswordlessTokenAddedType, user.HumanU2FTokenAddedType})
|
||||
}
|
||||
@ -178,7 +194,7 @@ func (p *userAuthMethodProjection) reduceActivateEvent(event eventstore.Event) (
|
||||
tokenID = e.WebAuthNTokenID
|
||||
name = e.WebAuthNTokenName
|
||||
case *user.HumanOTPVerifiedEvent:
|
||||
methodType = domain.UserAuthMethodTypeOTP
|
||||
methodType = domain.UserAuthMethodTypeTOTP
|
||||
|
||||
default:
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-f92f", "reduce.wrong.event.type %v", []eventstore.EventType{user.HumanPasswordlessTokenAddedType, user.HumanU2FTokenAddedType})
|
||||
@ -202,6 +218,34 @@ func (p *userAuthMethodProjection) reduceActivateEvent(event eventstore.Event) (
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *userAuthMethodProjection) reduceAddAuthMethod(event eventstore.Event) (*handler.Statement, error) {
|
||||
var methodType domain.UserAuthMethodType
|
||||
switch event.(type) {
|
||||
case *user.HumanOTPSMSAddedEvent:
|
||||
methodType = domain.UserAuthMethodTypeOTPSMS
|
||||
case *user.HumanOTPEmailAddedEvent:
|
||||
methodType = domain.UserAuthMethodTypeOTPEmail
|
||||
default:
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-DS4g3", "reduce.wrong.event.type %v", []eventstore.EventType{user.HumanOTPSMSAddedType, user.HumanOTPEmailAddedType})
|
||||
}
|
||||
|
||||
return crdb.NewCreateStatement(
|
||||
event,
|
||||
[]handler.Column{
|
||||
handler.NewCol(UserAuthMethodTokenIDCol, ""),
|
||||
handler.NewCol(UserAuthMethodCreationDateCol, event.CreationDate()),
|
||||
handler.NewCol(UserAuthMethodChangeDateCol, event.CreationDate()),
|
||||
handler.NewCol(UserAuthMethodResourceOwnerCol, event.Aggregate().ResourceOwner),
|
||||
handler.NewCol(UserAuthMethodInstanceIDCol, event.Aggregate().InstanceID),
|
||||
handler.NewCol(UserAuthMethodUserIDCol, event.Aggregate().ID),
|
||||
handler.NewCol(UserAuthMethodSequenceCol, event.Sequence()),
|
||||
handler.NewCol(UserAuthMethodStateCol, domain.MFAStateReady),
|
||||
handler.NewCol(UserAuthMethodTypeCol, methodType),
|
||||
handler.NewCol(UserAuthMethodNameCol, ""),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *userAuthMethodProjection) reduceRemoveAuthMethod(event eventstore.Event) (*handler.Statement, error) {
|
||||
var tokenID string
|
||||
var methodType domain.UserAuthMethodType
|
||||
@ -213,10 +257,17 @@ func (p *userAuthMethodProjection) reduceRemoveAuthMethod(event eventstore.Event
|
||||
methodType = domain.UserAuthMethodTypeU2F
|
||||
tokenID = e.WebAuthNTokenID
|
||||
case *user.HumanOTPRemovedEvent:
|
||||
methodType = domain.UserAuthMethodTypeOTP
|
||||
methodType = domain.UserAuthMethodTypeTOTP
|
||||
case *user.HumanOTPSMSRemovedEvent,
|
||||
*user.HumanPhoneRemovedEvent:
|
||||
methodType = domain.UserAuthMethodTypeOTPSMS
|
||||
case *user.HumanOTPEmailRemovedEvent:
|
||||
methodType = domain.UserAuthMethodTypeOTPEmail
|
||||
|
||||
default:
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-f92f", "reduce.wrong.event.type %v", []eventstore.EventType{user.HumanPasswordlessTokenAddedType, user.HumanU2FTokenAddedType})
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-f92f", "reduce.wrong.event.type %v",
|
||||
[]eventstore.EventType{user.HumanPasswordlessTokenAddedType, user.HumanU2FTokenAddedType, user.HumanMFAOTPRemovedType,
|
||||
user.HumanOTPSMSRemovedType, user.HumanPhoneRemovedType, user.HumanOTPEmailRemovedType})
|
||||
}
|
||||
conditions := []handler.Condition{
|
||||
handler.NewCond(UserAuthMethodUserIDCol, event.Aggregate().ID),
|
||||
|
@ -98,7 +98,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceAddedOTP",
|
||||
name: "reduceAddedTOTP",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
repository.EventType(user.HumanMFAOTPAddedType),
|
||||
@ -125,7 +125,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
|
||||
"agg-id",
|
||||
uint64(15),
|
||||
domain.MFAStateNotReady,
|
||||
domain.UserAuthMethodTypeOTP,
|
||||
domain.UserAuthMethodTypeTOTP,
|
||||
"",
|
||||
},
|
||||
},
|
||||
@ -208,7 +208,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceVerifiedOTP",
|
||||
name: "reduceVerifiedTOTP",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
repository.EventType(user.HumanMFAOTPVerifiedType),
|
||||
@ -232,7 +232,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
|
||||
"",
|
||||
domain.MFAStateReady,
|
||||
"agg-id",
|
||||
domain.UserAuthMethodTypeOTP,
|
||||
domain.UserAuthMethodTypeTOTP,
|
||||
"ro-id",
|
||||
"",
|
||||
"instance-id",
|
||||
@ -242,6 +242,256 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceAddedOTPSMS",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
repository.EventType(user.HumanOTPSMSAddedType),
|
||||
user.AggregateType,
|
||||
nil,
|
||||
), eventstore.GenericEventMapper[user.HumanOTPSMSAddedEvent]),
|
||||
},
|
||||
reduce: (&userAuthMethodProjection{}).reduceAddAuthMethod,
|
||||
want: wantReduce{
|
||||
aggregateType: user.AggregateType,
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.user_auth_methods4 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
|
||||
expectedArgs: []interface{}{
|
||||
"",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
"ro-id",
|
||||
"instance-id",
|
||||
"agg-id",
|
||||
uint64(15),
|
||||
domain.MFAStateReady,
|
||||
domain.UserAuthMethodTypeOTPSMS,
|
||||
"",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceAddedOTPEmail",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
repository.EventType(user.HumanOTPEmailAddedType),
|
||||
user.AggregateType,
|
||||
nil,
|
||||
), eventstore.GenericEventMapper[user.HumanOTPEmailAddedEvent]),
|
||||
},
|
||||
reduce: (&userAuthMethodProjection{}).reduceAddAuthMethod,
|
||||
want: wantReduce{
|
||||
aggregateType: user.AggregateType,
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.user_auth_methods4 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
|
||||
expectedArgs: []interface{}{
|
||||
"",
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
"ro-id",
|
||||
"instance-id",
|
||||
"agg-id",
|
||||
uint64(15),
|
||||
domain.MFAStateReady,
|
||||
domain.UserAuthMethodTypeOTPEmail,
|
||||
"",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceRemoveOTPPasswordless",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
repository.EventType(user.HumanPasswordlessTokenRemovedType),
|
||||
user.AggregateType,
|
||||
[]byte(`{
|
||||
"webAuthNTokenId": "token-id"
|
||||
}`),
|
||||
), user.HumanPasswordlessRemovedEventMapper),
|
||||
},
|
||||
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
|
||||
want: wantReduce{
|
||||
aggregateType: user.AggregateType,
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4) AND (token_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
domain.UserAuthMethodTypePasswordless,
|
||||
"ro-id",
|
||||
"instance-id",
|
||||
"token-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceRemoveOTPU2F",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
repository.EventType(user.HumanU2FTokenRemovedType),
|
||||
user.AggregateType,
|
||||
[]byte(`{
|
||||
"webAuthNTokenId": "token-id"
|
||||
}`),
|
||||
), user.HumanU2FRemovedEventMapper),
|
||||
},
|
||||
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
|
||||
want: wantReduce{
|
||||
aggregateType: user.AggregateType,
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4) AND (token_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
domain.UserAuthMethodTypeU2F,
|
||||
"ro-id",
|
||||
"instance-id",
|
||||
"token-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceRemoveTOTP",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
repository.EventType(user.HumanMFAOTPRemovedType),
|
||||
user.AggregateType,
|
||||
nil,
|
||||
), user.HumanOTPRemovedEventMapper),
|
||||
},
|
||||
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
|
||||
want: wantReduce{
|
||||
aggregateType: user.AggregateType,
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
domain.UserAuthMethodTypeTOTP,
|
||||
"ro-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceRemoveOTPSMS",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
repository.EventType(user.HumanOTPSMSRemovedType),
|
||||
user.AggregateType,
|
||||
nil,
|
||||
), eventstore.GenericEventMapper[user.HumanOTPSMSRemovedEvent]),
|
||||
},
|
||||
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
|
||||
want: wantReduce{
|
||||
aggregateType: user.AggregateType,
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
domain.UserAuthMethodTypeOTPSMS,
|
||||
"ro-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceRemovePhone",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
repository.EventType(user.HumanPhoneRemovedType),
|
||||
user.AggregateType,
|
||||
nil,
|
||||
), user.HumanPhoneRemovedEventMapper),
|
||||
},
|
||||
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
|
||||
want: wantReduce{
|
||||
aggregateType: user.AggregateType,
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
domain.UserAuthMethodTypeOTPSMS,
|
||||
"ro-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reduceRemoveOTPEmail",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
repository.EventType(user.HumanOTPEmailRemovedType),
|
||||
user.AggregateType,
|
||||
nil,
|
||||
), eventstore.GenericEventMapper[user.HumanOTPEmailRemovedEvent]),
|
||||
},
|
||||
reduce: (&userAuthMethodProjection{}).reduceRemoveAuthMethod,
|
||||
want: wantReduce{
|
||||
aggregateType: user.AggregateType,
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
domain.UserAuthMethodTypeOTPEmail,
|
||||
"ro-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "org reduceOwnerRemoved",
|
||||
reduce: (&userAuthMethodProjection{}).reduceOwnerRemoved,
|
||||
|
@ -280,7 +280,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
|
||||
},
|
||||
{
|
||||
true,
|
||||
domain.UserAuthMethodTypeOTP,
|
||||
domain.UserAuthMethodTypeTOTP,
|
||||
1,
|
||||
},
|
||||
},
|
||||
@ -292,7 +292,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
|
||||
},
|
||||
AuthMethodTypes: []domain.UserAuthMethodType{
|
||||
domain.UserAuthMethodTypePasswordless,
|
||||
domain.UserAuthMethodTypeOTP,
|
||||
domain.UserAuthMethodTypeTOTP,
|
||||
domain.UserAuthMethodTypePassword,
|
||||
domain.UserAuthMethodTypeIDP,
|
||||
},
|
||||
@ -399,7 +399,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
|
||||
},
|
||||
{
|
||||
true,
|
||||
domain.UserAuthMethodTypeOTP,
|
||||
domain.UserAuthMethodTypeTOTP,
|
||||
1,
|
||||
true,
|
||||
true,
|
||||
@ -411,7 +411,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
|
||||
object: &testUserAuthMethodTypesRequired{
|
||||
authMethods: []domain.UserAuthMethodType{
|
||||
domain.UserAuthMethodTypePasswordless,
|
||||
domain.UserAuthMethodTypeOTP,
|
||||
domain.UserAuthMethodTypeTOTP,
|
||||
domain.UserAuthMethodTypePassword,
|
||||
domain.UserAuthMethodTypeIDP,
|
||||
},
|
||||
|
@ -88,6 +88,14 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
|
||||
RegisterFilterEventMapper(AggregateType, HumanMFAOTPRemovedType, HumanOTPRemovedEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, HumanMFAOTPCheckSucceededType, HumanOTPCheckSucceededEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, HumanMFAOTPCheckFailedType, HumanOTPCheckFailedEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, HumanOTPSMSAddedType, eventstore.GenericEventMapper[HumanOTPSMSAddedEvent]).
|
||||
RegisterFilterEventMapper(AggregateType, HumanOTPSMSRemovedType, eventstore.GenericEventMapper[HumanOTPSMSRemovedEvent]).
|
||||
RegisterFilterEventMapper(AggregateType, HumanOTPSMSCheckSucceededType, eventstore.GenericEventMapper[HumanOTPSMSCheckSucceededEvent]).
|
||||
RegisterFilterEventMapper(AggregateType, HumanOTPSMSCheckFailedType, eventstore.GenericEventMapper[HumanOTPSMSCheckFailedEvent]).
|
||||
RegisterFilterEventMapper(AggregateType, HumanOTPEmailAddedType, eventstore.GenericEventMapper[HumanOTPEmailAddedEvent]).
|
||||
RegisterFilterEventMapper(AggregateType, HumanOTPEmailRemovedType, eventstore.GenericEventMapper[HumanOTPEmailRemovedEvent]).
|
||||
RegisterFilterEventMapper(AggregateType, HumanOTPEmailCheckSucceededType, eventstore.GenericEventMapper[HumanOTPEmailCheckSucceededEvent]).
|
||||
RegisterFilterEventMapper(AggregateType, HumanOTPEmailCheckFailedType, eventstore.GenericEventMapper[HumanOTPEmailCheckFailedEvent]).
|
||||
RegisterFilterEventMapper(AggregateType, HumanU2FTokenAddedType, HumanU2FAddedEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, HumanU2FTokenVerifiedType, HumanU2FVerifiedEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, HumanU2FTokenSignCountChangedType, HumanU2FSignCountChangedEventMapper).
|
||||
|
@ -12,12 +12,22 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
otpEventPrefix = mfaEventPrefix + "otp."
|
||||
HumanMFAOTPAddedType = otpEventPrefix + "added"
|
||||
HumanMFAOTPVerifiedType = otpEventPrefix + "verified"
|
||||
HumanMFAOTPRemovedType = otpEventPrefix + "removed"
|
||||
HumanMFAOTPCheckSucceededType = otpEventPrefix + "check.succeeded"
|
||||
HumanMFAOTPCheckFailedType = otpEventPrefix + "check.failed"
|
||||
otpEventPrefix = mfaEventPrefix + "otp."
|
||||
HumanMFAOTPAddedType = otpEventPrefix + "added"
|
||||
HumanMFAOTPVerifiedType = otpEventPrefix + "verified"
|
||||
HumanMFAOTPRemovedType = otpEventPrefix + "removed"
|
||||
HumanMFAOTPCheckSucceededType = otpEventPrefix + "check.succeeded"
|
||||
HumanMFAOTPCheckFailedType = otpEventPrefix + "check.failed"
|
||||
otpSMSEventPrefix = otpEventPrefix + "sms."
|
||||
HumanOTPSMSAddedType = otpSMSEventPrefix + "added"
|
||||
HumanOTPSMSRemovedType = otpSMSEventPrefix + "removed"
|
||||
HumanOTPSMSCheckSucceededType = otpSMSEventPrefix + "check.succeeded"
|
||||
HumanOTPSMSCheckFailedType = otpSMSEventPrefix + "check.failed"
|
||||
otpEmailEventPrefix = otpEventPrefix + "email."
|
||||
HumanOTPEmailAddedType = otpEmailEventPrefix + "added"
|
||||
HumanOTPEmailRemovedType = otpEmailEventPrefix + "removed"
|
||||
HumanOTPEmailCheckSucceededType = otpEmailEventPrefix + "check.succeeded"
|
||||
HumanOTPEmailCheckFailedType = otpEmailEventPrefix + "check.failed"
|
||||
)
|
||||
|
||||
type HumanOTPAddedEvent struct {
|
||||
@ -202,3 +212,247 @@ func HumanOTPCheckFailedEventMapper(event *repository.Event) (eventstore.Event,
|
||||
}
|
||||
return otpAdded, nil
|
||||
}
|
||||
|
||||
type HumanOTPSMSAddedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSAddedEvent) Data() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *event
|
||||
}
|
||||
|
||||
func NewHumanOTPSMSAddedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *HumanOTPSMSAddedEvent {
|
||||
return &HumanOTPSMSAddedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
HumanOTPSMSAddedType,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type HumanOTPSMSRemovedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSRemovedEvent) Data() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *event
|
||||
}
|
||||
|
||||
func NewHumanOTPSMSRemovedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *HumanOTPSMSRemovedEvent {
|
||||
return &HumanOTPSMSRemovedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
HumanOTPSMSRemovedType,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type HumanOTPSMSCheckSucceededEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
*AuthRequestInfo
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSCheckSucceededEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSCheckSucceededEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSCheckSucceededEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *event
|
||||
}
|
||||
|
||||
func NewHumanOTPSMSCheckSucceededEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
info *AuthRequestInfo,
|
||||
) *HumanOTPSMSCheckSucceededEvent {
|
||||
return &HumanOTPSMSCheckSucceededEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
HumanOTPSMSCheckSucceededType,
|
||||
),
|
||||
AuthRequestInfo: info,
|
||||
}
|
||||
}
|
||||
|
||||
type HumanOTPSMSCheckFailedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
*AuthRequestInfo
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSCheckFailedEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSCheckFailedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPSMSCheckFailedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *event
|
||||
}
|
||||
|
||||
func NewHumanOTPSMSCheckFailedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
info *AuthRequestInfo,
|
||||
) *HumanOTPSMSCheckFailedEvent {
|
||||
return &HumanOTPSMSCheckFailedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
HumanOTPSMSCheckFailedType,
|
||||
),
|
||||
AuthRequestInfo: info,
|
||||
}
|
||||
}
|
||||
|
||||
type HumanOTPEmailAddedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailAddedEvent) Data() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *event
|
||||
}
|
||||
|
||||
func NewHumanOTPEmailAddedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *HumanOTPEmailAddedEvent {
|
||||
return &HumanOTPEmailAddedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
HumanOTPEmailAddedType,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type HumanOTPEmailRemovedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailRemovedEvent) Data() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailRemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *event
|
||||
}
|
||||
|
||||
func NewHumanOTPEmailRemovedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
) *HumanOTPEmailRemovedEvent {
|
||||
return &HumanOTPEmailRemovedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
HumanOTPEmailRemovedType,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type HumanOTPEmailCheckSucceededEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
*AuthRequestInfo
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailCheckSucceededEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailCheckSucceededEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailCheckSucceededEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *event
|
||||
}
|
||||
|
||||
func NewHumanOTPEmailCheckSucceededEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
info *AuthRequestInfo,
|
||||
) *HumanOTPEmailCheckSucceededEvent {
|
||||
return &HumanOTPEmailCheckSucceededEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
HumanOTPEmailCheckSucceededType,
|
||||
),
|
||||
AuthRequestInfo: info,
|
||||
}
|
||||
}
|
||||
|
||||
type HumanOTPEmailCheckFailedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
*AuthRequestInfo
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailCheckFailedEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailCheckFailedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanOTPEmailCheckFailedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *event
|
||||
}
|
||||
|
||||
func NewHumanOTPEmailCheckFailedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
info *AuthRequestInfo,
|
||||
) *HumanOTPEmailCheckFailedEvent {
|
||||
return &HumanOTPEmailCheckFailedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
HumanOTPEmailCheckFailedType,
|
||||
),
|
||||
AuthRequestInfo: info,
|
||||
}
|
||||
}
|
||||
|
@ -158,7 +158,7 @@ func (u *UserView) MFATypesSetupPossible(level domain.MFALevel, policy *domain.L
|
||||
switch mfaType {
|
||||
case domain.SecondFactorTypeTOTP:
|
||||
if u.OTPState != MFAStateReady {
|
||||
types = append(types, domain.MFATypeOTP)
|
||||
types = append(types, domain.MFATypeTOTP)
|
||||
}
|
||||
case domain.SecondFactorTypeU2F:
|
||||
types = append(types, domain.MFATypeU2F)
|
||||
@ -183,7 +183,7 @@ func (u *UserView) MFATypesAllowed(level domain.MFALevel, policy *domain.LoginPo
|
||||
switch mfaType {
|
||||
case domain.SecondFactorTypeTOTP:
|
||||
if u.OTPState == MFAStateReady {
|
||||
types = append(types, domain.MFATypeOTP)
|
||||
types = append(types, domain.MFATypeTOTP)
|
||||
}
|
||||
case domain.SecondFactorTypeU2F:
|
||||
if u.IsU2FReady() {
|
||||
|
@ -134,11 +134,11 @@ func (v *UserSessionView) AppendEvent(event *models.Event) error {
|
||||
return err
|
||||
}
|
||||
if v.UserAgentID == data.UserAgentID {
|
||||
v.setSecondFactorVerification(event.CreationDate, domain.MFATypeOTP)
|
||||
v.setSecondFactorVerification(event.CreationDate, domain.MFATypeTOTP)
|
||||
}
|
||||
case user.UserV1MFAOTPCheckSucceededType,
|
||||
user.HumanMFAOTPCheckSucceededType:
|
||||
v.setSecondFactorVerification(event.CreationDate, domain.MFATypeOTP)
|
||||
v.setSecondFactorVerification(event.CreationDate, domain.MFATypeTOTP)
|
||||
case user.UserV1MFAOTPCheckFailedType,
|
||||
user.UserV1MFAOTPRemovedType,
|
||||
user.HumanMFAOTPCheckFailedType,
|
||||
|
@ -647,6 +647,70 @@ service AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
rpc AddMyAuthFactorOTPSMS(AddMyAuthFactorOTPSMSRequest) returns (AddMyAuthFactorOTPSMSResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/users/me/auth_factors/otp_sms"
|
||||
body: "*"
|
||||
};
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "authenticated"
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
tags: "User Authentication Factor"
|
||||
summary: "Add One-Time-Password (OTP) SMS";
|
||||
description: "Add a new One-Time-Password (OTP) SMS factor to the authenticated user. OTP SMS will enable the user to verify a OTP with the latest verified phone number. The phone number has to be verified to add the second factor."
|
||||
};
|
||||
}
|
||||
|
||||
rpc RemoveMyAuthFactorOTPSMS(RemoveMyAuthFactorOTPSMSRequest) returns (RemoveMyAuthFactorOTPSMSResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/users/me/auth_factors/otp_sms"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "authenticated"
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
tags: "User Authentication Factor"
|
||||
summary: "Remove One-Time-Password (OTP) SMS";
|
||||
description: "Remove the configured One-Time-Password (OTP) SMS factor of the authenticated user. As only one OTP SMS per user is allowed, the user will not have OTP SMS as a second-factor afterward."
|
||||
};
|
||||
}
|
||||
|
||||
rpc AddMyAuthFactorOTPEmail(AddMyAuthFactorOTPEmailRequest) returns (AddMyAuthFactorOTPEmailResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/users/me/auth_factors/otp_email"
|
||||
body: "*"
|
||||
};
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "authenticated"
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
tags: "User Authentication Factor"
|
||||
summary: "Add One-Time-Password (OTP) Email";
|
||||
description: "Add a new One-Time-Password (OTP) Email factor to the authenticated user. OTP Email will enable the user to verify a OTP with the latest verified email. The email has to be verified to add the second factor."
|
||||
};
|
||||
}
|
||||
|
||||
rpc RemoveMyAuthFactorOTPEmail(RemoveMyAuthFactorOTPEmailRequest) returns (RemoveMyAuthFactorOTPEmailResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/users/me/auth_factors/otp_email"
|
||||
};
|
||||
|
||||
option (zitadel.v1.auth_option) = {
|
||||
permission: "authenticated"
|
||||
};
|
||||
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
tags: "User Authentication Factor"
|
||||
summary: "Remove One-Time-Password (OTP) Email";
|
||||
description: "Remove the configured One-Time-Password (OTP) Email factor of the authenticated user. As only one OTP Email per user is allowed, the user will not have OTP Email as a second-factor afterward."
|
||||
};
|
||||
}
|
||||
|
||||
rpc AddMyAuthFactorU2F(AddMyAuthFactorU2FRequest) returns (AddMyAuthFactorU2FResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/users/me/auth_factors/u2f"
|
||||
@ -1340,6 +1404,34 @@ message RemoveMyAuthFactorOTPResponse {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
}
|
||||
|
||||
//This is an empty request
|
||||
message AddMyAuthFactorOTPSMSRequest {}
|
||||
|
||||
message AddMyAuthFactorOTPSMSResponse {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
}
|
||||
|
||||
//This is an empty request
|
||||
message RemoveMyAuthFactorOTPSMSRequest {}
|
||||
|
||||
message RemoveMyAuthFactorOTPSMSResponse {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
}
|
||||
|
||||
//This is an empty request
|
||||
message AddMyAuthFactorOTPEmailRequest {}
|
||||
|
||||
message AddMyAuthFactorOTPEmailResponse {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
}
|
||||
|
||||
//This is an empty request
|
||||
message RemoveMyAuthFactorOTPEmailRequest {}
|
||||
|
||||
message RemoveMyAuthFactorOTPEmailResponse {
|
||||
zitadel.v1.ObjectDetails details = 1;
|
||||
}
|
||||
|
||||
message RemoveMyAuthFactorU2FRequest {
|
||||
string token_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
}
|
||||
|
@ -342,12 +342,22 @@ message AuthFactor {
|
||||
oneof type {
|
||||
AuthFactorOTP otp = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "one type use OTP or U2F"
|
||||
description: "one type use OTP, OTPSMS, OTPEmail or U2F"
|
||||
}
|
||||
];
|
||||
AuthFactorU2F u2f = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "one type use OTP or U2F"
|
||||
description: "one type use OTP, OTPSMS, OTPEmail or U2F"
|
||||
}
|
||||
];
|
||||
AuthFactorOTPSMS otp_sms = 4 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "one type use OTP, OTPSMS, OTPEmail or U2F"
|
||||
}
|
||||
];
|
||||
AuthFactorOTPEmail otp_email = 5 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "one type use OTP, OTPSMS, OTPEmail or U2F"
|
||||
}
|
||||
];
|
||||
}
|
||||
@ -361,6 +371,8 @@ enum AuthFactorState {
|
||||
}
|
||||
|
||||
message AuthFactorOTP {}
|
||||
message AuthFactorOTPSMS {}
|
||||
message AuthFactorOTPEmail {}
|
||||
|
||||
message AuthFactorU2F {
|
||||
string id = 1 [
|
||||
|
@ -317,6 +317,96 @@ service UserService {
|
||||
};
|
||||
}
|
||||
|
||||
rpc AddOTPSMS (AddOTPSMSRequest) returns (AddOTPSMSResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v2alpha/users/{user_id}/otp_sms"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Add OTP SMS for a user";
|
||||
description: "Add a new One-Time-Password (OTP) SMS factor to the authenticated user. OTP SMS will enable the user to verify a OTP with the latest verified phone number. The phone number has to be verified to add the second factor."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc RemoveOTPSMS (RemoveOTPSMSRequest) returns (RemoveOTPSMSResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/v2alpha/users/{user_id}/otp_sms"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Remove One-Time-Password (OTP) SMS from a user";
|
||||
description: "Remove the configured One-Time-Password (OTP) SMS factor of the authenticated user. As only one OTP SMS per user is allowed, the user will not have OTP SMS as a second-factor afterward."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc AddOTPEmail (AddOTPEmailRequest) returns (AddOTPEmailResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v2alpha/users/{user_id}/otp_email"
|
||||
body: "*"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Add OTP Email for a user";
|
||||
description: "Add a new One-Time-Password (OTP) Email factor to the authenticated user. OTP Email will enable the user to verify a OTP with the latest verified email. The email has to be verified to add the second factor."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
rpc RemoveOTPEmail (RemoveOTPEmailRequest) returns (RemoveOTPEmailResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/v2alpha/users/{user_id}/otp_email"
|
||||
};
|
||||
|
||||
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||
auth_option: {
|
||||
permission: "authenticated"
|
||||
}
|
||||
};
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
summary: "Remove One-Time-Password (OTP) Email from a user";
|
||||
description: "Remove the configured One-Time-Password (OTP) Email factor of the authenticated user. As only one OTP Email per user is allowed, the user will not have OTP Email as a second-factor afterward."
|
||||
responses: {
|
||||
key: "200"
|
||||
value: {
|
||||
description: "OK";
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Start an IDP authentication (for external login, registration or linking)
|
||||
rpc StartIdentityProviderFlow (StartIdentityProviderFlowRequest) returns (StartIdentityProviderFlowResponse) {
|
||||
option (google.api.http) = {
|
||||
@ -779,6 +869,70 @@ message VerifyTOTPRegistrationResponse {
|
||||
zitadel.object.v2alpha.Details details = 1;
|
||||
}
|
||||
|
||||
message AddOTPSMSRequest {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"163840776835432705\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message AddOTPSMSResponse {
|
||||
zitadel.object.v2alpha.Details details = 1;
|
||||
}
|
||||
|
||||
message RemoveOTPSMSRequest {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"163840776835432705\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message RemoveOTPSMSResponse {
|
||||
zitadel.object.v2alpha.Details details = 1;
|
||||
}
|
||||
|
||||
message AddOTPEmailRequest {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"163840776835432705\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message AddOTPEmailResponse {
|
||||
zitadel.object.v2alpha.Details details = 1;
|
||||
}
|
||||
|
||||
message RemoveOTPEmailRequest {
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"163840776835432705\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message RemoveOTPEmailResponse {
|
||||
zitadel.object.v2alpha.Details details = 1;
|
||||
}
|
||||
|
||||
message CreatePasskeyRegistrationLinkRequest{
|
||||
string user_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
@ -979,4 +1133,6 @@ enum AuthenticationMethodType {
|
||||
AUTHENTICATION_METHOD_TYPE_IDP = 3;
|
||||
AUTHENTICATION_METHOD_TYPE_TOTP = 4;
|
||||
AUTHENTICATION_METHOD_TYPE_U2F = 5;
|
||||
AUTHENTICATION_METHOD_TYPE_OTP_SMS = 6;
|
||||
AUTHENTICATION_METHOD_TYPE_OTP_EMAIL = 7;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user