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:
Livio Spring
2023-08-02 18:57:53 +02:00
committed by GitHub
parent ca13e70c92
commit a1942ecdaa
44 changed files with 2253 additions and 215 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View 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
}

View 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)
})
}
}

View File

@@ -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:

View File

@@ -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) {

View File

@@ -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

View File

@@ -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{

View File

@@ -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:

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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:

View File

@@ -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