mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 07:57:32 +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:
@@ -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
|
||||
|
Reference in New Issue
Block a user